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.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.logger.Level
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
class LibreChatApplication : Application(), SingletonImageLoader.Factory {
|
||||
|
|
@ -33,7 +34,7 @@ class LibreChatApplication : Application(), SingletonImageLoader.Factory {
|
|||
try {
|
||||
startKoin {
|
||||
if (BuildConfig.DEBUG) {
|
||||
androidLogger(org.koin.core.logger.Level.DEBUG)
|
||||
androidLogger(Level.DEBUG)
|
||||
}
|
||||
androidContext(this@LibreChatApplication)
|
||||
allowOverride(false)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ class MainActivity : ComponentActivity() {
|
|||
private val connectivityObserver: ConnectivityObserver by inject()
|
||||
private val themeDataStore: ThemeDataStore by inject()
|
||||
|
||||
private var deepLinkUri by mutableStateOf<android.net.Uri?>(null)
|
||||
private var deepLinkUri by mutableStateOf<Uri?>(null)
|
||||
|
||||
/**
|
||||
* Incremented each time a share intent arrives.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import androidx.compose.ui.layout.Layout
|
|||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.garfiec.librechat.core.model.Banner
|
||||
import com.garfiec.librechat.core.ui.components.BannerDisplay
|
||||
import com.garfiec.librechat.shared.navigation.NavHostViewModel
|
||||
import com.garfiec.librechat.shared.navigation.Navigator
|
||||
|
|
@ -226,7 +227,7 @@ private fun MainContent(
|
|||
navigator: Navigator,
|
||||
navHostViewModel: NavHostViewModel,
|
||||
isInAuthFlow: Boolean,
|
||||
banners: List<com.garfiec.librechat.core.model.Banner>,
|
||||
banners: List<Banner>,
|
||||
dismissedBannerIds: Set<String>,
|
||||
onToggleDrawer: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package com.garfiec.librechat
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.common.di.commonModule
|
||||
import com.garfiec.librechat.core.common.network.ConnectivityObserver
|
||||
import com.garfiec.librechat.core.data.datastore.ConfigCacheDataStore
|
||||
|
|
@ -65,7 +68,9 @@ import com.garfiec.librechat.feature.settings.di.settingsModule
|
|||
import com.garfiec.librechat.shared.navigation.sharedAppModule
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import java.io.File
|
||||
import org.junit.Test
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.test.verify.verify
|
||||
import kotlin.reflect.KClass
|
||||
|
|
@ -93,14 +98,14 @@ class KoinGraphVerificationTest {
|
|||
* framework types so every module is verified in a single test class.
|
||||
* If a type is renamed or removed, this test will catch it.
|
||||
*/
|
||||
@OptIn(org.koin.core.annotation.KoinExperimentalAPI::class)
|
||||
@OptIn(KoinExperimentalAPI::class)
|
||||
@Test
|
||||
fun verifyFullKoinGraph() {
|
||||
val extraTypes = mutableListOf<KClass<*>>(
|
||||
// Android framework
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
// core:common provides
|
||||
CoroutineDispatcher::class,
|
||||
CoroutineScope::class,
|
||||
|
|
@ -162,8 +167,8 @@ class KoinGraphVerificationTest {
|
|||
// feature:files platform provides
|
||||
FileReader::class,
|
||||
// Wrappers/DSL types that verify can't resolve via constructor
|
||||
kotlin.Lazy::class,
|
||||
java.io.File::class,
|
||||
Lazy::class,
|
||||
File::class,
|
||||
)
|
||||
|
||||
// Types whose libraries aren't on the app test classpath (transitive
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.garfiec.librechat.core.common.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,8 +10,8 @@ class CommonModuleVerificationTest {
|
|||
fun verifyCommonModule() {
|
||||
commonModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.room.testing.MigrationTestHelper
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -250,7 +251,7 @@ class RoomMigrationTest {
|
|||
).build()
|
||||
|
||||
// Verify we can read the migrated data via DAO
|
||||
val conversation = kotlinx.coroutines.runBlocking {
|
||||
val conversation = runBlocking {
|
||||
roomDb.conversationDao().getById("conv-api-test")
|
||||
}
|
||||
assertThat(conversation).isNotNull()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.garfiec.librechat.core.data.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
|
|
@ -17,7 +18,7 @@ import org.koin.core.module.Module
|
|||
import org.koin.dsl.binds
|
||||
import org.koin.dsl.module
|
||||
|
||||
private val android.content.Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
private val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
|
||||
name = DATASTORE_FILE_NAME,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,35 @@
|
|||
package com.garfiec.librechat.core.data.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.common.network.ConnectivityObserver
|
||||
import com.garfiec.librechat.core.network.api.AgentsApi
|
||||
import com.garfiec.librechat.core.network.api.ApiKeysApi
|
||||
import com.garfiec.librechat.core.network.api.AuthApi
|
||||
import com.garfiec.librechat.core.network.api.BalanceApi
|
||||
import com.garfiec.librechat.core.network.api.BannerApi
|
||||
import com.garfiec.librechat.core.network.api.ChatApi
|
||||
import com.garfiec.librechat.core.network.api.ConfigApi
|
||||
import com.garfiec.librechat.core.network.api.ConversationsApi
|
||||
import com.garfiec.librechat.core.network.api.FilesApi
|
||||
import com.garfiec.librechat.core.network.api.FilesExtApi
|
||||
import com.garfiec.librechat.core.network.api.KeysApi
|
||||
import com.garfiec.librechat.core.network.api.McpApi
|
||||
import com.garfiec.librechat.core.network.api.MemoriesApi
|
||||
import com.garfiec.librechat.core.network.api.MessagesApi
|
||||
import com.garfiec.librechat.core.network.api.PresetsApi
|
||||
import com.garfiec.librechat.core.network.api.PromptsApi
|
||||
import com.garfiec.librechat.core.network.api.SearchApi
|
||||
import com.garfiec.librechat.core.network.api.ShareApi
|
||||
import com.garfiec.librechat.core.network.api.SpeechApi
|
||||
import com.garfiec.librechat.core.network.api.TagsApi
|
||||
import com.garfiec.librechat.core.network.api.UserApi
|
||||
import com.garfiec.librechat.core.network.sse.SseClient
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,37 +38,37 @@ class DataModuleVerificationTest {
|
|||
fun verifyDataModule() {
|
||||
dataModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
kotlin.Lazy::class,
|
||||
io.ktor.client.HttpClient::class,
|
||||
kotlinx.serialization.json.Json::class,
|
||||
kotlinx.coroutines.CoroutineDispatcher::class,
|
||||
com.garfiec.librechat.core.common.network.ConnectivityObserver::class,
|
||||
com.garfiec.librechat.core.network.sse.SseClient::class,
|
||||
com.garfiec.librechat.core.network.api.AuthApi::class,
|
||||
com.garfiec.librechat.core.network.api.UserApi::class,
|
||||
com.garfiec.librechat.core.network.api.ChatApi::class,
|
||||
com.garfiec.librechat.core.network.api.ConversationsApi::class,
|
||||
com.garfiec.librechat.core.network.api.MessagesApi::class,
|
||||
com.garfiec.librechat.core.network.api.FilesApi::class,
|
||||
com.garfiec.librechat.core.network.api.FilesExtApi::class,
|
||||
com.garfiec.librechat.core.network.api.AgentsApi::class,
|
||||
com.garfiec.librechat.core.network.api.PresetsApi::class,
|
||||
com.garfiec.librechat.core.network.api.PromptsApi::class,
|
||||
com.garfiec.librechat.core.network.api.TagsApi::class,
|
||||
com.garfiec.librechat.core.network.api.ShareApi::class,
|
||||
com.garfiec.librechat.core.network.api.ConfigApi::class,
|
||||
com.garfiec.librechat.core.network.api.BalanceApi::class,
|
||||
com.garfiec.librechat.core.network.api.SearchApi::class,
|
||||
com.garfiec.librechat.core.network.api.KeysApi::class,
|
||||
com.garfiec.librechat.core.network.api.ApiKeysApi::class,
|
||||
com.garfiec.librechat.core.network.api.McpApi::class,
|
||||
com.garfiec.librechat.core.network.api.MemoriesApi::class,
|
||||
com.garfiec.librechat.core.network.api.SpeechApi::class,
|
||||
com.garfiec.librechat.core.network.api.BannerApi::class,
|
||||
androidx.datastore.core.DataStore::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
Lazy::class,
|
||||
HttpClient::class,
|
||||
Json::class,
|
||||
CoroutineDispatcher::class,
|
||||
ConnectivityObserver::class,
|
||||
SseClient::class,
|
||||
AuthApi::class,
|
||||
UserApi::class,
|
||||
ChatApi::class,
|
||||
ConversationsApi::class,
|
||||
MessagesApi::class,
|
||||
FilesApi::class,
|
||||
FilesExtApi::class,
|
||||
AgentsApi::class,
|
||||
PresetsApi::class,
|
||||
PromptsApi::class,
|
||||
TagsApi::class,
|
||||
ShareApi::class,
|
||||
ConfigApi::class,
|
||||
BalanceApi::class,
|
||||
SearchApi::class,
|
||||
KeysApi::class,
|
||||
ApiKeysApi::class,
|
||||
McpApi::class,
|
||||
MemoriesApi::class,
|
||||
SpeechApi::class,
|
||||
BannerApi::class,
|
||||
DataStore::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.garfiec.librechat.core.data.db.dao
|
|||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Upsert
|
||||
import com.garfiec.librechat.core.data.db.entity.MessageEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -29,7 +30,7 @@ interface MessageDao {
|
|||
@Query("DELETE FROM messages WHERE conversationId = :conversationId")
|
||||
suspend fun deleteAllForConversation(conversationId: String)
|
||||
|
||||
@androidx.room.Transaction
|
||||
@Transaction
|
||||
suspend fun replaceAllForConversation(conversationId: String, messages: List<MessageEntity>) {
|
||||
deleteAllForConversation(conversationId)
|
||||
upsertAll(messages)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@ import com.garfiec.librechat.core.model.Conversation
|
|||
import com.garfiec.librechat.core.model.EModelEndpoint
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.serializer
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true }
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ fun Conversation.toEntity(): ConversationEntity = ConversationEntity(
|
|||
model = model,
|
||||
agentId = agentId,
|
||||
isArchived = isArchived,
|
||||
tags = json.encodeToString(kotlinx.serialization.builtins.ListSerializer(kotlinx.serialization.serializer<String>()), tags),
|
||||
tags = json.encodeToString(ListSerializer(serializer<String>()), tags),
|
||||
iconURL = iconURL,
|
||||
greeting = greeting,
|
||||
modelParams = null,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.garfiec.librechat.core.data.datastore
|
||||
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.alloc
|
||||
import kotlinx.cinterop.memScoped
|
||||
|
|
@ -36,7 +37,7 @@ import platform.Security.kSecReturnData
|
|||
import platform.Security.kSecValueData
|
||||
import co.touchlab.kermit.Logger
|
||||
|
||||
@OptIn(ExperimentalForeignApi::class, kotlinx.cinterop.BetaInteropApi::class)
|
||||
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
|
||||
class IosTokenDataStore(
|
||||
refreshClient: Lazy<HttpClient>,
|
||||
) : CommonTokenDataStore(refreshClient), ServerUrlKeychainFallback {
|
||||
|
|
|
|||
|
|
@ -14,16 +14,20 @@ import com.garfiec.librechat.core.data.repository.SessionCacheCleaner
|
|||
import com.garfiec.librechat.core.network.client.SecureTokenStorage
|
||||
import com.garfiec.librechat.core.network.client.TokenManager
|
||||
import io.ktor.client.HttpClient
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import okio.Path.Companion.toPath
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.binds
|
||||
import org.koin.dsl.module
|
||||
import platform.Foundation.NSCachesDirectory
|
||||
import platform.Foundation.NSFileManager
|
||||
import platform.Foundation.NSHomeDirectory
|
||||
import platform.Foundation.NSSearchPathForDirectoriesInDomains
|
||||
import platform.Foundation.NSUserDomainMask
|
||||
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
private fun ensureDirectoryExists(path: String) {
|
||||
NSFileManager.defaultManager.createDirectoryAtPath(path, withIntermediateDirectories = true, attributes = null, error = null)
|
||||
}
|
||||
|
|
@ -59,10 +63,10 @@ actual val dataPlatformModule: Module = module {
|
|||
|
||||
// --- Session Cache Cleaner ---
|
||||
single<SessionCacheCleaner> {
|
||||
@OptIn(kotlinx.cinterop.ExperimentalForeignApi::class)
|
||||
val cachePath = platform.Foundation.NSSearchPathForDirectoriesInDomains(
|
||||
platform.Foundation.NSCachesDirectory,
|
||||
platform.Foundation.NSUserDomainMask,
|
||||
@OptIn(ExperimentalForeignApi::class)
|
||||
val cachePath = NSSearchPathForDirectoriesInDomains(
|
||||
NSCachesDirectory,
|
||||
NSUserDomainMask,
|
||||
true,
|
||||
).firstOrNull() as? String ?: error("Unable to resolve NSCachesDirectory")
|
||||
CommonSessionCacheCleaner(cachePath)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
package com.garfiec.librechat.core.network.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import com.garfiec.librechat.core.network.client.ServerUrlProvider
|
||||
import com.garfiec.librechat.core.network.client.TokenManager
|
||||
import io.ktor.client.engine.HttpClientEngine
|
||||
import io.ktor.client.engine.HttpClientEngineFactory
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,12 +14,12 @@ class NetworkModuleVerificationTest {
|
|||
fun verifyNetworkModule() {
|
||||
networkModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
io.ktor.client.engine.HttpClientEngine::class,
|
||||
io.ktor.client.engine.HttpClientEngineFactory::class,
|
||||
com.garfiec.librechat.core.network.client.TokenManager::class,
|
||||
com.garfiec.librechat.core.network.client.ServerUrlProvider::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
HttpClientEngine::class,
|
||||
HttpClientEngineFactory::class,
|
||||
TokenManager::class,
|
||||
ServerUrlProvider::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import io.ktor.client.plugins.HttpTimeout
|
|||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.plugins.defaultRequest
|
||||
import io.ktor.client.plugins.logging.LogLevel
|
||||
import io.ktor.client.plugins.logging.Logger as KtorLogger
|
||||
import io.ktor.client.plugins.logging.Logging
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
|
|
@ -36,7 +37,7 @@ object LibreChatHttpClient {
|
|||
}
|
||||
|
||||
install(Logging) {
|
||||
logger = object : io.ktor.client.plugins.logging.Logger {
|
||||
logger = object : KtorLogger {
|
||||
override fun log(message: String) {
|
||||
val sanitized = message
|
||||
.replace(Regex("Authorization: Bearer [^\\s]+"), "Authorization: Bearer [REDACTED]")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import io.ktor.http.ContentType
|
|||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.isSuccess
|
||||
import io.ktor.http.path
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
|
@ -127,7 +128,7 @@ class SseClient(
|
|||
shouldResume = true
|
||||
}
|
||||
}
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
throw e // Re-throw cancellation — SKIE handles this gracefully
|
||||
} catch (e: Exception) {
|
||||
Logger.e("SSE", e) { "SSE: unhandled exception escaped flow" }
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.garfiec.librechat.core.network.sse
|
|||
import com.garfiec.librechat.core.model.Conversation
|
||||
import com.garfiec.librechat.core.model.Message
|
||||
import com.garfiec.librechat.core.model.StreamEvent
|
||||
import com.garfiec.librechat.core.model.content.MessageContentPart
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
|
@ -210,7 +211,7 @@ class SseEventMapper(private val json: Json) {
|
|||
aggregatedContent.mapNotNull { element ->
|
||||
try {
|
||||
json.decodeFromJsonElement(
|
||||
com.garfiec.librechat.core.model.content.MessageContentPart.serializer(),
|
||||
MessageContentPart.serializer(),
|
||||
element,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.garfiec.librechat.core.network.sse
|
|||
import co.touchlab.kermit.Logger
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.readUTF8Line
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.withTimeout
|
||||
|
|
@ -21,7 +22,7 @@ class SseLineParser(
|
|||
withTimeout(lineReadTimeoutMs) {
|
||||
channel.readUTF8Line()
|
||||
}
|
||||
} catch (e: kotlinx.coroutines.TimeoutCancellationException) {
|
||||
} catch (e: TimeoutCancellationException) {
|
||||
Logger.w("SSE") { "SSE stream stalled: no data for ${lineReadTimeoutMs / 1000}s" }
|
||||
throw SseStreamException("Stream stalled: no data received for ${lineReadTimeoutMs / 1000}s", e)
|
||||
} ?: break
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
package com.garfiec.librechat.feature.agents.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.data.datastore.ServerDataStore
|
||||
import com.garfiec.librechat.core.data.repository.AgentRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConfigRepository
|
||||
import com.garfiec.librechat.core.data.repository.McpRepository
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,13 +15,13 @@ class AgentsModuleVerificationTest {
|
|||
fun verifyAgentsModule() {
|
||||
agentsModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
com.garfiec.librechat.core.data.repository.AgentRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.McpRepository::class,
|
||||
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
AgentRepository::class,
|
||||
ConfigRepository::class,
|
||||
McpRepository::class,
|
||||
ServerDataStore::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.garfiec.librechat.feature.agents.components.model
|
||||
|
||||
@androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class AgentSharingState(
|
||||
val visibility: AgentVisibility = AgentVisibility.PRIVATE,
|
||||
val isCollaborative: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.garfiec.librechat.feature.agents.components.model
|
||||
|
||||
@androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
@Immutable
|
||||
data class SupportContactState(
|
||||
val name: String = "",
|
||||
val email: String = "",
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import androidx.compose.ui.semantics.heading
|
|||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.garfiec.librechat.core.model.EModelEndpoint
|
||||
import com.garfiec.librechat.core.ui.components.AvatarImage
|
||||
import com.garfiec.librechat.core.ui.components.endpointIconPainter
|
||||
import com.garfiec.librechat.core.ui.components.ErrorBanner
|
||||
|
|
@ -216,7 +217,7 @@ fun AgentDetailScreen(
|
|||
imageUrl = agent.avatarUrl,
|
||||
size = 80.dp,
|
||||
fallbackText = agent.name,
|
||||
fallbackIconPainter = endpointIconPainter(com.garfiec.librechat.core.model.EModelEndpoint.AGENTS),
|
||||
fallbackIconPainter = endpointIconPainter(EModelEndpoint.AGENTS),
|
||||
tintIcon = true,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import org.jetbrains.compose.resources.stringResource
|
|||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.garfiec.librechat.core.model.EModelEndpoint
|
||||
import com.garfiec.librechat.core.ui.components.AvatarImage
|
||||
import com.garfiec.librechat.core.ui.components.endpointIconPainter
|
||||
import com.garfiec.librechat.core.ui.components.EmptyState
|
||||
|
|
@ -254,7 +255,7 @@ private fun AgentCard(
|
|||
imageUrl = agent.avatarUrl,
|
||||
size = 40.dp,
|
||||
fallbackText = agent.name,
|
||||
fallbackIconPainter = endpointIconPainter(com.garfiec.librechat.core.model.EModelEndpoint.AGENTS),
|
||||
fallbackIconPainter = endpointIconPainter(EModelEndpoint.AGENTS),
|
||||
tintIcon = true,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.floatOrNull
|
||||
|
|
@ -820,7 +821,7 @@ class AgentEditorViewModel(
|
|||
* Parse model_parameters JsonElement into AgentAdvancedSettings.
|
||||
* The backend stores these as: { "temperature": 0.7, "top_p": 0.9, "max_tokens": 4096 }
|
||||
*/
|
||||
private fun parseModelParameters(params: kotlinx.serialization.json.JsonElement?): AgentAdvancedSettings {
|
||||
private fun parseModelParameters(params: JsonElement?): AgentAdvancedSettings {
|
||||
if (params == null) return AgentAdvancedSettings()
|
||||
return try {
|
||||
val obj = params.jsonObject
|
||||
|
|
@ -839,7 +840,7 @@ class AgentEditorViewModel(
|
|||
* Returns null if no parameters are set (to avoid sending empty objects).
|
||||
*/
|
||||
private fun buildModelParameters(settings: AgentAdvancedSettings): JsonObject? {
|
||||
val map = mutableMapOf<String, kotlinx.serialization.json.JsonElement>()
|
||||
val map = mutableMapOf<String, JsonElement>()
|
||||
settings.temperature?.let { map["temperature"] = JsonPrimitive(it) }
|
||||
settings.topP?.let { map["top_p"] = JsonPrimitive(it) }
|
||||
settings.maxTokens?.let { map["max_tokens"] = JsonPrimitive(it) }
|
||||
|
|
@ -921,13 +922,13 @@ class AgentEditorViewModel(
|
|||
private fun Agent.parseSupportContact(): SupportContactState {
|
||||
val json = supportContact ?: return SupportContactState()
|
||||
return try {
|
||||
val obj = json as? kotlinx.serialization.json.JsonObject
|
||||
val obj = json as? JsonObject
|
||||
?: return SupportContactState()
|
||||
val name = obj["name"]
|
||||
?.let { (it as? kotlinx.serialization.json.JsonPrimitive)?.content }
|
||||
?.let { (it as? JsonPrimitive)?.content }
|
||||
?: ""
|
||||
val email = obj["email"]
|
||||
?.let { (it as? kotlinx.serialization.json.JsonPrimitive)?.content }
|
||||
?.let { (it as? JsonPrimitive)?.content }
|
||||
?: ""
|
||||
SupportContactState(name = name, email = email)
|
||||
} catch (_: Exception) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,14 @@
|
|||
package com.garfiec.librechat.feature.auth.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.data.datastore.ServerDataStore
|
||||
import com.garfiec.librechat.core.data.repository.AuthRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConfigRepository
|
||||
import com.garfiec.librechat.core.data.repository.UserRepository
|
||||
import com.garfiec.librechat.core.network.client.SecureTokenStorage
|
||||
import com.garfiec.librechat.feature.auth.oauth.OAuthLauncher
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,15 +17,15 @@ class AuthModuleVerificationTest {
|
|||
fun verifyAuthModule() {
|
||||
authModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
|
||||
com.garfiec.librechat.core.data.repository.AuthRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.UserRepository::class,
|
||||
com.garfiec.librechat.core.network.client.SecureTokenStorage::class,
|
||||
com.garfiec.librechat.feature.auth.oauth.OAuthLauncher::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
ServerDataStore::class,
|
||||
AuthRepository::class,
|
||||
ConfigRepository::class,
|
||||
UserRepository::class,
|
||||
SecureTokenStorage::class,
|
||||
OAuthLauncher::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.garfiec.librechat.feature.chat.components
|
|||
|
||||
import android.util.Base64
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
|
|
@ -22,7 +23,7 @@ import androidx.media3.exoplayer.ExoPlayer
|
|||
import androidx.media3.exoplayer.source.ProgressiveMediaSource
|
||||
import androidx.media3.ui.PlayerView
|
||||
|
||||
@androidx.annotation.OptIn(UnstableApi::class)
|
||||
@OptIn(UnstableApi::class)
|
||||
@Composable
|
||||
actual fun AudioContent(
|
||||
data: String?,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.garfiec.librechat.feature.chat.components
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
|
|
@ -406,7 +407,7 @@ fun ChatInput(
|
|||
* Stored in the app's cache directory under `camera_photos/` which is
|
||||
* registered in the FileProvider paths XML.
|
||||
*/
|
||||
private fun createCameraPhotoFile(context: android.content.Context): File {
|
||||
private fun createCameraPhotoFile(context: Context): File {
|
||||
val cameraDir = File(context.cacheDir, "camera_photos")
|
||||
if (!cameraDir.exists()) {
|
||||
cameraDir.mkdirs()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.chat.components
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import com.garfiec.librechat.core.model.Attachment
|
||||
import com.garfiec.librechat.core.model.content.MessageContentPart
|
||||
|
||||
@Composable
|
||||
|
|
@ -12,7 +13,7 @@ actual fun ContentPartRenderer(
|
|||
baseUrl: String,
|
||||
fontSizeMultiplier: Float,
|
||||
useKatex: Boolean,
|
||||
attachments: List<com.garfiec.librechat.core.model.Attachment>,
|
||||
attachments: List<Attachment>,
|
||||
showImageDescriptions: Boolean,
|
||||
searchQuery: String?,
|
||||
searchFocusedOccurrence: Int,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.garfiec.librechat.feature.chat.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Color
|
||||
import android.net.http.SslError
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.SslErrorHandler
|
||||
|
|
@ -178,7 +179,7 @@ private fun KatexLatexBlock(
|
|||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
settings.javaScriptEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
settings.allowContentAccess = false
|
||||
|
|
@ -251,7 +252,7 @@ private fun KatexLatexInline(
|
|||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||
)
|
||||
setBackgroundColor(android.graphics.Color.TRANSPARENT)
|
||||
setBackgroundColor(Color.TRANSPARENT)
|
||||
settings.javaScriptEnabled = true
|
||||
settings.allowFileAccess = false
|
||||
settings.allowContentAccess = false
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.app.Activity
|
|||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.speech.RecognizerIntent
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
|
|
@ -201,7 +202,7 @@ actual fun ChatScreen(
|
|||
}
|
||||
} else {
|
||||
// Device speech recognizer path (Device, Google, or Default without server)
|
||||
val intent = android.content.Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply {
|
||||
putExtra(
|
||||
RecognizerIntent.EXTRA_LANGUAGE_MODEL,
|
||||
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package com.garfiec.librechat.feature.chat.util
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.webkit.MimeTypeMap
|
||||
import co.touchlab.kermit.Logger
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* Resolves a human-readable filename from a content URI using the ContentResolver.
|
||||
|
|
@ -224,7 +227,7 @@ internal fun reEncodeImageIfNeeded(bytes: ByteArray, mimeType: String): ReEncode
|
|||
// converts non-matching extensions (e.g. .jpg when imageOutputType=png)
|
||||
// while storing the original MIME type, causing the Anthropic 400 error.
|
||||
return try {
|
||||
val bitmap = android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
|
||||
?: return null // BitmapFactory can't decode this format
|
||||
|
||||
// If already PNG with correct magic bytes, skip re-encoding to save CPU/memory
|
||||
|
|
@ -233,8 +236,8 @@ internal fun reEncodeImageIfNeeded(bytes: ByteArray, mimeType: String): ReEncode
|
|||
return null
|
||||
}
|
||||
|
||||
val outputStream = java.io.ByteArrayOutputStream()
|
||||
bitmap.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
||||
bitmap.recycle()
|
||||
|
||||
val encodedBytes = outputStream.toByteArray()
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.garfiec.librechat.feature.chat.viewmodel.delegate
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import com.garfiec.librechat.core.common.EndpointConstants
|
||||
import com.garfiec.librechat.core.common.result.Result
|
||||
|
|
@ -134,10 +135,10 @@ class FileAttachmentDelegate(
|
|||
var imageHeight: Int? = null
|
||||
if (actualIsImage) {
|
||||
try {
|
||||
val options = android.graphics.BitmapFactory.Options().apply {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
}
|
||||
android.graphics.BitmapFactory.decodeByteArray(uploadBytes, 0, uploadBytes.size, options)
|
||||
BitmapFactory.decodeByteArray(uploadBytes, 0, uploadBytes.size, options)
|
||||
if (options.outWidth > 0 && options.outHeight > 0) {
|
||||
imageWidth = options.outWidth
|
||||
imageHeight = options.outHeight
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.garfiec.librechat.feature.chat.viewmodel.delegate
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Bundle
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import com.garfiec.librechat.core.common.result.Result
|
||||
|
|
@ -9,6 +11,7 @@ import com.garfiec.librechat.core.data.repository.SpeechRepository
|
|||
import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class TextToSpeechDelegate(
|
||||
private val stateHandle: ChatStateHandle,
|
||||
|
|
@ -20,7 +23,7 @@ class TextToSpeechDelegate(
|
|||
|
||||
private var ttsEngine: TextToSpeech? = null
|
||||
private var ttsReady = false
|
||||
private var serverTtsPlayer: android.media.MediaPlayer? = null
|
||||
private var serverTtsPlayer: MediaPlayer? = null
|
||||
|
||||
private fun getOrInitTts(onReady: () -> Unit) {
|
||||
if (ttsReady && ttsEngine != null) {
|
||||
|
|
@ -103,7 +106,7 @@ class TextToSpeechDelegate(
|
|||
engine.setVoice(voice)
|
||||
}
|
||||
}
|
||||
val params = android.os.Bundle()
|
||||
val params = Bundle()
|
||||
engine.speak(text, TextToSpeech.QUEUE_FLUSH, params, messageId)
|
||||
}
|
||||
}
|
||||
|
|
@ -113,12 +116,12 @@ class TextToSpeechDelegate(
|
|||
is Result.Success -> {
|
||||
try {
|
||||
val audioBytes = result.data
|
||||
val tempFile = java.io.File.createTempFile("tts_", ".mp3", appContext.cacheDir)
|
||||
val tempFile = File.createTempFile("tts_", ".mp3", appContext.cacheDir)
|
||||
tempFile.deleteOnExit()
|
||||
tempFile.writeBytes(audioBytes)
|
||||
|
||||
serverTtsPlayer?.release()
|
||||
serverTtsPlayer = android.media.MediaPlayer().apply {
|
||||
serverTtsPlayer = MediaPlayer().apply {
|
||||
setDataSource(tempFile.absolutePath)
|
||||
setOnCompletionListener {
|
||||
it.release()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,24 @@
|
|||
package com.garfiec.librechat.feature.chat.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.common.network.ConnectivityObserver
|
||||
import com.garfiec.librechat.core.data.datastore.ServerDataStore
|
||||
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||
import com.garfiec.librechat.core.data.repository.AgentRepository
|
||||
import com.garfiec.librechat.core.data.repository.ChatRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConfigRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConversationRepository
|
||||
import com.garfiec.librechat.core.data.repository.DraftRepository
|
||||
import com.garfiec.librechat.core.data.repository.FileRepository
|
||||
import com.garfiec.librechat.core.data.repository.McpRepository
|
||||
import com.garfiec.librechat.core.data.repository.MessageRepository
|
||||
import com.garfiec.librechat.core.data.repository.PresetRepository
|
||||
import com.garfiec.librechat.core.data.repository.PromptRepository
|
||||
import com.garfiec.librechat.core.data.repository.ShareRepository
|
||||
import com.garfiec.librechat.core.data.repository.SpeechRepository
|
||||
import com.garfiec.librechat.core.data.repository.UserRepository
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,25 +27,25 @@ class ChatModuleVerificationTest {
|
|||
fun verifyChatModule() {
|
||||
chatModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
com.garfiec.librechat.core.data.repository.AgentRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ChatRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.MessageRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ConversationRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.DraftRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.FileRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.PresetRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.PromptRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ShareRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.SpeechRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.McpRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.UserRepository::class,
|
||||
com.garfiec.librechat.core.common.network.ConnectivityObserver::class,
|
||||
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
|
||||
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
AgentRepository::class,
|
||||
ChatRepository::class,
|
||||
MessageRepository::class,
|
||||
ConfigRepository::class,
|
||||
ConversationRepository::class,
|
||||
DraftRepository::class,
|
||||
FileRepository::class,
|
||||
PresetRepository::class,
|
||||
PromptRepository::class,
|
||||
ShareRepository::class,
|
||||
SpeechRepository::class,
|
||||
McpRepository::class,
|
||||
UserRepository::class,
|
||||
ConnectivityObserver::class,
|
||||
ServerDataStore::class,
|
||||
SettingsDataStore::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ import androidx.compose.ui.semantics.Role
|
|||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
|
|
@ -203,7 +204,7 @@ private fun FilesChip(
|
|||
@Composable
|
||||
private fun ToolIndicatorChip(
|
||||
label: String,
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
icon: ImageVector,
|
||||
semanticDescription: String,
|
||||
) {
|
||||
AssistChip(
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp
|
|||
import librechat_mobile.feature.chat.generated.resources.Res
|
||||
import librechat_mobile.feature.chat.generated.resources.*
|
||||
import kotlin.time.Clock
|
||||
import kotlinx.datetime.DayOfWeek
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
|
||||
|
|
@ -114,7 +115,7 @@ private fun getTimeBasedGreeting(): String {
|
|||
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val hour = now.hour
|
||||
val dayOfWeek = now.dayOfWeek
|
||||
val isWeekend = dayOfWeek == kotlinx.datetime.DayOfWeek.SATURDAY || dayOfWeek == kotlinx.datetime.DayOfWeek.SUNDAY
|
||||
val isWeekend = dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY
|
||||
|
||||
if (isWeekend) {
|
||||
return when {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import com.garfiec.librechat.core.common.ChatLayoutConstants
|
||||
import com.garfiec.librechat.core.model.Attachment
|
||||
import com.garfiec.librechat.core.model.Message
|
||||
import com.garfiec.librechat.core.model.content.MessageContentPart
|
||||
|
||||
|
|
@ -56,7 +57,7 @@ expect fun ContentPartRenderer(
|
|||
baseUrl: String = "",
|
||||
fontSizeMultiplier: Float = 1.0f,
|
||||
useKatex: Boolean = false,
|
||||
attachments: List<com.garfiec.librechat.core.model.Attachment> = emptyList(),
|
||||
attachments: List<Attachment> = emptyList(),
|
||||
showImageDescriptions: Boolean = true,
|
||||
searchQuery: String? = null,
|
||||
searchFocusedOccurrence: Int = -1,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,15 @@
|
|||
package com.garfiec.librechat.feature.chat.viewmodel
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.garfiec.librechat.core.common.ChatLayoutConstants
|
||||
import com.garfiec.librechat.core.common.EndpointConstants
|
||||
import com.garfiec.librechat.core.common.ToolConstants
|
||||
import com.garfiec.librechat.core.data.datastore.ChatFontSize
|
||||
import com.garfiec.librechat.core.data.datastore.LatexRenderer
|
||||
import com.garfiec.librechat.core.model.Agent
|
||||
import com.garfiec.librechat.core.model.Attachment
|
||||
import com.garfiec.librechat.core.model.EndpointConfig
|
||||
import com.garfiec.librechat.core.model.Message
|
||||
import com.garfiec.librechat.core.ui.components.ModelParameters
|
||||
import com.garfiec.librechat.feature.chat.model.McpServerDisplayData
|
||||
import com.garfiec.librechat.feature.chat.model.PresetDisplayData
|
||||
|
|
@ -20,7 +23,7 @@ enum class ChatScreenState { LANDING, LOADING, ACTIVE }
|
|||
* Exposed as a single [StateFlow] to reduce the number of individual subscriptions
|
||||
* in the UI layer.
|
||||
*/
|
||||
@androidx.compose.runtime.Immutable
|
||||
@Immutable
|
||||
data class ChatPreferences(
|
||||
val showImageDescriptions: Boolean = false,
|
||||
val dismissKeyboardOnSend: Boolean = false,
|
||||
|
|
@ -38,19 +41,19 @@ data class ChatPreferences(
|
|||
* @param messageIndex Index into [ChatUiState.displayMessages] containing this occurrence.
|
||||
* @param occurrenceInMessage 0-based index of this occurrence within the message text.
|
||||
*/
|
||||
@androidx.compose.runtime.Immutable
|
||||
@Immutable
|
||||
data class SearchMatch(
|
||||
val messageIndex: Int,
|
||||
val occurrenceInMessage: Int,
|
||||
)
|
||||
|
||||
@androidx.compose.runtime.Immutable
|
||||
@Immutable
|
||||
data class RetryInfo(
|
||||
val attempt: Int,
|
||||
val maxAttempts: Int,
|
||||
)
|
||||
|
||||
@androidx.compose.runtime.Immutable
|
||||
@Immutable
|
||||
data class ActiveToolCall(
|
||||
val id: String,
|
||||
val name: String,
|
||||
|
|
@ -58,10 +61,10 @@ data class ActiveToolCall(
|
|||
val output: String? = null,
|
||||
)
|
||||
|
||||
@androidx.compose.runtime.Immutable
|
||||
@Immutable
|
||||
data class ChatUiState(
|
||||
val screenState: ChatScreenState = ChatScreenState.LANDING,
|
||||
val messages: List<com.garfiec.librechat.core.model.Message> = emptyList(),
|
||||
val messages: List<Message> = emptyList(),
|
||||
val displayMessages: List<MessageNode> = emptyList(),
|
||||
val activeBranches: Map<String, Int> = emptyMap(),
|
||||
val inputText: String = "",
|
||||
|
|
@ -71,7 +74,7 @@ data class ChatUiState(
|
|||
/** Attachments received during SSE streaming (e.g., tool-generated images).
|
||||
* Cleared when streaming ends. Used to provide attachment context while the
|
||||
* final message (with full attachments) has not yet been persisted to Room. */
|
||||
val streamingAttachments: List<com.garfiec.librechat.core.model.Attachment> = emptyList(),
|
||||
val streamingAttachments: List<Attachment> = emptyList(),
|
||||
val selectedModel: String? = null,
|
||||
val selectedEndpoint: String = EndpointConstants.AGENTS,
|
||||
val endpointConfigs: Map<String, EndpointConfig> = emptyMap(),
|
||||
|
|
|
|||
|
|
@ -22,7 +22,9 @@ import com.garfiec.librechat.core.data.repository.PresetRepository
|
|||
import com.garfiec.librechat.core.data.repository.PromptRepository
|
||||
import com.garfiec.librechat.core.data.repository.ShareRepository
|
||||
import com.garfiec.librechat.core.data.repository.UserRepository
|
||||
import com.garfiec.librechat.core.model.Attachment
|
||||
import com.garfiec.librechat.core.model.EModelEndpoint
|
||||
import com.garfiec.librechat.core.model.Message
|
||||
import com.garfiec.librechat.core.model.Preset
|
||||
import com.garfiec.librechat.core.model.StreamEvent
|
||||
import com.garfiec.librechat.core.model.request.EphemeralAgent
|
||||
|
|
@ -38,6 +40,7 @@ import com.garfiec.librechat.feature.chat.viewmodel.delegate.ModelSelectionDeleg
|
|||
import com.garfiec.librechat.feature.chat.viewmodel.delegate.PlatformDelegateFactory
|
||||
import com.garfiec.librechat.feature.chat.viewmodel.delegate.PresetPromptDelegate
|
||||
import com.garfiec.librechat.feature.chat.viewmodel.delegate.toSerialName
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
|
@ -467,7 +470,7 @@ class ChatViewModel(
|
|||
val messageText = text.ifBlank {
|
||||
if (hasFiles) "" else return
|
||||
}
|
||||
val optimisticMessage = com.garfiec.librechat.core.model.Message(
|
||||
val optimisticMessage = Message(
|
||||
messageId = Uuid.random().toString(),
|
||||
conversationId = conversationId ?: "",
|
||||
parentMessageId = lastMessageId,
|
||||
|
|
@ -637,7 +640,7 @@ class ChatViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun editUserMessage(originalMessage: com.garfiec.librechat.core.model.Message, newText: String) {
|
||||
private fun editUserMessage(originalMessage: Message, newText: String) {
|
||||
val parentMessageId = originalMessage.parentMessageId
|
||||
|
||||
isEditOrRegenerate = true
|
||||
|
|
@ -665,7 +668,7 @@ class ChatViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private fun editAiMessage(aiMessage: com.garfiec.librechat.core.model.Message, newText: String) {
|
||||
private fun editAiMessage(aiMessage: Message, newText: String) {
|
||||
val parentUserMessage = _uiState.value.messages.find {
|
||||
it.messageId == aiMessage.parentMessageId
|
||||
} ?: return
|
||||
|
|
@ -750,7 +753,7 @@ class ChatViewModel(
|
|||
private suspend fun collectStreamSafely(stream: Flow<StreamEvent>) {
|
||||
try {
|
||||
stream.collect { event -> handleStreamEvent(event) }
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
} catch (e: CancellationException) {
|
||||
throw e // Never swallow cancellation
|
||||
} catch (e: Exception) {
|
||||
Logger.e(e) { "Stream collection failed" }
|
||||
|
|
@ -1026,7 +1029,7 @@ class ChatViewModel(
|
|||
}
|
||||
}
|
||||
is StreamEvent.AttachmentCreated -> {
|
||||
val attachment = com.garfiec.librechat.core.model.Attachment(
|
||||
val attachment = Attachment(
|
||||
fileId = event.fileId,
|
||||
filename = event.filename,
|
||||
filepath = event.filepath,
|
||||
|
|
|
|||
|
|
@ -42,9 +42,11 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.layout.LayoutCoordinates
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -58,6 +60,7 @@ import androidx.compose.ui.unit.sp
|
|||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.garfiec.librechat.core.common.ChatLayoutConstants
|
||||
import com.garfiec.librechat.core.model.Attachment
|
||||
import com.garfiec.librechat.core.model.Message
|
||||
import com.garfiec.librechat.core.model.content.MessageContentPart
|
||||
import com.garfiec.librechat.core.ui.components.AvatarImage
|
||||
|
|
@ -334,7 +337,7 @@ private fun TwoSidedBubble(
|
|||
@Composable
|
||||
actual fun ContentPartRenderer(
|
||||
part: MessageContentPart, modifier: Modifier, baseUrl: String, fontSizeMultiplier: Float,
|
||||
useKatex: Boolean, attachments: List<com.garfiec.librechat.core.model.Attachment>,
|
||||
useKatex: Boolean, attachments: List<Attachment>,
|
||||
showImageDescriptions: Boolean, searchQuery: String?, searchFocusedOccurrence: Int,
|
||||
onFocusedOccurrencePositioned: ((LayoutCoordinates) -> Unit)?,
|
||||
) {
|
||||
|
|
@ -624,7 +627,7 @@ private fun IosMarkdownTable(
|
|||
|
||||
@Composable
|
||||
private fun TableCell(
|
||||
text: String, style: androidx.compose.ui.text.TextStyle, color: androidx.compose.ui.graphics.Color,
|
||||
text: String, style: TextStyle, color: Color,
|
||||
alignment: TableCellAlignment, cellWidth: Dp, paddingH: Dp, paddingV: Dp, modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
|
|
@ -646,7 +649,7 @@ private fun TableCell(
|
|||
}
|
||||
}
|
||||
|
||||
private fun androidx.compose.ui.text.TextStyle.scale(m: Float): androidx.compose.ui.text.TextStyle {
|
||||
private fun TextStyle.scale(m: Float): TextStyle {
|
||||
if (m == 1.0f) return this
|
||||
return copy(
|
||||
fontSize = if (fontSize.isSpecified) (fontSize.value * m).sp else fontSize,
|
||||
|
|
|
|||
|
|
@ -52,8 +52,10 @@ import co.touchlab.kermit.Logger
|
|||
import coil3.compose.AsyncImage
|
||||
import kotlinx.cinterop.BetaInteropApi
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.cValue
|
||||
import kotlinx.coroutines.delay
|
||||
import platform.AVFAudio.AVAudioPlayer
|
||||
import platform.AVFoundation.AVLayerVideoGravityResizeAspect
|
||||
import platform.AVFoundation.AVPlayer
|
||||
import platform.AVFoundation.AVPlayerItem
|
||||
import platform.AVFoundation.AVPlayerLayer
|
||||
|
|
@ -76,7 +78,10 @@ import platform.Foundation.writeToFile
|
|||
import kotlinx.cinterop.addressOf
|
||||
import kotlinx.cinterop.usePinned
|
||||
import platform.QuartzCore.CALayer
|
||||
import platform.UIKit.UIColor
|
||||
import platform.UIKit.UIView
|
||||
import platform.UIKit.UIViewController
|
||||
import platform.UIKit.UIWindowScene
|
||||
import platform.WebKit.WKNavigation
|
||||
import platform.WebKit.WKNavigationDelegateProtocol
|
||||
import platform.WebKit.WKWebView
|
||||
|
|
@ -385,7 +390,7 @@ actual fun VideoContent(
|
|||
factory = {
|
||||
val view = UIView()
|
||||
val playerLayer = AVPlayerLayer.playerLayerWithPlayer(player)
|
||||
playerLayer.videoGravity = platform.AVFoundation.AVLayerVideoGravityResizeAspect
|
||||
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect
|
||||
view.layer.addSublayer(playerLayer)
|
||||
view
|
||||
},
|
||||
|
|
@ -473,8 +478,8 @@ private fun KatexWebView(
|
|||
.height(contentHeight),
|
||||
factory = {
|
||||
val config = WKWebViewConfiguration()
|
||||
val webView = WKWebView(frame = kotlinx.cinterop.cValue { }, configuration = config)
|
||||
val nativeBg = platform.UIKit.UIColor(
|
||||
val webView = WKWebView(frame = cValue { }, configuration = config)
|
||||
val nativeBg = UIColor(
|
||||
red = bgComposeColor.red.toDouble(),
|
||||
green = bgComposeColor.green.toDouble(),
|
||||
blue = bgComposeColor.blue.toDouble(),
|
||||
|
|
@ -502,7 +507,7 @@ private fun KatexWebView(
|
|||
)
|
||||
}
|
||||
|
||||
private fun colorToCssHex(color: androidx.compose.ui.graphics.Color): String {
|
||||
private fun colorToCssHex(color: Color): String {
|
||||
val r = (color.red * 255).toInt()
|
||||
val g = (color.green * 255).toInt()
|
||||
val b = (color.blue * 255).toInt()
|
||||
|
|
@ -690,7 +695,7 @@ private fun MermaidWKWebView(
|
|||
modifier = modifier,
|
||||
factory = {
|
||||
val config = WKWebViewConfiguration()
|
||||
val webView = WKWebView(frame = kotlinx.cinterop.cValue { }, configuration = config)
|
||||
val webView = WKWebView(frame = cValue { }, configuration = config)
|
||||
webView.setOpaque(false)
|
||||
webView.loadHTMLString(html, baseURL = NSURL.URLWithString("https://cdn.jsdelivr.net"))
|
||||
webView
|
||||
|
|
@ -823,8 +828,8 @@ actual fun FullscreenImageViewer(
|
|||
}
|
||||
}
|
||||
|
||||
private fun getRootViewController(): platform.UIKit.UIViewController? {
|
||||
val scene = UIApplication.sharedApplication.connectedScenes.firstOrNull() as? platform.UIKit.UIWindowScene
|
||||
private fun getRootViewController(): UIViewController? {
|
||||
val scene = UIApplication.sharedApplication.connectedScenes.firstOrNull() as? UIWindowScene
|
||||
return scene?.keyWindow?.rootViewController
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.ui.window.Dialog
|
|||
import androidx.compose.ui.window.DialogProperties
|
||||
import com.garfiec.librechat.feature.chat.components.shareArtifact
|
||||
import kotlinx.cinterop.ExperimentalForeignApi
|
||||
import kotlinx.cinterop.cValue
|
||||
import platform.Foundation.NSURL
|
||||
import platform.WebKit.WKWebView
|
||||
import platform.WebKit.WKWebViewConfiguration
|
||||
|
|
@ -131,7 +132,7 @@ actual fun ArtifactPanel(
|
|||
defaultWebpagePreferences.allowsContentJavaScript = true
|
||||
}
|
||||
val webView = WKWebView(
|
||||
frame = kotlinx.cinterop.cValue { },
|
||||
frame = cValue { },
|
||||
configuration = config,
|
||||
)
|
||||
webView.setOpaque(false)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.garfiec.librechat.core.common.result.Result
|
|||
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||
import com.garfiec.librechat.core.data.repository.SpeechRepository
|
||||
import com.garfiec.librechat.feature.chat.viewmodel.ChatStateHandle
|
||||
import kotlinx.cinterop.ObjCSignatureOverride
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.launch
|
||||
import platform.AVFAudio.AVSpeechBoundary
|
||||
|
|
@ -29,7 +30,7 @@ class IosTts(
|
|||
private val synthesizer = AVSpeechSynthesizer()
|
||||
|
||||
private val delegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol {
|
||||
@kotlinx.cinterop.ObjCSignatureOverride
|
||||
@ObjCSignatureOverride
|
||||
override fun speechSynthesizer(
|
||||
synthesizer: AVSpeechSynthesizer,
|
||||
didFinishSpeechUtterance: AVSpeechUtterance,
|
||||
|
|
@ -37,7 +38,7 @@ class IosTts(
|
|||
stateHandle.update { copy(currentlyReadingMessageId = null) }
|
||||
}
|
||||
|
||||
@kotlinx.cinterop.ObjCSignatureOverride
|
||||
@ObjCSignatureOverride
|
||||
override fun speechSynthesizer(
|
||||
synthesizer: AVSpeechSynthesizer,
|
||||
didCancelSpeechUtterance: AVSpeechUtterance,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,16 @@
|
|||
package com.garfiec.librechat.feature.conversations.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.data.datastore.ServerDataStore
|
||||
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||
import com.garfiec.librechat.core.data.repository.ConfigRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConversationRepository
|
||||
import com.garfiec.librechat.core.data.repository.MessageRepository
|
||||
import com.garfiec.librechat.core.data.repository.SearchRepository
|
||||
import com.garfiec.librechat.core.data.repository.ShareRepository
|
||||
import com.garfiec.librechat.core.data.repository.TagRepository
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,17 +19,17 @@ class ConversationsModuleVerificationTest {
|
|||
fun verifyConversationsModule() {
|
||||
conversationsModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
com.garfiec.librechat.core.data.repository.ConversationRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.MessageRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.TagRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ShareRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.SearchRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ConfigRepository::class,
|
||||
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
|
||||
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
ConversationRepository::class,
|
||||
MessageRepository::class,
|
||||
TagRepository::class,
|
||||
ShareRepository::class,
|
||||
SearchRepository::class,
|
||||
ConfigRepository::class,
|
||||
ServerDataStore::class,
|
||||
SettingsDataStore::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -185,8 +186,8 @@ private fun ActionRow(
|
|||
icon: ImageVector,
|
||||
label: String,
|
||||
onClick: () -> Unit,
|
||||
iconTint: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface,
|
||||
labelColor: androidx.compose.ui.graphics.Color = MaterialTheme.colorScheme.onSurface,
|
||||
iconTint: Color = MaterialTheme.colorScheme.onSurface,
|
||||
labelColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
|
|||
|
|
@ -10,7 +10,13 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import co.touchlab.kermit.Logger
|
||||
import platform.Foundation.NSTimer
|
||||
import platform.UIKit.UIAlertController
|
||||
import platform.UIKit.UIAlertControllerStyleAlert
|
||||
import platform.UIKit.UIApplication
|
||||
import platform.UIKit.UIPasteboard
|
||||
import platform.UIKit.UIWindow
|
||||
import platform.UIKit.UIWindowScene
|
||||
|
||||
actual fun copyToClipboard(text: String, label: String) {
|
||||
UIPasteboard.generalPasteboard.string = text
|
||||
|
|
@ -20,19 +26,19 @@ actual fun showToast(message: String) {
|
|||
// iOS doesn't have native Toast — show a brief alert-style overlay.
|
||||
Logger.i("Toast") { message }
|
||||
// Use UIAlertController as a lightweight toast replacement
|
||||
val scene = platform.UIKit.UIApplication.sharedApplication.connectedScenes
|
||||
.firstOrNull() as? platform.UIKit.UIWindowScene
|
||||
val scene = UIApplication.sharedApplication.connectedScenes
|
||||
.firstOrNull() as? UIWindowScene
|
||||
val rootVc = scene?.windows?.firstOrNull {
|
||||
(it as? platform.UIKit.UIWindow)?.isKeyWindow() == true
|
||||
}?.let { (it as platform.UIKit.UIWindow).rootViewController } ?: return
|
||||
val alert = platform.UIKit.UIAlertController.alertControllerWithTitle(
|
||||
(it as? UIWindow)?.isKeyWindow() == true
|
||||
}?.let { (it as UIWindow).rootViewController } ?: return
|
||||
val alert = UIAlertController.alertControllerWithTitle(
|
||||
title = null,
|
||||
message = message,
|
||||
preferredStyle = platform.UIKit.UIAlertControllerStyleAlert,
|
||||
preferredStyle = UIAlertControllerStyleAlert,
|
||||
)
|
||||
rootVc.presentViewController(alert, animated = true, completion = null)
|
||||
// Auto-dismiss after 1.5 seconds
|
||||
platform.Foundation.NSTimer.scheduledTimerWithTimeInterval(
|
||||
NSTimer.scheduledTimerWithTimeInterval(
|
||||
interval = 1.5,
|
||||
repeats = false,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package com.garfiec.librechat.feature.files.platform
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.pdf.PdfRenderer
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.compose.foundation.Image
|
||||
|
|
@ -115,7 +116,7 @@ actual fun PdfPreview(
|
|||
bitmapHeight,
|
||||
Bitmap.Config.ARGB_8888,
|
||||
)
|
||||
bitmap.eraseColor(android.graphics.Color.WHITE)
|
||||
bitmap.eraseColor(Color.WHITE)
|
||||
page.render(
|
||||
bitmap,
|
||||
null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
package com.garfiec.librechat.feature.files.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.data.datastore.ServerDataStore
|
||||
import com.garfiec.librechat.core.data.repository.FileRepository
|
||||
import com.garfiec.librechat.feature.files.platform.FileReader
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,12 +14,12 @@ class FilesModuleVerificationTest {
|
|||
fun verifyFilesModule() {
|
||||
filesModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
com.garfiec.librechat.core.data.repository.FileRepository::class,
|
||||
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
|
||||
com.garfiec.librechat.feature.files.platform.FileReader::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
FileRepository::class,
|
||||
ServerDataStore::class,
|
||||
FileReader::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import com.garfiec.librechat.feature.files.FileDisplayData
|
|||
import com.garfiec.librechat.feature.files.FilePreviewDisplayData
|
||||
import com.garfiec.librechat.feature.files.platform.FileReader
|
||||
import com.garfiec.librechat.feature.files.platform.formatFileSize
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
|
@ -166,7 +167,7 @@ class FilesViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
private var uploadJob: kotlinx.coroutines.Job? = null
|
||||
private var uploadJob: Job? = null
|
||||
|
||||
/**
|
||||
* Upload a file from a platform-specific file reference.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.settings.viewmodel.delegate
|
|||
import android.content.Context
|
||||
import android.media.MediaPlayer
|
||||
import android.speech.tts.TextToSpeech
|
||||
import android.speech.tts.UtteranceProgressListener
|
||||
import com.garfiec.librechat.core.common.result.Result
|
||||
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||
import com.garfiec.librechat.core.data.repository.SpeechRepository
|
||||
|
|
@ -11,6 +12,7 @@ import com.garfiec.librechat.feature.settings.screen.DeviceVoiceInfo
|
|||
import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle
|
||||
import kotlinx.coroutines.launch
|
||||
import co.touchlab.kermit.Logger
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Handles TTS voice selection, test playback, device voice loading, and MediaPlayer lifecycle.
|
||||
|
|
@ -102,7 +104,7 @@ class SpeechSettingsDelegate(
|
|||
}
|
||||
}
|
||||
stateHandle.update { copy(isTtsPreviewPlaying = true) }
|
||||
tts.setOnUtteranceProgressListener(object : android.speech.tts.UtteranceProgressListener() {
|
||||
tts.setOnUtteranceProgressListener(object : UtteranceProgressListener() {
|
||||
override fun onStart(utteranceId: String?) { /* no-op */ }
|
||||
override fun onDone(utteranceId: String?) {
|
||||
stateHandle.update { copy(isTtsPreviewPlaying = false) }
|
||||
|
|
@ -217,7 +219,7 @@ class SpeechSettingsDelegate(
|
|||
currentMediaPlayer = null
|
||||
|
||||
// Write bytes to a temporary file
|
||||
val tempFile = java.io.File.createTempFile("voice_test", ".mp3", context.cacheDir)
|
||||
val tempFile = File.createTempFile("voice_test", ".mp3", context.cacheDir)
|
||||
tempFile.deleteOnExit()
|
||||
tempFile.writeBytes(audioBytes)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,25 @@
|
|||
package com.garfiec.librechat.feature.settings.di
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import com.garfiec.librechat.core.data.datastore.ServerDataStore
|
||||
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||
import com.garfiec.librechat.core.data.datastore.ThemeDataStore
|
||||
import com.garfiec.librechat.core.data.repository.ApiKeyRepository
|
||||
import com.garfiec.librechat.core.data.repository.AuthRepository
|
||||
import com.garfiec.librechat.core.data.repository.BalanceRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConversationRepository
|
||||
import com.garfiec.librechat.core.data.repository.KeyRepository
|
||||
import com.garfiec.librechat.core.data.repository.McpRepository
|
||||
import com.garfiec.librechat.core.data.repository.MemoryRepository
|
||||
import com.garfiec.librechat.core.data.repository.PresetRepository
|
||||
import com.garfiec.librechat.core.data.repository.ShareRepository
|
||||
import com.garfiec.librechat.core.data.repository.SpeechRepository
|
||||
import com.garfiec.librechat.core.data.repository.UserRepository
|
||||
import com.garfiec.librechat.feature.settings.util.ContentReader
|
||||
import com.garfiec.librechat.feature.settings.util.PlatformCacheCleaner
|
||||
import com.garfiec.librechat.feature.settings.viewmodel.delegate.SpeechSettingsFactory
|
||||
import org.junit.Test
|
||||
import org.koin.test.verify.verify
|
||||
|
||||
|
|
@ -8,26 +28,26 @@ class SettingsModuleVerificationTest {
|
|||
fun verifySettingsModule() {
|
||||
settingsModule.verify(
|
||||
extraTypes = listOf(
|
||||
android.content.Context::class,
|
||||
android.app.Application::class,
|
||||
androidx.lifecycle.SavedStateHandle::class,
|
||||
com.garfiec.librechat.core.data.repository.UserRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.AuthRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ConversationRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.McpRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.MemoryRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.SpeechRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.BalanceRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ShareRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.KeyRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.ApiKeyRepository::class,
|
||||
com.garfiec.librechat.core.data.repository.PresetRepository::class,
|
||||
com.garfiec.librechat.core.data.datastore.ThemeDataStore::class,
|
||||
com.garfiec.librechat.core.data.datastore.ServerDataStore::class,
|
||||
com.garfiec.librechat.core.data.datastore.SettingsDataStore::class,
|
||||
com.garfiec.librechat.feature.settings.util.ContentReader::class,
|
||||
com.garfiec.librechat.feature.settings.util.PlatformCacheCleaner::class,
|
||||
com.garfiec.librechat.feature.settings.viewmodel.delegate.SpeechSettingsFactory::class,
|
||||
Context::class,
|
||||
Application::class,
|
||||
SavedStateHandle::class,
|
||||
UserRepository::class,
|
||||
AuthRepository::class,
|
||||
ConversationRepository::class,
|
||||
McpRepository::class,
|
||||
MemoryRepository::class,
|
||||
SpeechRepository::class,
|
||||
BalanceRepository::class,
|
||||
ShareRepository::class,
|
||||
KeyRepository::class,
|
||||
ApiKeyRepository::class,
|
||||
PresetRepository::class,
|
||||
ThemeDataStore::class,
|
||||
ServerDataStore::class,
|
||||
SettingsDataStore::class,
|
||||
ContentReader::class,
|
||||
PlatformCacheCleaner::class,
|
||||
SpeechSettingsFactory::class,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import com.garfiec.librechat.core.data.repository.ShareRepository
|
|||
import com.garfiec.librechat.core.data.repository.SpeechRepository
|
||||
import com.garfiec.librechat.core.data.repository.UserRepository
|
||||
import com.garfiec.librechat.core.model.User
|
||||
import io.mockk.Ordering
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
|
|
@ -182,7 +183,7 @@ class SettingsViewModelTest {
|
|||
viewModel.deleteAccount()
|
||||
advanceUntilIdle()
|
||||
|
||||
coVerify(ordering = io.mockk.Ordering.ORDERED) {
|
||||
coVerify(ordering = Ordering.ORDERED) {
|
||||
userRepository.deleteUser()
|
||||
authRepository.logout()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.garfiec.librechat.feature.settings.screen
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -30,6 +31,7 @@ import androidx.compose.material3.MaterialTheme
|
|||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -173,7 +175,7 @@ fun ApiKeysScreen(
|
|||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
items(
|
||||
|
|
@ -210,7 +212,7 @@ fun ApiKeysScreen(
|
|||
viewModel.deleteKey(id)
|
||||
}
|
||||
},
|
||||
colors = androidx.compose.material3.ButtonDefaults.textButtonColors(
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error,
|
||||
),
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.garfiec.librechat.feature.settings.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
|
|
@ -25,6 +26,7 @@ import androidx.compose.material3.IconButton
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
|
|
@ -33,6 +35,7 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.heading
|
||||
|
|
@ -286,7 +289,7 @@ private fun AboutInfo(serverUrl: String) {
|
|||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.app_version_label),
|
||||
|
|
@ -301,7 +304,7 @@ private fun AboutInfo(serverUrl: String) {
|
|||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = androidx.compose.foundation.layout.Arrangement.SpaceBetween,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.server_label),
|
||||
|
|
@ -323,14 +326,14 @@ private fun AboutInfo(serverUrl: String) {
|
|||
|
||||
@Composable
|
||||
private fun GeneralSettingsRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
androidx.compose.material3.Surface(
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = onClick,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ internal fun McpSettingsSection(
|
|||
@Composable
|
||||
private fun McpServerItem(
|
||||
server: McpServer,
|
||||
serverStatus: com.garfiec.librechat.core.model.mcp.McpServerStatus?,
|
||||
serverStatus: McpServerStatus?,
|
||||
isReinitializing: Boolean,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.garfiec.librechat.feature.settings.util.copyToClipboard
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
|
@ -131,7 +132,7 @@ fun SharedLinksScreen(
|
|||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
) {
|
||||
items(
|
||||
items = links,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import androidx.compose.material3.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -108,7 +109,7 @@ internal fun DangerZone(
|
|||
|
||||
@Composable
|
||||
internal fun SettingsRow(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector,
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String,
|
||||
onClick: () -> Unit,
|
||||
|
|
|
|||
|
|
@ -6,8 +6,10 @@ import androidx.compose.foundation.layout.Spacer
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
|
|
@ -18,6 +20,7 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.unit.dp
|
||||
import librechat_mobile.feature.settings.generated.resources.Res
|
||||
import librechat_mobile.feature.settings.generated.resources.*
|
||||
|
|
@ -50,7 +53,7 @@ internal fun TwoFactorSetupDialog(
|
|||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
) {
|
||||
androidx.compose.foundation.text.selection.SelectionContainer {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
text = otpauthUrl,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
|
|
@ -59,7 +62,7 @@ internal fun TwoFactorSetupDialog(
|
|||
}
|
||||
}
|
||||
}
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) },
|
||||
label = { Text(stringResource(Res.string.hint_verification_code)) },
|
||||
|
|
@ -105,7 +108,7 @@ internal fun TwoFactorCodeDialog(
|
|||
text = description,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
androidx.compose.material3.OutlinedTextField(
|
||||
OutlinedTextField(
|
||||
value = code,
|
||||
onValueChange = { code = it.filter { ch -> ch.isDigit() }.take(6) },
|
||||
label = { Text(stringResource(Res.string.hint_verification_code)) },
|
||||
|
|
@ -160,7 +163,7 @@ internal fun BackupCodesDialog(
|
|||
Text(
|
||||
text = code,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace,
|
||||
fontFamily = FontFamily.Monospace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import com.garfiec.librechat.core.data.repository.SpeechRepository
|
|||
import com.garfiec.librechat.core.model.speech.TtsVoice
|
||||
import com.garfiec.librechat.feature.settings.screen.DeviceVoiceInfo
|
||||
import com.garfiec.librechat.feature.settings.viewmodel.SettingsStateHandle
|
||||
import kotlinx.cinterop.ObjCSignatureOverride
|
||||
import kotlinx.coroutines.launch
|
||||
import platform.AVFAudio.AVSpeechBoundary
|
||||
import platform.AVFAudio.AVSpeechSynthesisVoice
|
||||
|
|
@ -28,7 +29,7 @@ class IosSpeechSettingsDelegate(
|
|||
private val synthesizer = AVSpeechSynthesizer()
|
||||
|
||||
private val synthesizerDelegate = object : NSObject(), AVSpeechSynthesizerDelegateProtocol {
|
||||
@kotlinx.cinterop.ObjCSignatureOverride
|
||||
@ObjCSignatureOverride
|
||||
override fun speechSynthesizer(
|
||||
synthesizer: AVSpeechSynthesizer,
|
||||
didFinishSpeechUtterance: AVSpeechUtterance,
|
||||
|
|
@ -36,7 +37,7 @@ class IosSpeechSettingsDelegate(
|
|||
stateHandle.update { copy(isTtsPreviewPlaying = false) }
|
||||
}
|
||||
|
||||
@kotlinx.cinterop.ObjCSignatureOverride
|
||||
@ObjCSignatureOverride
|
||||
override fun speechSynthesizer(
|
||||
synthesizer: AVSpeechSynthesizer,
|
||||
didCancelSpeechUtterance: AVSpeechUtterance,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import com.garfiec.librechat.core.data.repository.AuthRepository
|
||||
import com.garfiec.librechat.core.data.repository.BannerRepository
|
||||
import com.garfiec.librechat.core.data.datastore.SettingsDataStore
|
||||
import com.garfiec.librechat.core.data.repository.ConfigRepository
|
||||
import com.garfiec.librechat.core.data.repository.ConversationRepository
|
||||
import com.garfiec.librechat.core.model.Banner
|
||||
|
|
@ -25,7 +26,7 @@ class NavHostViewModel(
|
|||
configRepository: ConfigRepository,
|
||||
conversationRepository: ConversationRepository,
|
||||
private val tokenManager: TokenManager,
|
||||
private val settingsDataStore: com.garfiec.librechat.core.data.datastore.SettingsDataStore,
|
||||
private val settingsDataStore: SettingsDataStore,
|
||||
) : ViewModel() {
|
||||
|
||||
private val bannerStateHolder = BannerStateHolder(bannerRepository, viewModelScope)
|
||||
|
|
|
|||
Loading…
Reference in a new issue