refactor: replace fully-qualified names with proper imports across codebase

This commit is contained in:
Garfie 2026-04-08 19:15:49 -05:00
parent 2f95d923e7
commit e8bda6ca05
59 changed files with 409 additions and 216 deletions

View file

@ -24,6 +24,7 @@ import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
class LibreChatApplication : Application(), SingletonImageLoader.Factory { class LibreChatApplication : Application(), SingletonImageLoader.Factory {
@ -33,7 +34,7 @@ class LibreChatApplication : Application(), SingletonImageLoader.Factory {
try { try {
startKoin { startKoin {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
androidLogger(org.koin.core.logger.Level.DEBUG) androidLogger(Level.DEBUG)
} }
androidContext(this@LibreChatApplication) androidContext(this@LibreChatApplication)
allowOverride(false) allowOverride(false)

View file

@ -47,7 +47,7 @@ class MainActivity : ComponentActivity() {
private val connectivityObserver: ConnectivityObserver by inject() private val connectivityObserver: ConnectivityObserver by inject()
private val themeDataStore: ThemeDataStore by inject() private val themeDataStore: ThemeDataStore by inject()
private var deepLinkUri by mutableStateOf<android.net.Uri?>(null) private var deepLinkUri by mutableStateOf<Uri?>(null)
/** /**
* Incremented each time a share intent arrives. * Incremented each time a share intent arrives.

View file

@ -26,6 +26,7 @@ import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.garfiec.librechat.core.model.Banner
import com.garfiec.librechat.core.ui.components.BannerDisplay import com.garfiec.librechat.core.ui.components.BannerDisplay
import com.garfiec.librechat.shared.navigation.NavHostViewModel import com.garfiec.librechat.shared.navigation.NavHostViewModel
import com.garfiec.librechat.shared.navigation.Navigator import com.garfiec.librechat.shared.navigation.Navigator
@ -226,7 +227,7 @@ private fun MainContent(
navigator: Navigator, navigator: Navigator,
navHostViewModel: NavHostViewModel, navHostViewModel: NavHostViewModel,
isInAuthFlow: Boolean, isInAuthFlow: Boolean,
banners: List<com.garfiec.librechat.core.model.Banner>, banners: List<Banner>,
dismissedBannerIds: Set<String>, dismissedBannerIds: Set<String>,
onToggleDrawer: () -> Unit, onToggleDrawer: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View file

@ -1,5 +1,8 @@
package com.garfiec.librechat package com.garfiec.librechat
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.common.di.commonModule import com.garfiec.librechat.core.common.di.commonModule
import com.garfiec.librechat.core.common.network.ConnectivityObserver import com.garfiec.librechat.core.common.network.ConnectivityObserver
import com.garfiec.librechat.core.data.datastore.ConfigCacheDataStore import com.garfiec.librechat.core.data.datastore.ConfigCacheDataStore
@ -65,7 +68,9 @@ import com.garfiec.librechat.feature.settings.di.settingsModule
import com.garfiec.librechat.shared.navigation.sharedAppModule import com.garfiec.librechat.shared.navigation.sharedAppModule
import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import java.io.File
import org.junit.Test import org.junit.Test
import org.koin.core.annotation.KoinExperimentalAPI
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.test.verify.verify import org.koin.test.verify.verify
import kotlin.reflect.KClass import kotlin.reflect.KClass
@ -93,14 +98,14 @@ class KoinGraphVerificationTest {
* framework types so every module is verified in a single test class. * framework types so every module is verified in a single test class.
* If a type is renamed or removed, this test will catch it. * If a type is renamed or removed, this test will catch it.
*/ */
@OptIn(org.koin.core.annotation.KoinExperimentalAPI::class) @OptIn(KoinExperimentalAPI::class)
@Test @Test
fun verifyFullKoinGraph() { fun verifyFullKoinGraph() {
val extraTypes = mutableListOf<KClass<*>>( val extraTypes = mutableListOf<KClass<*>>(
// Android framework // Android framework
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
// core:common provides // core:common provides
CoroutineDispatcher::class, CoroutineDispatcher::class,
CoroutineScope::class, CoroutineScope::class,
@ -162,8 +167,8 @@ class KoinGraphVerificationTest {
// feature:files platform provides // feature:files platform provides
FileReader::class, FileReader::class,
// Wrappers/DSL types that verify can't resolve via constructor // Wrappers/DSL types that verify can't resolve via constructor
kotlin.Lazy::class, Lazy::class,
java.io.File::class, File::class,
) )
// Types whose libraries aren't on the app test classpath (transitive // Types whose libraries aren't on the app test classpath (transitive

View file

@ -1,5 +1,7 @@
package com.garfiec.librechat.core.common.di package com.garfiec.librechat.core.common.di
import android.app.Application
import android.content.Context
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,8 +10,8 @@ class CommonModuleVerificationTest {
fun verifyCommonModule() { fun verifyCommonModule() {
commonModule.verify( commonModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
), ),
) )
} }

View file

@ -7,6 +7,7 @@ import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -250,7 +251,7 @@ class RoomMigrationTest {
).build() ).build()
// Verify we can read the migrated data via DAO // Verify we can read the migrated data via DAO
val conversation = kotlinx.coroutines.runBlocking { val conversation = runBlocking {
roomDb.conversationDao().getById("conv-api-test") roomDb.conversationDao().getById("conv-api-test")
} }
assertThat(conversation).isNotNull() assertThat(conversation).isNotNull()

View file

@ -1,5 +1,6 @@
package com.garfiec.librechat.core.data.di package com.garfiec.librechat.core.data.di
import android.content.Context
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
@ -17,7 +18,7 @@ import org.koin.core.module.Module
import org.koin.dsl.binds import org.koin.dsl.binds
import org.koin.dsl.module import org.koin.dsl.module
private val android.content.Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore( private val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = DATASTORE_FILE_NAME, name = DATASTORE_FILE_NAME,
) )

View file

@ -1,5 +1,35 @@
package com.garfiec.librechat.core.data.di package com.garfiec.librechat.core.data.di
import android.app.Application
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.common.network.ConnectivityObserver
import com.garfiec.librechat.core.network.api.AgentsApi
import com.garfiec.librechat.core.network.api.ApiKeysApi
import com.garfiec.librechat.core.network.api.AuthApi
import com.garfiec.librechat.core.network.api.BalanceApi
import com.garfiec.librechat.core.network.api.BannerApi
import com.garfiec.librechat.core.network.api.ChatApi
import com.garfiec.librechat.core.network.api.ConfigApi
import com.garfiec.librechat.core.network.api.ConversationsApi
import com.garfiec.librechat.core.network.api.FilesApi
import com.garfiec.librechat.core.network.api.FilesExtApi
import com.garfiec.librechat.core.network.api.KeysApi
import com.garfiec.librechat.core.network.api.McpApi
import com.garfiec.librechat.core.network.api.MemoriesApi
import com.garfiec.librechat.core.network.api.MessagesApi
import com.garfiec.librechat.core.network.api.PresetsApi
import com.garfiec.librechat.core.network.api.PromptsApi
import com.garfiec.librechat.core.network.api.SearchApi
import com.garfiec.librechat.core.network.api.ShareApi
import com.garfiec.librechat.core.network.api.SpeechApi
import com.garfiec.librechat.core.network.api.TagsApi
import com.garfiec.librechat.core.network.api.UserApi
import com.garfiec.librechat.core.network.sse.SseClient
import io.ktor.client.HttpClient
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,37 +38,37 @@ class DataModuleVerificationTest {
fun verifyDataModule() { fun verifyDataModule() {
dataModule.verify( dataModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
kotlin.Lazy::class, Lazy::class,
io.ktor.client.HttpClient::class, HttpClient::class,
kotlinx.serialization.json.Json::class, Json::class,
kotlinx.coroutines.CoroutineDispatcher::class, CoroutineDispatcher::class,
com.garfiec.librechat.core.common.network.ConnectivityObserver::class, ConnectivityObserver::class,
com.garfiec.librechat.core.network.sse.SseClient::class, SseClient::class,
com.garfiec.librechat.core.network.api.AuthApi::class, AuthApi::class,
com.garfiec.librechat.core.network.api.UserApi::class, UserApi::class,
com.garfiec.librechat.core.network.api.ChatApi::class, ChatApi::class,
com.garfiec.librechat.core.network.api.ConversationsApi::class, ConversationsApi::class,
com.garfiec.librechat.core.network.api.MessagesApi::class, MessagesApi::class,
com.garfiec.librechat.core.network.api.FilesApi::class, FilesApi::class,
com.garfiec.librechat.core.network.api.FilesExtApi::class, FilesExtApi::class,
com.garfiec.librechat.core.network.api.AgentsApi::class, AgentsApi::class,
com.garfiec.librechat.core.network.api.PresetsApi::class, PresetsApi::class,
com.garfiec.librechat.core.network.api.PromptsApi::class, PromptsApi::class,
com.garfiec.librechat.core.network.api.TagsApi::class, TagsApi::class,
com.garfiec.librechat.core.network.api.ShareApi::class, ShareApi::class,
com.garfiec.librechat.core.network.api.ConfigApi::class, ConfigApi::class,
com.garfiec.librechat.core.network.api.BalanceApi::class, BalanceApi::class,
com.garfiec.librechat.core.network.api.SearchApi::class, SearchApi::class,
com.garfiec.librechat.core.network.api.KeysApi::class, KeysApi::class,
com.garfiec.librechat.core.network.api.ApiKeysApi::class, ApiKeysApi::class,
com.garfiec.librechat.core.network.api.McpApi::class, McpApi::class,
com.garfiec.librechat.core.network.api.MemoriesApi::class, MemoriesApi::class,
com.garfiec.librechat.core.network.api.SpeechApi::class, SpeechApi::class,
com.garfiec.librechat.core.network.api.BannerApi::class, BannerApi::class,
androidx.datastore.core.DataStore::class, DataStore::class,
), ),
) )
} }

View file

@ -2,6 +2,7 @@ package com.garfiec.librechat.core.data.db.dao
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
import com.garfiec.librechat.core.data.db.entity.MessageEntity import com.garfiec.librechat.core.data.db.entity.MessageEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -29,7 +30,7 @@ interface MessageDao {
@Query("DELETE FROM messages WHERE conversationId = :conversationId") @Query("DELETE FROM messages WHERE conversationId = :conversationId")
suspend fun deleteAllForConversation(conversationId: String) suspend fun deleteAllForConversation(conversationId: String)
@androidx.room.Transaction @Transaction
suspend fun replaceAllForConversation(conversationId: String, messages: List<MessageEntity>) { suspend fun replaceAllForConversation(conversationId: String, messages: List<MessageEntity>) {
deleteAllForConversation(conversationId) deleteAllForConversation(conversationId)
upsertAll(messages) upsertAll(messages)

View file

@ -5,7 +5,9 @@ import com.garfiec.librechat.core.model.Conversation
import com.garfiec.librechat.core.model.EModelEndpoint import com.garfiec.librechat.core.model.EModelEndpoint
import kotlin.time.Clock import kotlin.time.Clock
import kotlin.time.Instant import kotlin.time.Instant
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
private val json = Json { ignoreUnknownKeys = true } private val json = Json { ignoreUnknownKeys = true }
@ -18,7 +20,7 @@ fun Conversation.toEntity(): ConversationEntity = ConversationEntity(
model = model, model = model,
agentId = agentId, agentId = agentId,
isArchived = isArchived, isArchived = isArchived,
tags = json.encodeToString(kotlinx.serialization.builtins.ListSerializer(kotlinx.serialization.serializer<String>()), tags), tags = json.encodeToString(ListSerializer(serializer<String>()), tags),
iconURL = iconURL, iconURL = iconURL,
greeting = greeting, greeting = greeting,
modelParams = null, modelParams = null,

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.core.data.datastore package com.garfiec.librechat.core.data.datastore
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.alloc import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped import kotlinx.cinterop.memScoped
@ -36,7 +37,7 @@ import platform.Security.kSecReturnData
import platform.Security.kSecValueData import platform.Security.kSecValueData
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
@OptIn(ExperimentalForeignApi::class, kotlinx.cinterop.BetaInteropApi::class) @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
class IosTokenDataStore( class IosTokenDataStore(
refreshClient: Lazy<HttpClient>, refreshClient: Lazy<HttpClient>,
) : CommonTokenDataStore(refreshClient), ServerUrlKeychainFallback { ) : CommonTokenDataStore(refreshClient), ServerUrlKeychainFallback {

View file

@ -14,16 +14,20 @@ import com.garfiec.librechat.core.data.repository.SessionCacheCleaner
import com.garfiec.librechat.core.network.client.SecureTokenStorage import com.garfiec.librechat.core.network.client.SecureTokenStorage
import com.garfiec.librechat.core.network.client.TokenManager import com.garfiec.librechat.core.network.client.TokenManager
import io.ktor.client.HttpClient import io.ktor.client.HttpClient
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.dsl.binds import org.koin.dsl.binds
import org.koin.dsl.module import org.koin.dsl.module
import platform.Foundation.NSCachesDirectory
import platform.Foundation.NSFileManager import platform.Foundation.NSFileManager
import platform.Foundation.NSHomeDirectory import platform.Foundation.NSHomeDirectory
import platform.Foundation.NSSearchPathForDirectoriesInDomains
import platform.Foundation.NSUserDomainMask
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
private fun ensureDirectoryExists(path: String) { private fun ensureDirectoryExists(path: String) {
NSFileManager.defaultManager.createDirectoryAtPath(path, withIntermediateDirectories = true, attributes = null, error = null) NSFileManager.defaultManager.createDirectoryAtPath(path, withIntermediateDirectories = true, attributes = null, error = null)
} }
@ -59,10 +63,10 @@ actual val dataPlatformModule: Module = module {
// --- Session Cache Cleaner --- // --- Session Cache Cleaner ---
single<SessionCacheCleaner> { single<SessionCacheCleaner> {
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class) @OptIn(ExperimentalForeignApi::class)
val cachePath = platform.Foundation.NSSearchPathForDirectoriesInDomains( val cachePath = NSSearchPathForDirectoriesInDomains(
platform.Foundation.NSCachesDirectory, NSCachesDirectory,
platform.Foundation.NSUserDomainMask, NSUserDomainMask,
true, true,
).firstOrNull() as? String ?: error("Unable to resolve NSCachesDirectory") ).firstOrNull() as? String ?: error("Unable to resolve NSCachesDirectory")
CommonSessionCacheCleaner(cachePath) CommonSessionCacheCleaner(cachePath)

View file

@ -1,5 +1,11 @@
package com.garfiec.librechat.core.network.di package com.garfiec.librechat.core.network.di
import android.app.Application
import android.content.Context
import com.garfiec.librechat.core.network.client.ServerUrlProvider
import com.garfiec.librechat.core.network.client.TokenManager
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.HttpClientEngineFactory
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,12 +14,12 @@ class NetworkModuleVerificationTest {
fun verifyNetworkModule() { fun verifyNetworkModule() {
networkModule.verify( networkModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
io.ktor.client.engine.HttpClientEngine::class, HttpClientEngine::class,
io.ktor.client.engine.HttpClientEngineFactory::class, HttpClientEngineFactory::class,
com.garfiec.librechat.core.network.client.TokenManager::class, TokenManager::class,
com.garfiec.librechat.core.network.client.ServerUrlProvider::class, ServerUrlProvider::class,
), ),
) )
} }

View file

@ -9,6 +9,7 @@ import io.ktor.client.plugins.HttpTimeout
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.plugins.defaultRequest import io.ktor.client.plugins.defaultRequest
import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.LogLevel
import io.ktor.client.plugins.logging.Logger as KtorLogger
import io.ktor.client.plugins.logging.Logging import io.ktor.client.plugins.logging.Logging
import io.ktor.client.statement.bodyAsText import io.ktor.client.statement.bodyAsText
import io.ktor.http.ContentType import io.ktor.http.ContentType
@ -36,7 +37,7 @@ object LibreChatHttpClient {
} }
install(Logging) { install(Logging) {
logger = object : io.ktor.client.plugins.logging.Logger { logger = object : KtorLogger {
override fun log(message: String) { override fun log(message: String) {
val sanitized = message val sanitized = message
.replace(Regex("Authorization: Bearer [^\\s]+"), "Authorization: Bearer [REDACTED]") .replace(Regex("Authorization: Bearer [^\\s]+"), "Authorization: Bearer [REDACTED]")

View file

@ -11,6 +11,7 @@ import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.http.isSuccess import io.ktor.http.isSuccess
import io.ktor.http.path import io.ktor.http.path
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -127,7 +128,7 @@ class SseClient(
shouldResume = true shouldResume = true
} }
} }
} catch (e: kotlinx.coroutines.CancellationException) { } catch (e: CancellationException) {
throw e // Re-throw cancellation — SKIE handles this gracefully throw e // Re-throw cancellation — SKIE handles this gracefully
} catch (e: Exception) { } catch (e: Exception) {
Logger.e("SSE", e) { "SSE: unhandled exception escaped flow" } Logger.e("SSE", e) { "SSE: unhandled exception escaped flow" }

View file

@ -3,6 +3,7 @@ package com.garfiec.librechat.core.network.sse
import com.garfiec.librechat.core.model.Conversation import com.garfiec.librechat.core.model.Conversation
import com.garfiec.librechat.core.model.Message import com.garfiec.librechat.core.model.Message
import com.garfiec.librechat.core.model.StreamEvent import com.garfiec.librechat.core.model.StreamEvent
import com.garfiec.librechat.core.model.content.MessageContentPart
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@ -210,7 +211,7 @@ class SseEventMapper(private val json: Json) {
aggregatedContent.mapNotNull { element -> aggregatedContent.mapNotNull { element ->
try { try {
json.decodeFromJsonElement( json.decodeFromJsonElement(
com.garfiec.librechat.core.model.content.MessageContentPart.serializer(), MessageContentPart.serializer(),
element, element,
) )
} catch (e: Exception) { } catch (e: Exception) {

View file

@ -3,6 +3,7 @@ package com.garfiec.librechat.core.network.sse
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import io.ktor.utils.io.ByteReadChannel import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readUTF8Line import io.ktor.utils.io.readUTF8Line
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
@ -21,7 +22,7 @@ class SseLineParser(
withTimeout(lineReadTimeoutMs) { withTimeout(lineReadTimeoutMs) {
channel.readUTF8Line() channel.readUTF8Line()
} }
} catch (e: kotlinx.coroutines.TimeoutCancellationException) { } catch (e: TimeoutCancellationException) {
Logger.w("SSE") { "SSE stream stalled: no data for ${lineReadTimeoutMs / 1000}s" } Logger.w("SSE") { "SSE stream stalled: no data for ${lineReadTimeoutMs / 1000}s" }
throw SseStreamException("Stream stalled: no data received for ${lineReadTimeoutMs / 1000}s", e) throw SseStreamException("Stream stalled: no data received for ${lineReadTimeoutMs / 1000}s", e)
} ?: break } ?: break

View file

@ -1,5 +1,12 @@
package com.garfiec.librechat.feature.agents.di package com.garfiec.librechat.feature.agents.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.data.datastore.ServerDataStore
import com.garfiec.librechat.core.data.repository.AgentRepository
import com.garfiec.librechat.core.data.repository.ConfigRepository
import com.garfiec.librechat.core.data.repository.McpRepository
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,13 +15,13 @@ class AgentsModuleVerificationTest {
fun verifyAgentsModule() { fun verifyAgentsModule() {
agentsModule.verify( agentsModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.AgentRepository::class, AgentRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class, ConfigRepository::class,
com.garfiec.librechat.core.data.repository.McpRepository::class, McpRepository::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class, ServerDataStore::class,
), ),
) )
} }

View file

@ -1,6 +1,8 @@
package com.garfiec.librechat.feature.agents.components.model package com.garfiec.librechat.feature.agents.components.model
@androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
@Immutable
data class AgentSharingState( data class AgentSharingState(
val visibility: AgentVisibility = AgentVisibility.PRIVATE, val visibility: AgentVisibility = AgentVisibility.PRIVATE,
val isCollaborative: Boolean = false, val isCollaborative: Boolean = false,

View file

@ -1,6 +1,8 @@
package com.garfiec.librechat.feature.agents.components.model package com.garfiec.librechat.feature.agents.components.model
@androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
@Immutable
data class SupportContactState( data class SupportContactState(
val name: String = "", val name: String = "",
val email: String = "", val email: String = "",

View file

@ -47,6 +47,7 @@ import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.garfiec.librechat.core.model.EModelEndpoint
import com.garfiec.librechat.core.ui.components.AvatarImage import com.garfiec.librechat.core.ui.components.AvatarImage
import com.garfiec.librechat.core.ui.components.endpointIconPainter import com.garfiec.librechat.core.ui.components.endpointIconPainter
import com.garfiec.librechat.core.ui.components.ErrorBanner import com.garfiec.librechat.core.ui.components.ErrorBanner
@ -216,7 +217,7 @@ fun AgentDetailScreen(
imageUrl = agent.avatarUrl, imageUrl = agent.avatarUrl,
size = 80.dp, size = 80.dp,
fallbackText = agent.name, fallbackText = agent.name,
fallbackIconPainter = endpointIconPainter(com.garfiec.librechat.core.model.EModelEndpoint.AGENTS), fallbackIconPainter = endpointIconPainter(EModelEndpoint.AGENTS),
tintIcon = true, tintIcon = true,
) )

View file

@ -45,6 +45,7 @@ import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.garfiec.librechat.core.model.EModelEndpoint
import com.garfiec.librechat.core.ui.components.AvatarImage import com.garfiec.librechat.core.ui.components.AvatarImage
import com.garfiec.librechat.core.ui.components.endpointIconPainter import com.garfiec.librechat.core.ui.components.endpointIconPainter
import com.garfiec.librechat.core.ui.components.EmptyState import com.garfiec.librechat.core.ui.components.EmptyState
@ -254,7 +255,7 @@ private fun AgentCard(
imageUrl = agent.avatarUrl, imageUrl = agent.avatarUrl,
size = 40.dp, size = 40.dp,
fallbackText = agent.name, fallbackText = agent.name,
fallbackIconPainter = endpointIconPainter(com.garfiec.librechat.core.model.EModelEndpoint.AGENTS), fallbackIconPainter = endpointIconPainter(EModelEndpoint.AGENTS),
tintIcon = true, tintIcon = true,
) )
} }

View file

@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.floatOrNull import kotlinx.serialization.json.floatOrNull
@ -820,7 +821,7 @@ class AgentEditorViewModel(
* Parse model_parameters JsonElement into AgentAdvancedSettings. * Parse model_parameters JsonElement into AgentAdvancedSettings.
* The backend stores these as: { "temperature": 0.7, "top_p": 0.9, "max_tokens": 4096 } * The backend stores these as: { "temperature": 0.7, "top_p": 0.9, "max_tokens": 4096 }
*/ */
private fun parseModelParameters(params: kotlinx.serialization.json.JsonElement?): AgentAdvancedSettings { private fun parseModelParameters(params: JsonElement?): AgentAdvancedSettings {
if (params == null) return AgentAdvancedSettings() if (params == null) return AgentAdvancedSettings()
return try { return try {
val obj = params.jsonObject val obj = params.jsonObject
@ -839,7 +840,7 @@ class AgentEditorViewModel(
* Returns null if no parameters are set (to avoid sending empty objects). * Returns null if no parameters are set (to avoid sending empty objects).
*/ */
private fun buildModelParameters(settings: AgentAdvancedSettings): JsonObject? { private fun buildModelParameters(settings: AgentAdvancedSettings): JsonObject? {
val map = mutableMapOf<String, kotlinx.serialization.json.JsonElement>() val map = mutableMapOf<String, JsonElement>()
settings.temperature?.let { map["temperature"] = JsonPrimitive(it) } settings.temperature?.let { map["temperature"] = JsonPrimitive(it) }
settings.topP?.let { map["top_p"] = JsonPrimitive(it) } settings.topP?.let { map["top_p"] = JsonPrimitive(it) }
settings.maxTokens?.let { map["max_tokens"] = JsonPrimitive(it) } settings.maxTokens?.let { map["max_tokens"] = JsonPrimitive(it) }
@ -921,13 +922,13 @@ class AgentEditorViewModel(
private fun Agent.parseSupportContact(): SupportContactState { private fun Agent.parseSupportContact(): SupportContactState {
val json = supportContact ?: return SupportContactState() val json = supportContact ?: return SupportContactState()
return try { return try {
val obj = json as? kotlinx.serialization.json.JsonObject val obj = json as? JsonObject
?: return SupportContactState() ?: return SupportContactState()
val name = obj["name"] val name = obj["name"]
?.let { (it as? kotlinx.serialization.json.JsonPrimitive)?.content } ?.let { (it as? JsonPrimitive)?.content }
?: "" ?: ""
val email = obj["email"] val email = obj["email"]
?.let { (it as? kotlinx.serialization.json.JsonPrimitive)?.content } ?.let { (it as? JsonPrimitive)?.content }
?: "" ?: ""
SupportContactState(name = name, email = email) SupportContactState(name = name, email = email)
} catch (_: Exception) { } catch (_: Exception) {

View file

@ -1,5 +1,14 @@
package com.garfiec.librechat.feature.auth.di package com.garfiec.librechat.feature.auth.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.data.datastore.ServerDataStore
import com.garfiec.librechat.core.data.repository.AuthRepository
import com.garfiec.librechat.core.data.repository.ConfigRepository
import com.garfiec.librechat.core.data.repository.UserRepository
import com.garfiec.librechat.core.network.client.SecureTokenStorage
import com.garfiec.librechat.feature.auth.oauth.OAuthLauncher
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,15 +17,15 @@ class AuthModuleVerificationTest {
fun verifyAuthModule() { fun verifyAuthModule() {
authModule.verify( authModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class, ServerDataStore::class,
com.garfiec.librechat.core.data.repository.AuthRepository::class, AuthRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class, ConfigRepository::class,
com.garfiec.librechat.core.data.repository.UserRepository::class, UserRepository::class,
com.garfiec.librechat.core.network.client.SecureTokenStorage::class, SecureTokenStorage::class,
com.garfiec.librechat.feature.auth.oauth.OAuthLauncher::class, OAuthLauncher::class,
), ),
) )
} }

View file

@ -2,6 +2,7 @@ package com.garfiec.librechat.feature.chat.components
import android.util.Base64 import android.util.Base64
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -22,7 +23,7 @@ import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
@androidx.annotation.OptIn(UnstableApi::class) @OptIn(UnstableApi::class)
@Composable @Composable
actual fun AudioContent( actual fun AudioContent(
data: String?, data: String?,

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.feature.chat.components package com.garfiec.librechat.feature.chat.components
import android.Manifest import android.Manifest
import android.content.Context
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
@ -406,7 +407,7 @@ fun ChatInput(
* Stored in the app's cache directory under `camera_photos/` which is * Stored in the app's cache directory under `camera_photos/` which is
* registered in the FileProvider paths XML. * registered in the FileProvider paths XML.
*/ */
private fun createCameraPhotoFile(context: android.content.Context): File { private fun createCameraPhotoFile(context: Context): File {
val cameraDir = File(context.cacheDir, "camera_photos") val cameraDir = File(context.cacheDir, "camera_photos")
if (!cameraDir.exists()) { if (!cameraDir.exists()) {
cameraDir.mkdirs() cameraDir.mkdirs()

View file

@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.chat.components
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.LayoutCoordinates
import com.garfiec.librechat.core.model.Attachment
import com.garfiec.librechat.core.model.content.MessageContentPart import com.garfiec.librechat.core.model.content.MessageContentPart
@Composable @Composable
@ -12,7 +13,7 @@ actual fun ContentPartRenderer(
baseUrl: String, baseUrl: String,
fontSizeMultiplier: Float, fontSizeMultiplier: Float,
useKatex: Boolean, useKatex: Boolean,
attachments: List<com.garfiec.librechat.core.model.Attachment>, attachments: List<Attachment>,
showImageDescriptions: Boolean, showImageDescriptions: Boolean,
searchQuery: String?, searchQuery: String?,
searchFocusedOccurrence: Int, searchFocusedOccurrence: Int,

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.feature.chat.components package com.garfiec.librechat.feature.chat.components
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.graphics.Color
import android.net.http.SslError import android.net.http.SslError
import android.view.ViewGroup import android.view.ViewGroup
import android.webkit.SslErrorHandler import android.webkit.SslErrorHandler
@ -178,7 +179,7 @@ private fun KatexLatexBlock(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
) )
setBackgroundColor(android.graphics.Color.TRANSPARENT) setBackgroundColor(Color.TRANSPARENT)
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
settings.allowFileAccess = false settings.allowFileAccess = false
settings.allowContentAccess = false settings.allowContentAccess = false
@ -251,7 +252,7 @@ private fun KatexLatexInline(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT,
) )
setBackgroundColor(android.graphics.Color.TRANSPARENT) setBackgroundColor(Color.TRANSPARENT)
settings.javaScriptEnabled = true settings.javaScriptEnabled = true
settings.allowFileAccess = false settings.allowFileAccess = false
settings.allowContentAccess = false settings.allowContentAccess = false

View file

@ -5,6 +5,7 @@ import android.app.Activity
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.speech.RecognizerIntent import android.speech.RecognizerIntent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
@ -201,7 +202,7 @@ actual fun ChatScreen(
} }
} else { } else {
// Device speech recognizer path (Device, Google, or Default without server) // Device speech recognizer path (Device, Google, or Default without server)
val intent = android.content.Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
putExtra( putExtra(
RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,

View file

@ -1,10 +1,13 @@
package com.garfiec.librechat.feature.chat.util package com.garfiec.librechat.feature.chat.util
import android.content.Context import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.provider.OpenableColumns import android.provider.OpenableColumns
import android.webkit.MimeTypeMap import android.webkit.MimeTypeMap
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import java.io.ByteArrayOutputStream
/** /**
* Resolves a human-readable filename from a content URI using the ContentResolver. * Resolves a human-readable filename from a content URI using the ContentResolver.
@ -224,7 +227,7 @@ internal fun reEncodeImageIfNeeded(bytes: ByteArray, mimeType: String): ReEncode
// converts non-matching extensions (e.g. .jpg when imageOutputType=png) // converts non-matching extensions (e.g. .jpg when imageOutputType=png)
// while storing the original MIME type, causing the Anthropic 400 error. // while storing the original MIME type, causing the Anthropic 400 error.
return try { return try {
val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size) val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
?: return null // BitmapFactory can't decode this format ?: return null // BitmapFactory can't decode this format
// If already PNG with correct magic bytes, skip re-encoding to save CPU/memory // If already PNG with correct magic bytes, skip re-encoding to save CPU/memory
@ -233,8 +236,8 @@ internal fun reEncodeImageIfNeeded(bytes: ByteArray, mimeType: String): ReEncode
return null return null
} }
val outputStream = java.io.ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, outputStream) bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
bitmap.recycle() bitmap.recycle()
val encodedBytes = outputStream.toByteArray() val encodedBytes = outputStream.toByteArray()

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.feature.chat.viewmodel.delegate package com.garfiec.librechat.feature.chat.viewmodel.delegate
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import com.garfiec.librechat.core.common.EndpointConstants import com.garfiec.librechat.core.common.EndpointConstants
import com.garfiec.librechat.core.common.result.Result import com.garfiec.librechat.core.common.result.Result
@ -134,10 +135,10 @@ class FileAttachmentDelegate(
var imageHeight: Int? = null var imageHeight: Int? = null
if (actualIsImage) { if (actualIsImage) {
try { try {
val options = android.graphics.BitmapFactory.Options().apply { val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true inJustDecodeBounds = true
} }
android.graphics.BitmapFactory.decodeByteArray(uploadBytes, 0, uploadBytes.size, options) BitmapFactory.decodeByteArray(uploadBytes, 0, uploadBytes.size, options)
if (options.outWidth > 0 && options.outHeight > 0) { if (options.outWidth > 0 && options.outHeight > 0) {
imageWidth = options.outWidth imageWidth = options.outWidth
imageHeight = options.outHeight imageHeight = options.outHeight

View file

@ -1,6 +1,8 @@
package com.garfiec.librechat.feature.chat.viewmodel.delegate package com.garfiec.librechat.feature.chat.viewmodel.delegate
import android.content.Context import android.content.Context
import android.media.MediaPlayer
import android.os.Bundle
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener import android.speech.tts.UtteranceProgressListener
import com.garfiec.librechat.core.common.result.Result import com.garfiec.librechat.core.common.result.Result
@ -9,6 +11,7 @@ import com.garfiec.librechat.core.data.repository.SpeechRepository
import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.io.File
class TextToSpeechDelegate( class TextToSpeechDelegate(
private val stateHandle: ChatStateHandle, private val stateHandle: ChatStateHandle,
@ -20,7 +23,7 @@ class TextToSpeechDelegate(
private var ttsEngine: TextToSpeech? = null private var ttsEngine: TextToSpeech? = null
private var ttsReady = false private var ttsReady = false
private var serverTtsPlayer: android.media.MediaPlayer? = null private var serverTtsPlayer: MediaPlayer? = null
private fun getOrInitTts(onReady: () -> Unit) { private fun getOrInitTts(onReady: () -> Unit) {
if (ttsReady && ttsEngine != null) { if (ttsReady && ttsEngine != null) {
@ -103,7 +106,7 @@ class TextToSpeechDelegate(
engine.setVoice(voice) engine.setVoice(voice)
} }
} }
val params = android.os.Bundle() val params = Bundle()
engine.speak(text, TextToSpeech.QUEUE_FLUSH, params, messageId) engine.speak(text, TextToSpeech.QUEUE_FLUSH, params, messageId)
} }
} }
@ -113,12 +116,12 @@ class TextToSpeechDelegate(
is Result.Success -> { is Result.Success -> {
try { try {
val audioBytes = result.data val audioBytes = result.data
val tempFile = java.io.File.createTempFile("tts_", ".mp3", appContext.cacheDir) val tempFile = File.createTempFile("tts_", ".mp3", appContext.cacheDir)
tempFile.deleteOnExit() tempFile.deleteOnExit()
tempFile.writeBytes(audioBytes) tempFile.writeBytes(audioBytes)
serverTtsPlayer?.release() serverTtsPlayer?.release()
serverTtsPlayer = android.media.MediaPlayer().apply { serverTtsPlayer = MediaPlayer().apply {
setDataSource(tempFile.absolutePath) setDataSource(tempFile.absolutePath)
setOnCompletionListener { setOnCompletionListener {
it.release() it.release()

View file

@ -1,5 +1,24 @@
package com.garfiec.librechat.feature.chat.di package com.garfiec.librechat.feature.chat.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.common.network.ConnectivityObserver
import com.garfiec.librechat.core.data.datastore.ServerDataStore
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
import com.garfiec.librechat.core.data.repository.AgentRepository
import com.garfiec.librechat.core.data.repository.ChatRepository
import com.garfiec.librechat.core.data.repository.ConfigRepository
import com.garfiec.librechat.core.data.repository.ConversationRepository
import com.garfiec.librechat.core.data.repository.DraftRepository
import com.garfiec.librechat.core.data.repository.FileRepository
import com.garfiec.librechat.core.data.repository.McpRepository
import com.garfiec.librechat.core.data.repository.MessageRepository
import com.garfiec.librechat.core.data.repository.PresetRepository
import com.garfiec.librechat.core.data.repository.PromptRepository
import com.garfiec.librechat.core.data.repository.ShareRepository
import com.garfiec.librechat.core.data.repository.SpeechRepository
import com.garfiec.librechat.core.data.repository.UserRepository
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,25 +27,25 @@ class ChatModuleVerificationTest {
fun verifyChatModule() { fun verifyChatModule() {
chatModule.verify( chatModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.AgentRepository::class, AgentRepository::class,
com.garfiec.librechat.core.data.repository.ChatRepository::class, ChatRepository::class,
com.garfiec.librechat.core.data.repository.MessageRepository::class, MessageRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class, ConfigRepository::class,
com.garfiec.librechat.core.data.repository.ConversationRepository::class, ConversationRepository::class,
com.garfiec.librechat.core.data.repository.DraftRepository::class, DraftRepository::class,
com.garfiec.librechat.core.data.repository.FileRepository::class, FileRepository::class,
com.garfiec.librechat.core.data.repository.PresetRepository::class, PresetRepository::class,
com.garfiec.librechat.core.data.repository.PromptRepository::class, PromptRepository::class,
com.garfiec.librechat.core.data.repository.ShareRepository::class, ShareRepository::class,
com.garfiec.librechat.core.data.repository.SpeechRepository::class, SpeechRepository::class,
com.garfiec.librechat.core.data.repository.McpRepository::class, McpRepository::class,
com.garfiec.librechat.core.data.repository.UserRepository::class, UserRepository::class,
com.garfiec.librechat.core.common.network.ConnectivityObserver::class, ConnectivityObserver::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class, ServerDataStore::class,
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class, SettingsDataStore::class,
), ),
) )
} }

View file

@ -48,6 +48,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
@ -203,7 +204,7 @@ private fun FilesChip(
@Composable @Composable
private fun ToolIndicatorChip( private fun ToolIndicatorChip(
label: String, label: String,
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: ImageVector,
semanticDescription: String, semanticDescription: String,
) { ) {
AssistChip( AssistChip(

View file

@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
import librechat_mobile.feature.chat.generated.resources.Res import librechat_mobile.feature.chat.generated.resources.Res
import librechat_mobile.feature.chat.generated.resources.* import librechat_mobile.feature.chat.generated.resources.*
import kotlin.time.Clock import kotlin.time.Clock
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.TimeZone import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime import kotlinx.datetime.toLocalDateTime
@ -114,7 +115,7 @@ private fun getTimeBasedGreeting(): String {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()) val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
val hour = now.hour val hour = now.hour
val dayOfWeek = now.dayOfWeek val dayOfWeek = now.dayOfWeek
val isWeekend = dayOfWeek == kotlinx.datetime.DayOfWeek.SATURDAY || dayOfWeek == kotlinx.datetime.DayOfWeek.SUNDAY val isWeekend = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY
if (isWeekend) { if (isWeekend) {
return when { return when {

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.LayoutCoordinates
import com.garfiec.librechat.core.common.ChatLayoutConstants import com.garfiec.librechat.core.common.ChatLayoutConstants
import com.garfiec.librechat.core.model.Attachment
import com.garfiec.librechat.core.model.Message import com.garfiec.librechat.core.model.Message
import com.garfiec.librechat.core.model.content.MessageContentPart import com.garfiec.librechat.core.model.content.MessageContentPart
@ -56,7 +57,7 @@ expect fun ContentPartRenderer(
baseUrl: String = "", baseUrl: String = "",
fontSizeMultiplier: Float = 1.0f, fontSizeMultiplier: Float = 1.0f,
useKatex: Boolean = false, useKatex: Boolean = false,
attachments: List<com.garfiec.librechat.core.model.Attachment> = emptyList(), attachments: List<Attachment> = emptyList(),
showImageDescriptions: Boolean = true, showImageDescriptions: Boolean = true,
searchQuery: String? = null, searchQuery: String? = null,
searchFocusedOccurrence: Int = -1, searchFocusedOccurrence: Int = -1,

View file

@ -1,12 +1,15 @@
package com.garfiec.librechat.feature.chat.viewmodel package com.garfiec.librechat.feature.chat.viewmodel
import androidx.compose.runtime.Immutable
import com.garfiec.librechat.core.common.ChatLayoutConstants import com.garfiec.librechat.core.common.ChatLayoutConstants
import com.garfiec.librechat.core.common.EndpointConstants import com.garfiec.librechat.core.common.EndpointConstants
import com.garfiec.librechat.core.common.ToolConstants import com.garfiec.librechat.core.common.ToolConstants
import com.garfiec.librechat.core.data.datastore.ChatFontSize import com.garfiec.librechat.core.data.datastore.ChatFontSize
import com.garfiec.librechat.core.data.datastore.LatexRenderer import com.garfiec.librechat.core.data.datastore.LatexRenderer
import com.garfiec.librechat.core.model.Agent import com.garfiec.librechat.core.model.Agent
import com.garfiec.librechat.core.model.Attachment
import com.garfiec.librechat.core.model.EndpointConfig import com.garfiec.librechat.core.model.EndpointConfig
import com.garfiec.librechat.core.model.Message
import com.garfiec.librechat.core.ui.components.ModelParameters import com.garfiec.librechat.core.ui.components.ModelParameters
import com.garfiec.librechat.feature.chat.model.McpServerDisplayData import com.garfiec.librechat.feature.chat.model.McpServerDisplayData
import com.garfiec.librechat.feature.chat.model.PresetDisplayData import com.garfiec.librechat.feature.chat.model.PresetDisplayData
@ -20,7 +23,7 @@ enum class ChatScreenState { LANDING, LOADING, ACTIVE }
* Exposed as a single [StateFlow] to reduce the number of individual subscriptions * Exposed as a single [StateFlow] to reduce the number of individual subscriptions
* in the UI layer. * in the UI layer.
*/ */
@androidx.compose.runtime.Immutable @Immutable
data class ChatPreferences( data class ChatPreferences(
val showImageDescriptions: Boolean = false, val showImageDescriptions: Boolean = false,
val dismissKeyboardOnSend: Boolean = false, val dismissKeyboardOnSend: Boolean = false,
@ -38,19 +41,19 @@ data class ChatPreferences(
* @param messageIndex Index into [ChatUiState.displayMessages] containing this occurrence. * @param messageIndex Index into [ChatUiState.displayMessages] containing this occurrence.
* @param occurrenceInMessage 0-based index of this occurrence within the message text. * @param occurrenceInMessage 0-based index of this occurrence within the message text.
*/ */
@androidx.compose.runtime.Immutable @Immutable
data class SearchMatch( data class SearchMatch(
val messageIndex: Int, val messageIndex: Int,
val occurrenceInMessage: Int, val occurrenceInMessage: Int,
) )
@androidx.compose.runtime.Immutable @Immutable
data class RetryInfo( data class RetryInfo(
val attempt: Int, val attempt: Int,
val maxAttempts: Int, val maxAttempts: Int,
) )
@androidx.compose.runtime.Immutable @Immutable
data class ActiveToolCall( data class ActiveToolCall(
val id: String, val id: String,
val name: String, val name: String,
@ -58,10 +61,10 @@ data class ActiveToolCall(
val output: String? = null, val output: String? = null,
) )
@androidx.compose.runtime.Immutable @Immutable
data class ChatUiState( data class ChatUiState(
val screenState: ChatScreenState = ChatScreenState.LANDING, val screenState: ChatScreenState = ChatScreenState.LANDING,
val messages: List<com.garfiec.librechat.core.model.Message> = emptyList(), val messages: List<Message> = emptyList(),
val displayMessages: List<MessageNode> = emptyList(), val displayMessages: List<MessageNode> = emptyList(),
val activeBranches: Map<String, Int> = emptyMap(), val activeBranches: Map<String, Int> = emptyMap(),
val inputText: String = "", val inputText: String = "",
@ -71,7 +74,7 @@ data class ChatUiState(
/** Attachments received during SSE streaming (e.g., tool-generated images). /** Attachments received during SSE streaming (e.g., tool-generated images).
* Cleared when streaming ends. Used to provide attachment context while the * Cleared when streaming ends. Used to provide attachment context while the
* final message (with full attachments) has not yet been persisted to Room. */ * final message (with full attachments) has not yet been persisted to Room. */
val streamingAttachments: List<com.garfiec.librechat.core.model.Attachment> = emptyList(), val streamingAttachments: List<Attachment> = emptyList(),
val selectedModel: String? = null, val selectedModel: String? = null,
val selectedEndpoint: String = EndpointConstants.AGENTS, val selectedEndpoint: String = EndpointConstants.AGENTS,
val endpointConfigs: Map<String, EndpointConfig> = emptyMap(), val endpointConfigs: Map<String, EndpointConfig> = emptyMap(),

View file

@ -22,7 +22,9 @@ import com.garfiec.librechat.core.data.repository.PresetRepository
import com.garfiec.librechat.core.data.repository.PromptRepository import com.garfiec.librechat.core.data.repository.PromptRepository
import com.garfiec.librechat.core.data.repository.ShareRepository import com.garfiec.librechat.core.data.repository.ShareRepository
import com.garfiec.librechat.core.data.repository.UserRepository import com.garfiec.librechat.core.data.repository.UserRepository
import com.garfiec.librechat.core.model.Attachment
import com.garfiec.librechat.core.model.EModelEndpoint import com.garfiec.librechat.core.model.EModelEndpoint
import com.garfiec.librechat.core.model.Message
import com.garfiec.librechat.core.model.Preset import com.garfiec.librechat.core.model.Preset
import com.garfiec.librechat.core.model.StreamEvent import com.garfiec.librechat.core.model.StreamEvent
import com.garfiec.librechat.core.model.request.EphemeralAgent import com.garfiec.librechat.core.model.request.EphemeralAgent
@ -38,6 +40,7 @@ import com.garfiec.librechat.feature.chat.viewmodel.delegate.ModelSelectionDeleg
import com.garfiec.librechat.feature.chat.viewmodel.delegate.PlatformDelegateFactory import com.garfiec.librechat.feature.chat.viewmodel.delegate.PlatformDelegateFactory
import com.garfiec.librechat.feature.chat.viewmodel.delegate.PresetPromptDelegate import com.garfiec.librechat.feature.chat.viewmodel.delegate.PresetPromptDelegate
import com.garfiec.librechat.feature.chat.viewmodel.delegate.toSerialName import com.garfiec.librechat.feature.chat.viewmodel.delegate.toSerialName
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -467,7 +470,7 @@ class ChatViewModel(
val messageText = text.ifBlank { val messageText = text.ifBlank {
if (hasFiles) "" else return if (hasFiles) "" else return
} }
val optimisticMessage = com.garfiec.librechat.core.model.Message( val optimisticMessage = Message(
messageId = Uuid.random().toString(), messageId = Uuid.random().toString(),
conversationId = conversationId ?: "", conversationId = conversationId ?: "",
parentMessageId = lastMessageId, parentMessageId = lastMessageId,
@ -637,7 +640,7 @@ class ChatViewModel(
} }
} }
private fun editUserMessage(originalMessage: com.garfiec.librechat.core.model.Message, newText: String) { private fun editUserMessage(originalMessage: Message, newText: String) {
val parentMessageId = originalMessage.parentMessageId val parentMessageId = originalMessage.parentMessageId
isEditOrRegenerate = true isEditOrRegenerate = true
@ -665,7 +668,7 @@ class ChatViewModel(
} }
} }
private fun editAiMessage(aiMessage: com.garfiec.librechat.core.model.Message, newText: String) { private fun editAiMessage(aiMessage: Message, newText: String) {
val parentUserMessage = _uiState.value.messages.find { val parentUserMessage = _uiState.value.messages.find {
it.messageId == aiMessage.parentMessageId it.messageId == aiMessage.parentMessageId
} ?: return } ?: return
@ -750,7 +753,7 @@ class ChatViewModel(
private suspend fun collectStreamSafely(stream: Flow<StreamEvent>) { private suspend fun collectStreamSafely(stream: Flow<StreamEvent>) {
try { try {
stream.collect { event -> handleStreamEvent(event) } stream.collect { event -> handleStreamEvent(event) }
} catch (e: kotlinx.coroutines.CancellationException) { } catch (e: CancellationException) {
throw e // Never swallow cancellation throw e // Never swallow cancellation
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(e) { "Stream collection failed" } Logger.e(e) { "Stream collection failed" }
@ -1026,7 +1029,7 @@ class ChatViewModel(
} }
} }
is StreamEvent.AttachmentCreated -> { is StreamEvent.AttachmentCreated -> {
val attachment = com.garfiec.librechat.core.model.Attachment( val attachment = Attachment(
fileId = event.fileId, fileId = event.fileId,
filename = event.filename, filename = event.filename,
filepath = event.filepath, filepath = event.filepath,

View file

@ -42,9 +42,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -58,6 +60,7 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.garfiec.librechat.core.common.ChatLayoutConstants import com.garfiec.librechat.core.common.ChatLayoutConstants
import com.garfiec.librechat.core.model.Attachment
import com.garfiec.librechat.core.model.Message import com.garfiec.librechat.core.model.Message
import com.garfiec.librechat.core.model.content.MessageContentPart import com.garfiec.librechat.core.model.content.MessageContentPart
import com.garfiec.librechat.core.ui.components.AvatarImage import com.garfiec.librechat.core.ui.components.AvatarImage
@ -334,7 +337,7 @@ private fun TwoSidedBubble(
@Composable @Composable
actual fun ContentPartRenderer( actual fun ContentPartRenderer(
part: MessageContentPart, modifier: Modifier, baseUrl: String, fontSizeMultiplier: Float, part: MessageContentPart, modifier: Modifier, baseUrl: String, fontSizeMultiplier: Float,
useKatex: Boolean, attachments: List<com.garfiec.librechat.core.model.Attachment>, useKatex: Boolean, attachments: List<Attachment>,
showImageDescriptions: Boolean, searchQuery: String?, searchFocusedOccurrence: Int, showImageDescriptions: Boolean, searchQuery: String?, searchFocusedOccurrence: Int,
onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)?, onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)?,
) { ) {
@ -624,7 +627,7 @@ private fun IosMarkdownTable(
@Composable @Composable
private fun TableCell( private fun TableCell(
text: String, style: androidx.compose.ui.text.TextStyle, color: androidx.compose.ui.graphics.Color, text: String, style: TextStyle, color: Color,
alignment: TableCellAlignment, cellWidth: Dp, paddingH: Dp, paddingV: Dp, modifier: Modifier = Modifier, alignment: TableCellAlignment, cellWidth: Dp, paddingH: Dp, paddingV: Dp, modifier: Modifier = Modifier,
) { ) {
Box( Box(
@ -646,7 +649,7 @@ private fun TableCell(
} }
} }
private fun androidx.compose.ui.text.TextStyle.scale(m: Float): androidx.compose.ui.text.TextStyle { private fun TextStyle.scale(m: Float): TextStyle {
if (m == 1.0f) return this if (m == 1.0f) return this
return copy( return copy(
fontSize = if (fontSize.isSpecified) (fontSize.value * m).sp else fontSize, fontSize = if (fontSize.isSpecified) (fontSize.value * m).sp else fontSize,

View file

@ -52,8 +52,10 @@ import co.touchlab.kermit.Logger
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import platform.AVFAudio.AVAudioPlayer import platform.AVFAudio.AVAudioPlayer
import platform.AVFoundation.AVLayerVideoGravityResizeAspect
import platform.AVFoundation.AVPlayer import platform.AVFoundation.AVPlayer
import platform.AVFoundation.AVPlayerItem import platform.AVFoundation.AVPlayerItem
import platform.AVFoundation.AVPlayerLayer import platform.AVFoundation.AVPlayerLayer
@ -76,7 +78,10 @@ import platform.Foundation.writeToFile
import kotlinx.cinterop.addressOf import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned import kotlinx.cinterop.usePinned
import platform.QuartzCore.CALayer import platform.QuartzCore.CALayer
import platform.UIKit.UIColor
import platform.UIKit.UIView import platform.UIKit.UIView
import platform.UIKit.UIViewController
import platform.UIKit.UIWindowScene
import platform.WebKit.WKNavigation import platform.WebKit.WKNavigation
import platform.WebKit.WKNavigationDelegateProtocol import platform.WebKit.WKNavigationDelegateProtocol
import platform.WebKit.WKWebView import platform.WebKit.WKWebView
@ -385,7 +390,7 @@ actual fun VideoContent(
factory = { factory = {
val view = UIView() val view = UIView()
val playerLayer = AVPlayerLayer.playerLayerWithPlayer(player) val playerLayer = AVPlayerLayer.playerLayerWithPlayer(player)
playerLayer.videoGravity = platform.AVFoundation.AVLayerVideoGravityResizeAspect playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
view.layer.addSublayer(playerLayer) view.layer.addSublayer(playerLayer)
view view
}, },
@ -473,8 +478,8 @@ private fun KatexWebView(
.height(contentHeight), .height(contentHeight),
factory = { factory = {
val config = WKWebViewConfiguration() val config = WKWebViewConfiguration()
val webView = WKWebView(frame = kotlinx.cinterop.cValue { }, configuration = config) val webView = WKWebView(frame = cValue { }, configuration = config)
val nativeBg = platform.UIKit.UIColor( val nativeBg = UIColor(
red = bgComposeColor.red.toDouble(), red = bgComposeColor.red.toDouble(),
green = bgComposeColor.green.toDouble(), green = bgComposeColor.green.toDouble(),
blue = bgComposeColor.blue.toDouble(), blue = bgComposeColor.blue.toDouble(),
@ -502,7 +507,7 @@ private fun KatexWebView(
) )
} }
private fun colorToCssHex(color: androidx.compose.ui.graphics.Color): String { private fun colorToCssHex(color: Color): String {
val r = (color.red * 255).toInt() val r = (color.red * 255).toInt()
val g = (color.green * 255).toInt() val g = (color.green * 255).toInt()
val b = (color.blue * 255).toInt() val b = (color.blue * 255).toInt()
@ -690,7 +695,7 @@ private fun MermaidWKWebView(
modifier = modifier, modifier = modifier,
factory = { factory = {
val config = WKWebViewConfiguration() val config = WKWebViewConfiguration()
val webView = WKWebView(frame = kotlinx.cinterop.cValue { }, configuration = config) val webView = WKWebView(frame = cValue { }, configuration = config)
webView.setOpaque(false) webView.setOpaque(false)
webView.loadHTMLString(html, baseURL = NSURL.URLWithString("https://cdn.jsdelivr.net")) webView.loadHTMLString(html, baseURL = NSURL.URLWithString("https://cdn.jsdelivr.net"))
webView webView
@ -823,8 +828,8 @@ actual fun FullscreenImageViewer(
} }
} }
private fun getRootViewController(): platform.UIKit.UIViewController? { private fun getRootViewController(): UIViewController? {
val scene = UIApplication.sharedApplication.connectedScenes.firstOrNull() as? platform.UIKit.UIWindowScene val scene = UIApplication.sharedApplication.connectedScenes.firstOrNull() as? UIWindowScene
return scene?.keyWindow?.rootViewController return scene?.keyWindow?.rootViewController
} }

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.garfiec.librechat.feature.chat.components.shareArtifact import com.garfiec.librechat.feature.chat.components.shareArtifact
import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import platform.Foundation.NSURL import platform.Foundation.NSURL
import platform.WebKit.WKWebView import platform.WebKit.WKWebView
import platform.WebKit.WKWebViewConfiguration import platform.WebKit.WKWebViewConfiguration
@ -131,7 +132,7 @@ actual fun ArtifactPanel(
defaultWebpagePreferences.allowsContentJavaScript = true defaultWebpagePreferences.allowsContentJavaScript = true
} }
val webView = WKWebView( val webView = WKWebView(
frame = kotlinx.cinterop.cValue { }, frame = cValue { },
configuration = config, configuration = config,
) )
webView.setOpaque(false) webView.setOpaque(false)

View file

@ -4,6 +4,7 @@ import com.garfiec.librechat.core.common.result.Result
import com.garfiec.librechat.core.data.datastore.SettingsDataStore import com.garfiec.librechat.core.data.datastore.SettingsDataStore
import com.garfiec.librechat.core.data.repository.SpeechRepository import com.garfiec.librechat.core.data.repository.SpeechRepository
import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle
import kotlinx.cinterop.ObjCSignatureOverride
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import platform.AVFAudio.AVSpeechBoundary import platform.AVFAudio.AVSpeechBoundary
@ -29,7 +30,7 @@ class IosTts(
private val synthesizer = AVSpeechSynthesizer() private val synthesizer = AVSpeechSynthesizer()
private val delegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol { private val delegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol {
@kotlinx.cinterop.ObjCSignatureOverride @ObjCSignatureOverride
override fun speechSynthesizer( override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer, synthesizer: AVSpeechSynthesizer,
didFinishSpeechUtterance: AVSpeechUtterance, didFinishSpeechUtterance: AVSpeechUtterance,
@ -37,7 +38,7 @@ class IosTts(
stateHandle.update { copy(currentlyReadingMessageId = null) } stateHandle.update { copy(currentlyReadingMessageId = null) }
} }
@kotlinx.cinterop.ObjCSignatureOverride @ObjCSignatureOverride
override fun speechSynthesizer( override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer, synthesizer: AVSpeechSynthesizer,
didCancelSpeechUtterance: AVSpeechUtterance, didCancelSpeechUtterance: AVSpeechUtterance,

View file

@ -1,5 +1,16 @@
package com.garfiec.librechat.feature.conversations.di package com.garfiec.librechat.feature.conversations.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.data.datastore.ServerDataStore
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
import com.garfiec.librechat.core.data.repository.ConfigRepository
import com.garfiec.librechat.core.data.repository.ConversationRepository
import com.garfiec.librechat.core.data.repository.MessageRepository
import com.garfiec.librechat.core.data.repository.SearchRepository
import com.garfiec.librechat.core.data.repository.ShareRepository
import com.garfiec.librechat.core.data.repository.TagRepository
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,17 +19,17 @@ class ConversationsModuleVerificationTest {
fun verifyConversationsModule() { fun verifyConversationsModule() {
conversationsModule.verify( conversationsModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.ConversationRepository::class, ConversationRepository::class,
com.garfiec.librechat.core.data.repository.MessageRepository::class, MessageRepository::class,
com.garfiec.librechat.core.data.repository.TagRepository::class, TagRepository::class,
com.garfiec.librechat.core.data.repository.ShareRepository::class, ShareRepository::class,
com.garfiec.librechat.core.data.repository.SearchRepository::class, SearchRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class, ConfigRepository::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class, ServerDataStore::class,
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class, SettingsDataStore::class,
), ),
) )
} }

View file

@ -33,6 +33,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -185,8 +186,8 @@ private fun ActionRow(
icon: ImageVector, icon: ImageVector,
label: String, label: String,
onClick: () -> Unit, onClick: () -> Unit,
iconTint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface, iconTint: Color = MaterialTheme.colorScheme.onSurface,
labelColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface, labelColor: Color = MaterialTheme.colorScheme.onSurface,
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier

View file

@ -10,7 +10,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import platform.Foundation.NSTimer
import platform.UIKit.UIAlertController
import platform.UIKit.UIAlertControllerStyleAlert
import platform.UIKit.UIApplication
import platform.UIKit.UIPasteboard import platform.UIKit.UIPasteboard
import platform.UIKit.UIWindow
import platform.UIKit.UIWindowScene
actual fun copyToClipboard(text: String, label: String) { actual fun copyToClipboard(text: String, label: String) {
UIPasteboard.generalPasteboard.string = text UIPasteboard.generalPasteboard.string = text
@ -20,19 +26,19 @@ actual fun showToast(message: String) {
// iOS doesn't have native Toast — show a brief alert-style overlay. // iOS doesn't have native Toast — show a brief alert-style overlay.
Logger.i("Toast") { message } Logger.i("Toast") { message }
// Use UIAlertController as a lightweight toast replacement // Use UIAlertController as a lightweight toast replacement
val scene = platform.UIKit.UIApplication.sharedApplication.connectedScenes val scene = UIApplication.sharedApplication.connectedScenes
.firstOrNull() as? platform.UIKit.UIWindowScene .firstOrNull() as? UIWindowScene
val rootVc = scene?.windows?.firstOrNull { val rootVc = scene?.windows?.firstOrNull {
(it as? platform.UIKit.UIWindow)?.isKeyWindow() == true (it as? UIWindow)?.isKeyWindow() == true
}?.let { (it as platform.UIKit.UIWindow).rootViewController } ?: return }?.let { (it as UIWindow).rootViewController } ?: return
val alert = platform.UIKit.UIAlertController.alertControllerWithTitle( val alert = UIAlertController.alertControllerWithTitle(
title = null, title = null,
message = message, message = message,
preferredStyle = platform.UIKit.UIAlertControllerStyleAlert, preferredStyle = UIAlertControllerStyleAlert,
) )
rootVc.presentViewController(alert, animated = true, completion = null) rootVc.presentViewController(alert, animated = true, completion = null)
// Auto-dismiss after 1.5 seconds // Auto-dismiss after 1.5 seconds
platform.Foundation.NSTimer.scheduledTimerWithTimeInterval( NSTimer.scheduledTimerWithTimeInterval(
interval = 1.5, interval = 1.5,
repeats = false, repeats = false,
) { ) {

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.feature.files.platform package com.garfiec.librechat.feature.files.platform
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.pdf.PdfRenderer import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
@ -115,7 +116,7 @@ actual fun PdfPreview(
bitmapHeight, bitmapHeight,
Bitmap.Config.ARGB_8888, Bitmap.Config.ARGB_8888,
) )
bitmap.eraseColor(android.graphics.Color.WHITE) bitmap.eraseColor(Color.WHITE)
page.render( page.render(
bitmap, bitmap,
null, null,

View file

@ -1,5 +1,11 @@
package com.garfiec.librechat.feature.files.di package com.garfiec.librechat.feature.files.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.data.datastore.ServerDataStore
import com.garfiec.librechat.core.data.repository.FileRepository
import com.garfiec.librechat.feature.files.platform.FileReader
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,12 +14,12 @@ class FilesModuleVerificationTest {
fun verifyFilesModule() { fun verifyFilesModule() {
filesModule.verify( filesModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.FileRepository::class, FileRepository::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class, ServerDataStore::class,
com.garfiec.librechat.feature.files.platform.FileReader::class, FileReader::class,
), ),
) )
} }

View file

@ -12,6 +12,7 @@ import com.garfiec.librechat.feature.files.FileDisplayData
import com.garfiec.librechat.feature.files.FilePreviewDisplayData import com.garfiec.librechat.feature.files.FilePreviewDisplayData
import com.garfiec.librechat.feature.files.platform.FileReader import com.garfiec.librechat.feature.files.platform.FileReader
import com.garfiec.librechat.feature.files.platform.formatFileSize import com.garfiec.librechat.feature.files.platform.formatFileSize
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -166,7 +167,7 @@ class FilesViewModel(
} }
} }
private var uploadJob: kotlinx.coroutines.Job? = null private var uploadJob: Job? = null
/** /**
* Upload a file from a platform-specific file reference. * Upload a file from a platform-specific file reference.

View file

@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.settings.viewmodel.delegate
import android.content.Context import android.content.Context
import android.media.MediaPlayer import android.media.MediaPlayer
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import com.garfiec.librechat.core.common.result.Result import com.garfiec.librechat.core.common.result.Result
import com.garfiec.librechat.core.data.datastore.SettingsDataStore import com.garfiec.librechat.core.data.datastore.SettingsDataStore
import com.garfiec.librechat.core.data.repository.SpeechRepository import com.garfiec.librechat.core.data.repository.SpeechRepository
@ -11,6 +12,7 @@ import com.garfiec.librechat.feature.settings.screen.DeviceVoiceInfo
import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import java.io.File
/** /**
* Handles TTS voice selection, test playback, device voice loading, and MediaPlayer lifecycle. * Handles TTS voice selection, test playback, device voice loading, and MediaPlayer lifecycle.
@ -102,7 +104,7 @@ class SpeechSettingsDelegate(
} }
} }
stateHandle.update { copy(isTtsPreviewPlaying = true) } stateHandle.update { copy(isTtsPreviewPlaying = true) }
tts.setOnUtteranceProgressListener(object : android.speech.tts.UtteranceProgressListener() { tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
override fun onStart(utteranceId: String?) { /* no-op */ } override fun onStart(utteranceId: String?) { /* no-op */ }
override fun onDone(utteranceId: String?) { override fun onDone(utteranceId: String?) {
stateHandle.update { copy(isTtsPreviewPlaying = false) } stateHandle.update { copy(isTtsPreviewPlaying = false) }
@ -217,7 +219,7 @@ class SpeechSettingsDelegate(
currentMediaPlayer = null currentMediaPlayer = null
// Write bytes to a temporary file // Write bytes to a temporary file
val tempFile = java.io.File.createTempFile("voice_test", ".mp3", context.cacheDir) val tempFile = File.createTempFile("voice_test", ".mp3", context.cacheDir)
tempFile.deleteOnExit() tempFile.deleteOnExit()
tempFile.writeBytes(audioBytes) tempFile.writeBytes(audioBytes)

View file

@ -1,5 +1,25 @@
package com.garfiec.librechat.feature.settings.di package com.garfiec.librechat.feature.settings.di
import android.app.Application
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import com.garfiec.librechat.core.data.datastore.ServerDataStore
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
import com.garfiec.librechat.core.data.datastore.ThemeDataStore
import com.garfiec.librechat.core.data.repository.ApiKeyRepository
import com.garfiec.librechat.core.data.repository.AuthRepository
import com.garfiec.librechat.core.data.repository.BalanceRepository
import com.garfiec.librechat.core.data.repository.ConversationRepository
import com.garfiec.librechat.core.data.repository.KeyRepository
import com.garfiec.librechat.core.data.repository.McpRepository
import com.garfiec.librechat.core.data.repository.MemoryRepository
import com.garfiec.librechat.core.data.repository.PresetRepository
import com.garfiec.librechat.core.data.repository.ShareRepository
import com.garfiec.librechat.core.data.repository.SpeechRepository
import com.garfiec.librechat.core.data.repository.UserRepository
import com.garfiec.librechat.feature.settings.util.ContentReader
import com.garfiec.librechat.feature.settings.util.PlatformCacheCleaner
import com.garfiec.librechat.feature.settings.viewmodel.delegate.SpeechSettingsFactory
import org.junit.Test import org.junit.Test
import org.koin.test.verify.verify import org.koin.test.verify.verify
@ -8,26 +28,26 @@ class SettingsModuleVerificationTest {
fun verifySettingsModule() { fun verifySettingsModule() {
settingsModule.verify( settingsModule.verify(
extraTypes = listOf( extraTypes = listOf(
android.content.Context::class, Context::class,
android.app.Application::class, Application::class,
androidx.lifecycle.SavedStateHandle::class, SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.UserRepository::class, UserRepository::class,
com.garfiec.librechat.core.data.repository.AuthRepository::class, AuthRepository::class,
com.garfiec.librechat.core.data.repository.ConversationRepository::class, ConversationRepository::class,
com.garfiec.librechat.core.data.repository.McpRepository::class, McpRepository::class,
com.garfiec.librechat.core.data.repository.MemoryRepository::class, MemoryRepository::class,
com.garfiec.librechat.core.data.repository.SpeechRepository::class, SpeechRepository::class,
com.garfiec.librechat.core.data.repository.BalanceRepository::class, BalanceRepository::class,
com.garfiec.librechat.core.data.repository.ShareRepository::class, ShareRepository::class,
com.garfiec.librechat.core.data.repository.KeyRepository::class, KeyRepository::class,
com.garfiec.librechat.core.data.repository.ApiKeyRepository::class, ApiKeyRepository::class,
com.garfiec.librechat.core.data.repository.PresetRepository::class, PresetRepository::class,
com.garfiec.librechat.core.data.datastore.ThemeDataStore::class, ThemeDataStore::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class, ServerDataStore::class,
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class, SettingsDataStore::class,
com.garfiec.librechat.feature.settings.util.ContentReader::class, ContentReader::class,
com.garfiec.librechat.feature.settings.util.PlatformCacheCleaner::class, PlatformCacheCleaner::class,
com.garfiec.librechat.feature.settings.viewmodel.delegate.SpeechSettingsFactory::class, SpeechSettingsFactory::class,
), ),
) )
} }

View file

@ -23,6 +23,7 @@ import com.garfiec.librechat.core.data.repository.ShareRepository
import com.garfiec.librechat.core.data.repository.SpeechRepository import com.garfiec.librechat.core.data.repository.SpeechRepository
import com.garfiec.librechat.core.data.repository.UserRepository import com.garfiec.librechat.core.data.repository.UserRepository
import com.garfiec.librechat.core.model.User import com.garfiec.librechat.core.model.User
import io.mockk.Ordering
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.every import io.mockk.every
@ -182,7 +183,7 @@ class SettingsViewModelTest {
viewModel.deleteAccount() viewModel.deleteAccount()
advanceUntilIdle() advanceUntilIdle()
coVerify(ordering = io.mockk.Ordering.ORDERED) { coVerify(ordering = Ordering.ORDERED) {
userRepository.deleteUser() userRepository.deleteUser()
authRepository.logout() authRepository.logout()
} }

View file

@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.settings.screen
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -30,6 +31,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -173,7 +175,7 @@ fun ApiKeysScreen(
else -> { else -> {
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
items( items(
@ -210,7 +212,7 @@ fun ApiKeysScreen(
viewModel.deleteKey(id) viewModel.deleteKey(id)
} }
}, },
colors = androidx.compose.material3.ButtonDefaults.textButtonColors( colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.error,
), ),
) { ) {

View file

@ -1,5 +1,6 @@
package com.garfiec.librechat.feature.settings.screen package com.garfiec.librechat.feature.settings.screen
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
@ -25,6 +26,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
@ -33,6 +35,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.heading import androidx.compose.ui.semantics.heading
@ -286,7 +289,7 @@ private fun AboutInfo(serverUrl: String) {
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(
text = stringResource(Res.string.app_version_label), text = stringResource(Res.string.app_version_label),
@ -301,7 +304,7 @@ private fun AboutInfo(serverUrl: String) {
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text( Text(
text = stringResource(Res.string.server_label), text = stringResource(Res.string.server_label),
@ -323,14 +326,14 @@ private fun AboutInfo(serverUrl: String) {
@Composable @Composable
private fun GeneralSettingsRow( private fun GeneralSettingsRow(
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: ImageVector,
title: String, title: String,
subtitle: String, subtitle: String,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column(modifier = modifier) { Column(modifier = modifier) {
androidx.compose.material3.Surface( Surface(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = onClick, onClick = onClick,
) { ) {

View file

@ -103,7 +103,7 @@ internal fun McpSettingsSection(
@Composable @Composable
private fun McpServerItem( private fun McpServerItem(
server: McpServer, server: McpServer,
serverStatus: com.garfiec.librechat.core.model.mcp.McpServerStatus?, serverStatus: McpServerStatus?,
isReinitializing: Boolean, isReinitializing: Boolean,
onEdit: () -> Unit, onEdit: () -> Unit,
onDelete: () -> Unit, onDelete: () -> Unit,

View file

@ -4,6 +4,7 @@ import com.garfiec.librechat.feature.settings.util.copyToClipboard
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -131,7 +132,7 @@ fun SharedLinksScreen(
.fillMaxSize() .fillMaxSize()
.padding(padding), .padding(padding),
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp), contentPadding = PaddingValues(16.dp),
) { ) {
items( items(
items = links, items = links,

View file

@ -22,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -108,7 +109,7 @@ internal fun DangerZone(
@Composable @Composable
internal fun SettingsRow( internal fun SettingsRow(
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: ImageVector,
title: String, title: String,
subtitle: String, subtitle: String,
onClick: () -> Unit, onClick: () -> Unit,

View file

@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
@ -18,6 +20,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.stringResource import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import librechat_mobile.feature.settings.generated.resources.Res import librechat_mobile.feature.settings.generated.resources.Res
import librechat_mobile.feature.settings.generated.resources.* import librechat_mobile.feature.settings.generated.resources.*
@ -50,7 +53,7 @@ internal fun TwoFactorSetupDialog(
color = MaterialTheme.colorScheme.surfaceVariant, color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small, shape = MaterialTheme.shapes.small,
) { ) {
androidx.compose.foundation.text.selection.SelectionContainer { SelectionContainer {
Text( Text(
text = otpauthUrl, text = otpauthUrl,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@ -59,7 +62,7 @@ internal fun TwoFactorSetupDialog(
} }
} }
} }
androidx.compose.material3.OutlinedTextField( OutlinedTextField(
value = code, value = code,
onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) }, onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) },
label = { Text(stringResource(Res.string.hint_verification_code)) }, label = { Text(stringResource(Res.string.hint_verification_code)) },
@ -105,7 +108,7 @@ internal fun TwoFactorCodeDialog(
text = description, text = description,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
) )
androidx.compose.material3.OutlinedTextField( OutlinedTextField(
value = code, value = code,
onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) }, onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) },
label = { Text(stringResource(Res.string.hint_verification_code)) }, label = { Text(stringResource(Res.string.hint_verification_code)) },
@ -160,7 +163,7 @@ internal fun BackupCodesDialog(
Text( Text(
text = code, text = code,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, fontFamily = FontFamily.Monospace,
) )
} }
} }

View file

@ -7,6 +7,7 @@ import com.garfiec.librechat.core.data.repository.SpeechRepository
import com.garfiec.librechat.core.model.speech.TtsVoice import com.garfiec.librechat.core.model.speech.TtsVoice
import com.garfiec.librechat.feature.settings.screen.DeviceVoiceInfo import com.garfiec.librechat.feature.settings.screen.DeviceVoiceInfo
import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle
import kotlinx.cinterop.ObjCSignatureOverride
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import platform.AVFAudio.AVSpeechBoundary import platform.AVFAudio.AVSpeechBoundary
import platform.AVFAudio.AVSpeechSynthesisVoice import platform.AVFAudio.AVSpeechSynthesisVoice
@ -28,7 +29,7 @@ class IosSpeechSettingsDelegate(
private val synthesizer = AVSpeechSynthesizer() private val synthesizer = AVSpeechSynthesizer()
private val synthesizerDelegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol { private val synthesizerDelegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol {
@kotlinx.cinterop.ObjCSignatureOverride @ObjCSignatureOverride
override fun speechSynthesizer( override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer, synthesizer: AVSpeechSynthesizer,
didFinishSpeechUtterance: AVSpeechUtterance, didFinishSpeechUtterance: AVSpeechUtterance,
@ -36,7 +37,7 @@ class IosSpeechSettingsDelegate(
stateHandle.update { copy(isTtsPreviewPlaying = false) } stateHandle.update { copy(isTtsPreviewPlaying = false) }
} }
@kotlinx.cinterop.ObjCSignatureOverride @ObjCSignatureOverride
override fun speechSynthesizer( override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer, synthesizer: AVSpeechSynthesizer,
didCancelSpeechUtterance: AVSpeechUtterance, didCancelSpeechUtterance: AVSpeechUtterance,

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.garfiec.librechat.core.data.repository.AuthRepository import com.garfiec.librechat.core.data.repository.AuthRepository
import com.garfiec.librechat.core.data.repository.BannerRepository import com.garfiec.librechat.core.data.repository.BannerRepository
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
import com.garfiec.librechat.core.data.repository.ConfigRepository import com.garfiec.librechat.core.data.repository.ConfigRepository
import com.garfiec.librechat.core.data.repository.ConversationRepository import com.garfiec.librechat.core.data.repository.ConversationRepository
import com.garfiec.librechat.core.model.Banner import com.garfiec.librechat.core.model.Banner
@ -25,7 +26,7 @@ class NavHostViewModel(
configRepository: ConfigRepository, configRepository: ConfigRepository,
conversationRepository: ConversationRepository, conversationRepository: ConversationRepository,
private val tokenManager: TokenManager, private val tokenManager: TokenManager,
private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore, private val settingsDataStore: SettingsDataStore,
) : ViewModel() { ) : ViewModel() {
private val bannerStateHolder = BannerStateHolder(bannerRepository, viewModelScope) private val bannerStateHolder = BannerStateHolder(bannerRepository, viewModelScope)