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.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import co.touchlab.kermit.Logger
class LibreChatApplication : Application(), SingletonImageLoader.Factory {
@ -33,7 +34,7 @@ class LibreChatApplication : Application(), SingletonImageLoader.Factory {
try {
startKoin {
if (BuildConfig.DEBUG) {
androidLogger(org.koin.core.logger.Level.DEBUG)
androidLogger(Level.DEBUG)
}
androidContext(this@LibreChatApplication)
allowOverride(false)

View file

@ -47,7 +47,7 @@ class MainActivity : ComponentActivity() {
private val connectivityObserver: ConnectivityObserver 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.

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
package com.garfiec.librechat.core.data.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
@ -17,7 +18,7 @@ import org.koin.core.module.Module
import org.koin.dsl.binds
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,
)

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.core.data.datastore
import io.ktor.client.HttpClient
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
@ -36,7 +37,7 @@ import platform.Security.kSecReturnData
import platform.Security.kSecValueData
import co.touchlab.kermit.Logger
@OptIn(ExperimentalForeignApi::class, kotlinx.cinterop.BetaInteropApi::class)
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
class IosTokenDataStore(
refreshClient: Lazy<HttpClient>,
) : 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.TokenManager
import io.ktor.client.HttpClient
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import okio.Path.Companion.toPath
import org.koin.core.module.Module
import org.koin.dsl.binds
import org.koin.dsl.module
import platform.Foundation.NSCachesDirectory
import platform.Foundation.NSFileManager
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) {
NSFileManager.defaultManager.createDirectoryAtPath(path, withIntermediateDirectories = true, attributes = null, error = null)
}
@ -59,10 +63,10 @@ actual val dataPlatformModule: Module = module {
// --- Session Cache Cleaner ---
single<SessionCacheCleaner> {
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
val cachePath = platform.Foundation.NSSearchPathForDirectoriesInDomains(
platform.Foundation.NSCachesDirectory,
platform.Foundation.NSUserDomainMask,
@OptIn(ExperimentalForeignApi::class)
val cachePath = NSSearchPathForDirectoriesInDomains(
NSCachesDirectory,
NSUserDomainMask,
true,
).firstOrNull() as? String ?: error("Unable to resolve NSCachesDirectory")
CommonSessionCacheCleaner(cachePath)

View file

@ -1,5 +1,11 @@
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.koin.test.verify.verify
@ -8,12 +14,12 @@ class NetworkModuleVerificationTest {
fun verifyNetworkModule() {
networkModule.verify(
extraTypes = listOf(
android.content.Context::class,
android.app.Application::class,
io.ktor.client.engine.HttpClientEngine::class,
io.ktor.client.engine.HttpClientEngineFactory::class,
com.garfiec.librechat.core.network.client.TokenManager::class,
com.garfiec.librechat.core.network.client.ServerUrlProvider::class,
Context::class,
Application::class,
HttpClientEngine::class,
HttpClientEngineFactory::class,
TokenManager::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.defaultRequest
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.statement.bodyAsText
import io.ktor.http.ContentType
@ -36,7 +37,7 @@ object LibreChatHttpClient {
}
install(Logging) {
logger = object : io.ktor.client.plugins.logging.Logger {
logger = object : KtorLogger {
override fun log(message: String) {
val sanitized = message
.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.isSuccess
import io.ktor.http.path
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@ -127,7 +128,7 @@ class SseClient(
shouldResume = true
}
}
} catch (e: kotlinx.coroutines.CancellationException) {
} catch (e: CancellationException) {
throw e // Re-throw cancellation — SKIE handles this gracefully
} catch (e: Exception) {
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.Message
import com.garfiec.librechat.core.model.StreamEvent
import com.garfiec.librechat.core.model.content.MessageContentPart
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
@ -210,7 +211,7 @@ class SseEventMapper(private val json: Json) {
aggregatedContent.mapNotNull { element ->
try {
json.decodeFromJsonElement(
com.garfiec.librechat.core.model.content.MessageContentPart.serializer(),
MessageContentPart.serializer(),
element,
)
} catch (e: Exception) {

View file

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

View file

@ -1,5 +1,12 @@
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.koin.test.verify.verify
@ -8,13 +15,13 @@ class AgentsModuleVerificationTest {
fun verifyAgentsModule() {
agentsModule.verify(
extraTypes = listOf(
android.content.Context::class,
android.app.Application::class,
androidx.lifecycle.SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.AgentRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
com.garfiec.librechat.core.data.repository.McpRepository::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
Context::class,
Application::class,
SavedStateHandle::class,
AgentRepository::class,
ConfigRepository::class,
McpRepository::class,
ServerDataStore::class,
),
)
}

View file

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

View file

@ -1,6 +1,8 @@
package com.garfiec.librechat.feature.agents.components.model
@androidx.compose.runtime.Immutable
import androidx.compose.runtime.Immutable
@Immutable
data class SupportContactState(
val name: 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.unit.dp
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.endpointIconPainter
import com.garfiec.librechat.core.ui.components.ErrorBanner
@ -216,7 +217,7 @@ fun AgentDetailScreen(
imageUrl = agent.avatarUrl,
size = 80.dp,
fallbackText = agent.name,
fallbackIconPainter = endpointIconPainter(com.garfiec.librechat.core.model.EModelEndpoint.AGENTS),
fallbackIconPainter = endpointIconPainter(EModelEndpoint.AGENTS),
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.unit.dp
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.endpointIconPainter
import com.garfiec.librechat.core.ui.components.EmptyState
@ -254,7 +255,7 @@ private fun AgentCard(
imageUrl = agent.avatarUrl,
size = 40.dp,
fallbackText = agent.name,
fallbackIconPainter = endpointIconPainter(com.garfiec.librechat.core.model.EModelEndpoint.AGENTS),
fallbackIconPainter = endpointIconPainter(EModelEndpoint.AGENTS),
tintIcon = true,
)
}

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package com.garfiec.librechat.feature.chat.components
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
@ -406,7 +407,7 @@ fun ChatInput(
* Stored in the app's cache directory under `camera_photos/` which is
* 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")
if (!cameraDir.exists()) {
cameraDir.mkdirs()

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.speech.RecognizerIntent
import androidx.activity.compose.rememberLauncherForActivityResult
@ -201,7 +202,7 @@ actual fun ChatScreen(
}
} else {
// 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(
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,

View file

@ -1,10 +1,13 @@
package com.garfiec.librechat.feature.chat.util
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import co.touchlab.kermit.Logger
import java.io.ByteArrayOutputStream
/**
* 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)
// while storing the original MIME type, causing the Anthropic 400 error.
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
// 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
}
val outputStream = java.io.ByteArrayOutputStream()
bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, outputStream)
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
bitmap.recycle()
val encodedBytes = outputStream.toByteArray()

View file

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

View file

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

View file

@ -1,5 +1,24 @@
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.koin.test.verify.verify
@ -8,25 +27,25 @@ class ChatModuleVerificationTest {
fun verifyChatModule() {
chatModule.verify(
extraTypes = listOf(
android.content.Context::class,
android.app.Application::class,
androidx.lifecycle.SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.AgentRepository::class,
com.garfiec.librechat.core.data.repository.ChatRepository::class,
com.garfiec.librechat.core.data.repository.MessageRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
com.garfiec.librechat.core.data.repository.ConversationRepository::class,
com.garfiec.librechat.core.data.repository.DraftRepository::class,
com.garfiec.librechat.core.data.repository.FileRepository::class,
com.garfiec.librechat.core.data.repository.PresetRepository::class,
com.garfiec.librechat.core.data.repository.PromptRepository::class,
com.garfiec.librechat.core.data.repository.ShareRepository::class,
com.garfiec.librechat.core.data.repository.SpeechRepository::class,
com.garfiec.librechat.core.data.repository.McpRepository::class,
com.garfiec.librechat.core.data.repository.UserRepository::class,
com.garfiec.librechat.core.common.network.ConnectivityObserver::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class,
Context::class,
Application::class,
SavedStateHandle::class,
AgentRepository::class,
ChatRepository::class,
MessageRepository::class,
ConfigRepository::class,
ConversationRepository::class,
DraftRepository::class,
FileRepository::class,
PresetRepository::class,
PromptRepository::class,
ShareRepository::class,
SpeechRepository::class,
McpRepository::class,
UserRepository::class,
ConnectivityObserver::class,
ServerDataStore::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.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
@ -203,7 +204,7 @@ private fun FilesChip(
@Composable
private fun ToolIndicatorChip(
label: String,
icon: androidx.compose.ui.graphics.vector.ImageVector,
icon: ImageVector,
semanticDescription: String,
) {
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.*
import kotlin.time.Clock
import kotlinx.datetime.DayOfWeek
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
@ -114,7 +115,7 @@ private fun getTimeBasedGreeting(): String {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
val hour = now.hour
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) {
return when {

View file

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

View file

@ -1,12 +1,15 @@
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.EndpointConstants
import com.garfiec.librechat.core.common.ToolConstants
import com.garfiec.librechat.core.data.datastore.ChatFontSize
import com.garfiec.librechat.core.data.datastore.LatexRenderer
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.Message
import com.garfiec.librechat.core.ui.components.ModelParameters
import com.garfiec.librechat.feature.chat.model.McpServerDisplayData
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
* in the UI layer.
*/
@androidx.compose.runtime.Immutable
@Immutable
data class ChatPreferences(
val showImageDescriptions: Boolean = false,
val dismissKeyboardOnSend: Boolean = false,
@ -38,19 +41,19 @@ data class ChatPreferences(
* @param messageIndex Index into [ChatUiState.displayMessages] containing this occurrence.
* @param occurrenceInMessage 0-based index of this occurrence within the message text.
*/
@androidx.compose.runtime.Immutable
@Immutable
data class SearchMatch(
val messageIndex: Int,
val occurrenceInMessage: Int,
)
@androidx.compose.runtime.Immutable
@Immutable
data class RetryInfo(
val attempt: Int,
val maxAttempts: Int,
)
@androidx.compose.runtime.Immutable
@Immutable
data class ActiveToolCall(
val id: String,
val name: String,
@ -58,10 +61,10 @@ data class ActiveToolCall(
val output: String? = null,
)
@androidx.compose.runtime.Immutable
@Immutable
data class ChatUiState(
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 activeBranches: Map<String, Int> = emptyMap(),
val inputText: String = "",
@ -71,7 +74,7 @@ data class ChatUiState(
/** Attachments received during SSE streaming (e.g., tool-generated images).
* Cleared when streaming ends. Used to provide attachment context while the
* 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 selectedEndpoint: String = EndpointConstants.AGENTS,
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.ShareRepository
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.Message
import com.garfiec.librechat.core.model.Preset
import com.garfiec.librechat.core.model.StreamEvent
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.PresetPromptDelegate
import com.garfiec.librechat.feature.chat.viewmodel.delegate.toSerialName
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
@ -467,7 +470,7 @@ class ChatViewModel(
val messageText = text.ifBlank {
if (hasFiles) "" else return
}
val optimisticMessage = com.garfiec.librechat.core.model.Message(
val optimisticMessage = Message(
messageId = Uuid.random().toString(),
conversationId = conversationId ?: "",
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
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 {
it.messageId == aiMessage.parentMessageId
} ?: return
@ -750,7 +753,7 @@ class ChatViewModel(
private suspend fun collectStreamSafely(stream: Flow<StreamEvent>) {
try {
stream.collect { event -> handleStreamEvent(event) }
} catch (e: kotlinx.coroutines.CancellationException) {
} catch (e: CancellationException) {
throw e // Never swallow cancellation
} catch (e: Exception) {
Logger.e(e) { "Stream collection failed" }
@ -1026,7 +1029,7 @@ class ChatViewModel(
}
}
is StreamEvent.AttachmentCreated -> {
val attachment = com.garfiec.librechat.core.model.Attachment(
val attachment = Attachment(
fileId = event.fileId,
filename = event.filename,
filepath = event.filepath,

View file

@ -42,9 +42,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.LayoutCoordinates
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.FontStyle
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.DialogProperties
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.content.MessageContentPart
import com.garfiec.librechat.core.ui.components.AvatarImage
@ -334,7 +337,7 @@ private fun TwoSidedBubble(
@Composable
actual fun ContentPartRenderer(
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,
onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)?,
) {
@ -624,7 +627,7 @@ private fun IosMarkdownTable(
@Composable
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,
) {
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
return copy(
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 kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import kotlinx.coroutines.delay
import platform.AVFAudio.AVAudioPlayer
import platform.AVFoundation.AVLayerVideoGravityResizeAspect
import platform.AVFoundation.AVPlayer
import platform.AVFoundation.AVPlayerItem
import platform.AVFoundation.AVPlayerLayer
@ -76,7 +78,10 @@ import platform.Foundation.writeToFile
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.QuartzCore.CALayer
import platform.UIKit.UIColor
import platform.UIKit.UIView
import platform.UIKit.UIViewController
import platform.UIKit.UIWindowScene
import platform.WebKit.WKNavigation
import platform.WebKit.WKNavigationDelegateProtocol
import platform.WebKit.WKWebView
@ -385,7 +390,7 @@ actual fun VideoContent(
factory = {
val view = UIView()
val playerLayer = AVPlayerLayer.playerLayerWithPlayer(player)
playerLayer.videoGravity = platform.AVFoundation.AVLayerVideoGravityResizeAspect
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
view.layer.addSublayer(playerLayer)
view
},
@ -473,8 +478,8 @@ private fun KatexWebView(
.height(contentHeight),
factory = {
val config = WKWebViewConfiguration()
val webView = WKWebView(frame = kotlinx.cinterop.cValue { }, configuration = config)
val nativeBg = platform.UIKit.UIColor(
val webView = WKWebView(frame = cValue { }, configuration = config)
val nativeBg = UIColor(
red = bgComposeColor.red.toDouble(),
green = bgComposeColor.green.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 g = (color.green * 255).toInt()
val b = (color.blue * 255).toInt()
@ -690,7 +695,7 @@ private fun MermaidWKWebView(
modifier = modifier,
factory = {
val config = WKWebViewConfiguration()
val webView = WKWebView(frame = kotlinx.cinterop.cValue { }, configuration = config)
val webView = WKWebView(frame = cValue { }, configuration = config)
webView.setOpaque(false)
webView.loadHTMLString(html, baseURL = NSURL.URLWithString("https://cdn.jsdelivr.net"))
webView
@ -823,8 +828,8 @@ actual fun FullscreenImageViewer(
}
}
private fun getRootViewController(): platform.UIKit.UIViewController? {
val scene = UIApplication.sharedApplication.connectedScenes.firstOrNull() as? platform.UIKit.UIWindowScene
private fun getRootViewController(): UIViewController? {
val scene = UIApplication.sharedApplication.connectedScenes.firstOrNull() as? UIWindowScene
return scene?.keyWindow?.rootViewController
}

View file

@ -33,6 +33,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.garfiec.librechat.feature.chat.components.shareArtifact
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.cValue
import platform.Foundation.NSURL
import platform.WebKit.WKWebView
import platform.WebKit.WKWebViewConfiguration
@ -131,7 +132,7 @@ actual fun ArtifactPanel(
defaultWebpagePreferences.allowsContentJavaScript = true
}
val webView = WKWebView(
frame = kotlinx.cinterop.cValue { },
frame = cValue { },
configuration = config,
)
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.repository.SpeechRepository
import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle
import kotlinx.cinterop.ObjCSignatureOverride
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import platform.AVFAudio.AVSpeechBoundary
@ -29,7 +30,7 @@ class IosTts(
private val synthesizer = AVSpeechSynthesizer()
private val delegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol {
@kotlinx.cinterop.ObjCSignatureOverride
@ObjCSignatureOverride
override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer,
didFinishSpeechUtterance: AVSpeechUtterance,
@ -37,7 +38,7 @@ class IosTts(
stateHandle.update { copy(currentlyReadingMessageId = null) }
}
@kotlinx.cinterop.ObjCSignatureOverride
@ObjCSignatureOverride
override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer,
didCancelSpeechUtterance: AVSpeechUtterance,

View file

@ -1,5 +1,16 @@
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.koin.test.verify.verify
@ -8,17 +19,17 @@ class ConversationsModuleVerificationTest {
fun verifyConversationsModule() {
conversationsModule.verify(
extraTypes = listOf(
android.content.Context::class,
android.app.Application::class,
androidx.lifecycle.SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.ConversationRepository::class,
com.garfiec.librechat.core.data.repository.MessageRepository::class,
com.garfiec.librechat.core.data.repository.TagRepository::class,
com.garfiec.librechat.core.data.repository.ShareRepository::class,
com.garfiec.librechat.core.data.repository.SearchRepository::class,
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class,
Context::class,
Application::class,
SavedStateHandle::class,
ConversationRepository::class,
MessageRepository::class,
TagRepository::class,
ShareRepository::class,
SearchRepository::class,
ConfigRepository::class,
ServerDataStore::class,
SettingsDataStore::class,
),
)
}

View file

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

View file

@ -10,7 +10,13 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.UIWindow
import platform.UIKit.UIWindowScene
actual fun copyToClipboard(text: String, label: String) {
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.
Logger.i("Toast") { message }
// Use UIAlertController as a lightweight toast replacement
val scene = platform.UIKit.UIApplication.sharedApplication.connectedScenes
.firstOrNull() as? platform.UIKit.UIWindowScene
val scene = UIApplication.sharedApplication.connectedScenes
.firstOrNull() as? UIWindowScene
val rootVc = scene?.windows?.firstOrNull {
(it as? platform.UIKit.UIWindow)?.isKeyWindow() == true
}?.let { (it as platform.UIKit.UIWindow).rootViewController } ?: return
val alert = platform.UIKit.UIAlertController.alertControllerWithTitle(
(it as? UIWindow)?.isKeyWindow() == true
}?.let { (it as UIWindow).rootViewController } ?: return
val alert = UIAlertController.alertControllerWithTitle(
title = null,
message = message,
preferredStyle = platform.UIKit.UIAlertControllerStyleAlert,
preferredStyle = UIAlertControllerStyleAlert,
)
rootVc.presentViewController(alert, animated = true, completion = null)
// Auto-dismiss after 1.5 seconds
platform.Foundation.NSTimer.scheduledTimerWithTimeInterval(
NSTimer.scheduledTimerWithTimeInterval(
interval = 1.5,
repeats = false,
) {

View file

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

View file

@ -1,5 +1,11 @@
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.koin.test.verify.verify
@ -8,12 +14,12 @@ class FilesModuleVerificationTest {
fun verifyFilesModule() {
filesModule.verify(
extraTypes = listOf(
android.content.Context::class,
android.app.Application::class,
androidx.lifecycle.SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.FileRepository::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
com.garfiec.librechat.feature.files.platform.FileReader::class,
Context::class,
Application::class,
SavedStateHandle::class,
FileRepository::class,
ServerDataStore::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.platform.FileReader
import com.garfiec.librechat.feature.files.platform.formatFileSize
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
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.

View file

@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.settings.viewmodel.delegate
import android.content.Context
import android.media.MediaPlayer
import android.speech.tts.TextToSpeech
import android.speech.tts.UtteranceProgressListener
import com.garfiec.librechat.core.common.result.Result
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
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 kotlinx.coroutines.launch
import co.touchlab.kermit.Logger
import java.io.File
/**
* Handles TTS voice selection, test playback, device voice loading, and MediaPlayer lifecycle.
@ -102,7 +104,7 @@ class SpeechSettingsDelegate(
}
}
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 onDone(utteranceId: String?) {
stateHandle.update { copy(isTtsPreviewPlaying = false) }
@ -217,7 +219,7 @@ class SpeechSettingsDelegate(
currentMediaPlayer = null
// 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.writeBytes(audioBytes)

View file

@ -1,5 +1,25 @@
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.koin.test.verify.verify
@ -8,26 +28,26 @@ class SettingsModuleVerificationTest {
fun verifySettingsModule() {
settingsModule.verify(
extraTypes = listOf(
android.content.Context::class,
android.app.Application::class,
androidx.lifecycle.SavedStateHandle::class,
com.garfiec.librechat.core.data.repository.UserRepository::class,
com.garfiec.librechat.core.data.repository.AuthRepository::class,
com.garfiec.librechat.core.data.repository.ConversationRepository::class,
com.garfiec.librechat.core.data.repository.McpRepository::class,
com.garfiec.librechat.core.data.repository.MemoryRepository::class,
com.garfiec.librechat.core.data.repository.SpeechRepository::class,
com.garfiec.librechat.core.data.repository.BalanceRepository::class,
com.garfiec.librechat.core.data.repository.ShareRepository::class,
com.garfiec.librechat.core.data.repository.KeyRepository::class,
com.garfiec.librechat.core.data.repository.ApiKeyRepository::class,
com.garfiec.librechat.core.data.repository.PresetRepository::class,
com.garfiec.librechat.core.data.datastore.ThemeDataStore::class,
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class,
com.garfiec.librechat.feature.settings.util.ContentReader::class,
com.garfiec.librechat.feature.settings.util.PlatformCacheCleaner::class,
com.garfiec.librechat.feature.settings.viewmodel.delegate.SpeechSettingsFactory::class,
Context::class,
Application::class,
SavedStateHandle::class,
UserRepository::class,
AuthRepository::class,
ConversationRepository::class,
McpRepository::class,
MemoryRepository::class,
SpeechRepository::class,
BalanceRepository::class,
ShareRepository::class,
KeyRepository::class,
ApiKeyRepository::class,
PresetRepository::class,
ThemeDataStore::class,
ServerDataStore::class,
SettingsDataStore::class,
ContentReader::class,
PlatformCacheCleaner::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.UserRepository
import com.garfiec.librechat.core.model.User
import io.mockk.Ordering
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@ -182,7 +183,7 @@ class SettingsViewModelTest {
viewModel.deleteAccount()
advanceUntilIdle()
coVerify(ordering = io.mockk.Ordering.ORDERED) {
coVerify(ordering = Ordering.ORDERED) {
userRepository.deleteUser()
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -30,6 +31,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@ -173,7 +175,7 @@ fun ApiKeysScreen(
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
items(
@ -210,7 +212,7 @@ fun ApiKeysScreen(
viewModel.deleteKey(id)
}
},
colors = androidx.compose.material3.ButtonDefaults.textButtonColors(
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error,
),
) {

View file

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

View file

@ -103,7 +103,7 @@ internal fun McpSettingsSection(
@Composable
private fun McpServerItem(
server: McpServer,
serverStatus: com.garfiec.librechat.core.model.mcp.McpServerStatus?,
serverStatus: McpServerStatus?,
isReinitializing: Boolean,
onEdit: () -> 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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -131,7 +132,7 @@ fun SharedLinksScreen(
.fillMaxSize()
.padding(padding),
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
contentPadding = PaddingValues(16.dp),
) {
items(
items = links,

View file

@ -22,6 +22,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@ -108,7 +109,7 @@ internal fun DangerZone(
@Composable
internal fun SettingsRow(
icon: androidx.compose.ui.graphics.vector.ImageVector,
icon: ImageVector,
title: String,
subtitle: String,
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@ -18,6 +20,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import librechat_mobile.feature.settings.generated.resources.Res
import librechat_mobile.feature.settings.generated.resources.*
@ -50,7 +53,7 @@ internal fun TwoFactorSetupDialog(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.small,
) {
androidx.compose.foundation.text.selection.SelectionContainer {
SelectionContainer {
Text(
text = otpauthUrl,
style = MaterialTheme.typography.bodySmall,
@ -59,7 +62,7 @@ internal fun TwoFactorSetupDialog(
}
}
}
androidx.compose.material3.OutlinedTextField(
OutlinedTextField(
value = code,
onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) },
label = { Text(stringResource(Res.string.hint_verification_code)) },
@ -105,7 +108,7 @@ internal fun TwoFactorCodeDialog(
text = description,
style = MaterialTheme.typography.bodyMedium,
)
androidx.compose.material3.OutlinedTextField(
OutlinedTextField(
value = code,
onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) },
label = { Text(stringResource(Res.string.hint_verification_code)) },
@ -160,7 +163,7 @@ internal fun BackupCodesDialog(
Text(
text = code,
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.feature.settings.screen.DeviceVoiceInfo
import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle
import kotlinx.cinterop.ObjCSignatureOverride
import kotlinx.coroutines.launch
import platform.AVFAudio.AVSpeechBoundary
import platform.AVFAudio.AVSpeechSynthesisVoice
@ -28,7 +29,7 @@ class IosSpeechSettingsDelegate(
private val synthesizer = AVSpeechSynthesizer()
private val synthesizerDelegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol {
@kotlinx.cinterop.ObjCSignatureOverride
@ObjCSignatureOverride
override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer,
didFinishSpeechUtterance: AVSpeechUtterance,
@ -36,7 +37,7 @@ class IosSpeechSettingsDelegate(
stateHandle.update { copy(isTtsPreviewPlaying = false) }
}
@kotlinx.cinterop.ObjCSignatureOverride
@ObjCSignatureOverride
override fun speechSynthesizer(
synthesizer: AVSpeechSynthesizer,
didCancelSpeechUtterance: AVSpeechUtterance,

View file

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