refactor: replace fully-qualified names with proper imports across codebase
This commit is contained in:
parent
2f95d923e7
commit
e8bda6ca05
59 changed files with 409 additions and 216 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]")
|
||||||
|
|
|
||||||
|
|
@ -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" }
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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 = "",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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?,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue