From 554acfe0b6fe1503cceb8fee26494d95fe42e30b Mon Sep 17 00:00:00 2001 From: Yubyf Date: Sat, 22 Jul 2023 16:57:30 +0800 Subject: [PATCH] fix: Since the OpenAI usage API(/v1/dashboard/billing/usage) is no longer available in July 22, the cost usage on the openai settings page has been replaced with token usage. * Also fixed the API key validation error . --- .../1.json | 58 +++++++++ .../configs/openai/OpenAIConfigsViewModel.kt | 11 +- .../app/configs/openai/OpenAIScreen.kt | 75 +++++++---- .../quotelock/data/api/OpenAIAccount.kt | 9 -- .../data/modules/openai/OpenAIDatabase.kt | 93 ++++++++++++++ .../data/modules/openai/OpenAIRepository.kt | 117 ++++++++---------- .../crossbowffs/quotelock/di/DataModules.kt | 28 +++++ app/src/main/res/values-zh-rCN/strings.xml | 2 +- app/src/main/res/values-zh-rTW/strings.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- 10 files changed, 292 insertions(+), 105 deletions(-) create mode 100644 app/schemas/com.crossbowffs.quotelock.data.modules.openai.OpenAIUsageDatabase/1.json delete mode 100644 app/src/main/java/com/crossbowffs/quotelock/data/api/OpenAIAccount.kt create mode 100644 app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIDatabase.kt diff --git a/app/schemas/com.crossbowffs.quotelock.data.modules.openai.OpenAIUsageDatabase/1.json b/app/schemas/com.crossbowffs.quotelock.data.modules.openai.OpenAIUsageDatabase/1.json new file mode 100644 index 0000000..cd15d2c --- /dev/null +++ b/app/schemas/com.crossbowffs.quotelock.data.modules.openai.OpenAIUsageDatabase/1.json @@ -0,0 +1,58 @@ +{ + "formatVersion": 1, + "database": { + "version": 1, + "identityHash": "56f49147e19f56a03019b2fa3a1bb569", + "entities": [ + { + "tableName": "usage", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `api_key` TEXT NOT NULL, `model` TEXT NOT NULL, `tokens` INTEGER NOT NULL, `timestamp` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "apiKey", + "columnName": "api_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "model", + "columnName": "model", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tokens", + "columnName": "tokens", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '56f49147e19f56a03019b2fa3a1bb569')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIConfigsViewModel.kt b/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIConfigsViewModel.kt index 6247d0c..b01ec39 100644 --- a/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIConfigsViewModel.kt +++ b/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIConfigsViewModel.kt @@ -9,10 +9,10 @@ import com.crossbowffs.quotelock.app.SnackBarEvent import com.crossbowffs.quotelock.app.emptySnackBarEvent import com.crossbowffs.quotelock.data.AsyncResult import com.crossbowffs.quotelock.data.api.AndroidString -import com.crossbowffs.quotelock.data.api.OpenAIAccount import com.crossbowffs.quotelock.data.api.OpenAIConfigs import com.crossbowffs.quotelock.data.modules.openai.OpenAIException import com.crossbowffs.quotelock.data.modules.openai.OpenAIRepository +import com.crossbowffs.quotelock.data.modules.openai.OpenAIUsage import com.crossbowffs.quotelock.utils.Xlog import com.crossbowffs.quotelock.utils.prefix import com.yubyf.quotelockx.R @@ -30,7 +30,7 @@ import javax.inject.Inject data class OpenAIUiState( val openAIConfigs: OpenAIConfigs = OpenAIConfigs(), val validateResult: AsyncResult? = null, - val openAIAccount: OpenAIAccount? = null, + val openAIUsage: OpenAIUsage? = null, ) @HiltViewModel @@ -55,12 +55,11 @@ class OpenAIConfigsViewModel @Inject constructor( openAIConfigs = it, ) }.launchIn(this) - openAIRepository.openAIAccountFlow.onEach { + openAIRepository.openAIUsageFlow.onEach { _uiState.value = _uiState.value.copy( - openAIAccount = it, + openAIUsage = it, ) }.launchIn(this) - runCatching { openAIRepository.fetchAccountInfo() } } } @@ -80,7 +79,7 @@ class OpenAIConfigsViewModel @Inject constructor( if (openAIRepository.apiKey != apiKey) { openAIRepository.apiKey = apiKey _uiState.value = _uiState.value.copy( - openAIAccount = null, + openAIUsage = null, ) viewModelScope.launch { runCatching { openAIRepository.fetchAccountInfo() } diff --git a/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIScreen.kt b/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIScreen.kt index 82643de..f3e2f68 100644 --- a/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIScreen.kt +++ b/app/src/main/java/com/crossbowffs/quotelock/app/configs/openai/OpenAIScreen.kt @@ -32,7 +32,7 @@ import androidx.compose.material.icons.rounded.ArrowDropDown import androidx.compose.material.icons.rounded.ArrowDropUp import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.MonetizationOn +import androidx.compose.material.icons.rounded.Token import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -325,7 +325,7 @@ fun OpenAIScreen( placeholder = { Text( PREF_OPENAI_API_KEY_PREFIX.plus( - "*".toString().repeat(20) + "*".repeat(20) ), color = hintColor ) @@ -379,27 +379,56 @@ fun OpenAIScreen( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { - uiState.openAIAccount?.takeIf { it.apiKey == configs.apiKey }?.let { account -> - Row( - modifier = Modifier - .padding(start = PREFERENCE_ITEM_HORIZONTAL_PADDING, top = 24.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Rounded.MonetizationOn, - modifier = Modifier.size(16.dp), - contentDescription = null - ) - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = stringResource( - id = R.string.module_openai_usage, - account.usageUsd, - account.hardLimitUsd - ), - style = MaterialTheme.typography.labelMedium, - fontWeight = FontWeight.Normal - ) + uiState.openAIUsage?.takeIf { it.apiKey == configs.apiKey }?.let { usage -> + Column { + Spacer(modifier = Modifier.height(24.dp)) + val usageTooltipState: PlainTooltipState = remember { PlainTooltipState() } + PlainTooltipBox( + tooltip = { + val modelColumns = usage.usages.map { it.first } + val tokenColumns = usage.usages.map { it.second } + Row { + Column { + modelColumns.forEach { + Text( + text = it, + ) + } + } + Spacer(modifier = Modifier.width(8.dp)) + Column(horizontalAlignment = Alignment.End) { + tokenColumns.forEach { + Text( + text = it.toString(), + ) + } + } + } + }, + tooltipState = usageTooltipState + ) { + Row( + modifier = Modifier + .padding(start = PREFERENCE_ITEM_HORIZONTAL_PADDING) + .clip(RoundedCornerShape(10)) + .clickable { scope.launch { usageTooltipState.show() } }, + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Rounded.Token, + modifier = Modifier.size(16.dp), + contentDescription = null + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = stringResource( + id = R.string.module_openai_token_usage, usage.totalTokens + ), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Normal + ) + } + } } } ?: Spacer(modifier = Modifier.weight(1f, true)) val haptic = LocalHapticFeedback.current diff --git a/app/src/main/java/com/crossbowffs/quotelock/data/api/OpenAIAccount.kt b/app/src/main/java/com/crossbowffs/quotelock/data/api/OpenAIAccount.kt deleted file mode 100644 index e5a98ea..0000000 --- a/app/src/main/java/com/crossbowffs/quotelock/data/api/OpenAIAccount.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.crossbowffs.quotelock.data.api - -data class OpenAIAccount( - val apiKey: String, - val hasPaymentMethod: Boolean, - val softLimitUsd: Double, - val hardLimitUsd: Double, - val usageUsd: Double, -) \ No newline at end of file diff --git a/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIDatabase.kt b/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIDatabase.kt new file mode 100644 index 0000000..822f759 --- /dev/null +++ b/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIDatabase.kt @@ -0,0 +1,93 @@ +package com.crossbowffs.quotelock.data.modules.openai + +import android.content.Context +import android.provider.BaseColumns +import androidx.room.ColumnInfo +import androidx.room.Dao +import androidx.room.Database +import androidx.room.Delete +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query +import androidx.room.Room +import androidx.room.RoomDatabase +import com.crossbowffs.quotelock.data.modules.openai.OpenAIUsageContract.DATABASE_NAME +import kotlinx.coroutines.flow.Flow + +const val OPENAI_USAGE_DB_VERSION = 1 + +object OpenAIUsageContract { + const val DATABASE_NAME = "openai_usage.db" + + const val TABLE = "usage" + + const val ID = BaseColumns._ID + const val API_KEY = "api_key" + const val MODEL = "model" + const val TOKENS = "tokens" + const val TIMESTAMP = "timestamp" +} + +@Entity(tableName = OpenAIUsageContract.TABLE) +data class OpenAIUsageEntity( + @PrimaryKey(autoGenerate = true) + @ColumnInfo(name = OpenAIUsageContract.ID) + val id: Int? = null, + @ColumnInfo(name = OpenAIUsageContract.API_KEY) + val apiKey: String, + @ColumnInfo(name = OpenAIUsageContract.MODEL) + val model: String, + @ColumnInfo(name = OpenAIUsageContract.TOKENS) + val tokens: Int, + @ColumnInfo(name = OpenAIUsageContract.TIMESTAMP) + val timestamp: Long = System.currentTimeMillis(), +) + +@Dao +interface OpenAIUsageDao { + + @Query( + "SELECT * FROM ${OpenAIUsageContract.TABLE} WHERE ${OpenAIUsageContract.API_KEY} = :apiKey " + + "AND ${OpenAIUsageContract.TIMESTAMP} >= (:startDate / 1000)" + ) + fun getUsageByApiKeyStream(apiKey: String, startDate: Long): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insert(quote: OpenAIUsageEntity): Long? + + @Delete + suspend fun delete(quote: OpenAIUsageEntity): Int + + @Query("DELETE FROM ${OpenAIUsageContract.TABLE} WHERE ${OpenAIUsageContract.ID} = :id") + suspend fun delete(id: Long): Int + + @Query("DELETE FROM ${OpenAIUsageContract.TABLE}") + suspend fun deleteAll() +} + +@Database(entities = [OpenAIUsageEntity::class], version = OPENAI_USAGE_DB_VERSION) +abstract class OpenAIUsageDatabase : RoomDatabase() { + abstract fun dao(): OpenAIUsageDao + + companion object { + @Volatile + private var INSTANCE: OpenAIUsageDatabase? = null + + fun getDatabase(context: Context): OpenAIUsageDatabase { + // if the INSTANCE is not null, then return it, + // if it is, then create the database + return INSTANCE ?: synchronized(this) { + val instance = Room.databaseBuilder( + context.applicationContext, + OpenAIUsageDatabase::class.java, + DATABASE_NAME + ).setJournalMode(JournalMode.TRUNCATE).build() + INSTANCE = instance + // return instance + instance + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIRepository.kt b/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIRepository.kt index 7232750..45286cb 100644 --- a/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIRepository.kt +++ b/app/src/main/java/com/crossbowffs/quotelock/data/modules/openai/OpenAIRepository.kt @@ -8,7 +8,6 @@ import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_A import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_API_KEY import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_CHAT_API_PATH import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_CHAT_SUB_PATH -import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_CHAT_USAGE_PATH import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_LANGUAGE import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_LANGUAGE_DEFAULT import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_MODEL @@ -19,19 +18,15 @@ import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_Q import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_QUOTE_TYPE import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_QUOTE_TYPE_AI_GENERATED import com.crossbowffs.quotelock.app.configs.openai.OpenAIPrefKeys.PREF_OPENAI_QUOTE_TYPE_DEFAULT -import com.crossbowffs.quotelock.data.api.OpenAIAccount import com.crossbowffs.quotelock.data.api.OpenAIConfigs import com.crossbowffs.quotelock.data.modules.openai.chat.OpenAIChatInput import com.crossbowffs.quotelock.data.modules.openai.chat.OpenAIChatMessage import com.crossbowffs.quotelock.data.modules.openai.chat.OpenAIChatResponse import com.crossbowffs.quotelock.data.modules.openai.chat.OpenAIQuote import com.crossbowffs.quotelock.data.modules.openai.chat.OpenAISubscriptionResponse -import com.crossbowffs.quotelock.data.modules.openai.chat.OpenAIUsageResponse import com.crossbowffs.quotelock.data.modules.openai.geo.OpenAITraceResponse import com.crossbowffs.quotelock.data.modules.openai.geo.SUPPORTED_COUNTRY_CODES import com.crossbowffs.quotelock.data.modules.openai.geo.parseTraceResponse -import com.crossbowffs.quotelock.di.IoDispatcher -import com.crossbowffs.quotelock.di.OpenAIDataStore import com.crossbowffs.quotelock.utils.HttpException import com.crossbowffs.quotelock.utils.fetchCustom import com.crossbowffs.quotelock.utils.fetchJson @@ -45,25 +40,33 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import java.io.IOException -import java.text.SimpleDateFormat import java.time.LocalDate -import java.time.ZoneId +import java.time.ZoneOffset import java.util.Calendar import java.util.Date -import java.util.Locale +import java.util.TimeZone import javax.inject.Inject import javax.inject.Singleton import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty +data class OpenAIUsage( + val apiKey: String, + val totalTokens: Int, + val usages: List>, +) + @Singleton class OpenAIRepository @Inject internal constructor( - @OpenAIDataStore private val openaiDataStore: DataStoreDelegate, - @IoDispatcher private val dispatcher: CoroutineDispatcher, + private val openaiDataStore: DataStoreDelegate, + private val openAIUsageDao: OpenAIUsageDao, + dispatcher: CoroutineDispatcher, private val httpClient: HttpClient, private val json: Json, ) { @@ -97,8 +100,8 @@ class OpenAIRepository @Inject internal constructor( private val _openAIConfigsFlow = MutableStateFlow(openAIConfigs) val openAIConfigsFlow = _openAIConfigsFlow.asStateFlow() - private val _openAIAccountFlow = MutableStateFlow(null) - val openAIAccountFlow = _openAIAccountFlow.asStateFlow() + private val _openAIUsageFlow = MutableStateFlow(null) + val openAIUsageFlow = _openAIUsageFlow.asStateFlow() init { openaiDataStore.collectIn(CoroutineScope(dispatcher)) { preferences, key -> @@ -149,6 +152,34 @@ class OpenAIRepository @Inject internal constructor( else -> {} } } + openAIConfigsFlow.onEach { configs -> + configs.apiKey?.let { apiKey -> + val startDate = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + LocalDate.now() + .withDayOfMonth(1) + .atStartOfDay(ZoneOffset.systemDefault()) + .toInstant() + .toEpochMilli() + } else { + Calendar.getInstance(TimeZone.getDefault()).apply { + time = Date() + val year = get(Calendar.YEAR) + val month = get(Calendar.MONTH) + set(year, month, 1, 0, 0) + }.timeInMillis + } + openAIUsageDao.getUsageByApiKeyStream(apiKey, startDate).onEach { usages -> + usages.groupBy(OpenAIUsageEntity::model).mapValues { (_, usageEntities) -> + usageEntities.sumOf { it.tokens } + }.toList().let { usage -> + _openAIUsageFlow.update { + OpenAIUsage(apiKey, usage.sumOf { it.second }, usage) + } + } + }.launchIn(CoroutineScope(dispatcher)) + } + }.launchIn(CoroutineScope(dispatcher)) } private suspend fun checkOpenAIAvailable(): Boolean { @@ -161,29 +192,15 @@ class OpenAIRepository @Inject internal constructor( suspend fun fetchAccountInfo() { val apiKey = requireApiKey() - if (_openAIAccountFlow.value?.apiKey != apiKey) { - _openAIAccountFlow.update { null } - } - val sub = getSubscription(apiKey) - val usage = getUsageOfCurrentMonth(apiKey) - if (sub == null || usage == null) { - throw OpenAIException.ConnectException - } - _openAIAccountFlow.update { - OpenAIAccount( - apiKey, - sub.hasPaymentMethod, - sub.softLimitUsd, - sub.hardLimitUsd, - usage.totalUsage / 100, - ) - } + getSubscription(apiKey) ?: throw OpenAIException.ConnectException } suspend fun requestQuote(): OpenAIQuote? { val quoteType = quoteType val language = language + val apiKey = requireApiKey() return chatByApi( + apiKey = apiKey, messages = listOf( OpenAIChatMessage.system( PREF_OPENAI_QUOTE_SYSTEM_PROMPTS[quoteType] @@ -193,6 +210,12 @@ class OpenAIRepository @Inject internal constructor( ), maxTokens = 2000 )?.let { response -> + OpenAIUsageEntity( + apiKey = apiKey, + model = response.model, + tokens = response.usage.totalTokens, + timestamp = response.created + ).let { openAIUsageDao.insert(it) } val quoteJson = response.choices.firstOrNull()?.message?.content quoteJson?.let(json::decodeFromString).let { if (quoteType == PREF_OPENAI_QUOTE_TYPE_AI_GENERATED) { @@ -218,44 +241,12 @@ class OpenAIRepository @Inject internal constructor( ) } - @Throws(IOException::class) - private suspend fun getUsageOfCurrentMonth(apiKey: String): OpenAIUsageResponse? = - requestOpenAI { host -> - val startData = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - Date.from( - LocalDate.now().withDayOfMonth(1).atStartOfDay(ZoneId.systemDefault()) - .toInstant() - ) - } else { - Calendar.getInstance().apply { - time = Date() - val year = get(Calendar.YEAR) - val month = get(Calendar.MONTH) - set(year, month, 1) - }.time - } - - httpClient.fetchJson( - url = host, - path = PREF_OPENAI_CHAT_USAGE_PATH, - method = HttpMethod.Get, - headers = mapOf( - HttpHeaders.Authorization to "Bearer $apiKey" - ), - queries = mapOf( - "start_date" to PREF_DATE_FORMATTER.format(startData), - "end_date" to PREF_DATE_FORMATTER.format(Date()) - ) - ) - } - @Throws(IOException::class) private suspend fun chatByApi( + apiKey: String, messages: List, maxTokens: Int, ): OpenAIChatResponse? = requestOpenAI { host -> - val apiKey = requireApiKey() httpClient.fetchJson( url = host, path = PREF_OPENAI_CHAT_API_PATH, @@ -304,8 +295,6 @@ class OpenAIRepository @Inject internal constructor( companion object { private const val TAG = "OpenAIConfigRepository" private const val OPENAI_TRACE_URL = "https://chat.openai.com/cdn-cgi/trace" - - private val PREF_DATE_FORMATTER = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT) } } diff --git a/app/src/main/java/com/crossbowffs/quotelock/di/DataModules.kt b/app/src/main/java/com/crossbowffs/quotelock/di/DataModules.kt index 7285116..d826adb 100644 --- a/app/src/main/java/com/crossbowffs/quotelock/di/DataModules.kt +++ b/app/src/main/java/com/crossbowffs/quotelock/di/DataModules.kt @@ -15,16 +15,20 @@ import com.crossbowffs.quotelock.data.modules.custom.CustomQuoteRepository import com.crossbowffs.quotelock.data.modules.custom.database.CustomQuoteDatabase import com.crossbowffs.quotelock.data.modules.fortune.database.FortuneQuoteDatabase import com.crossbowffs.quotelock.data.modules.openai.OpenAIRepository +import com.crossbowffs.quotelock.data.modules.openai.OpenAIUsageDatabase import com.crossbowffs.quotelock.data.modules.wikiquote.WikiquoteRepository import com.crossbowffs.quotelock.data.version.VersionRepository +import com.yubyf.datastore.DataStoreDelegate import dagger.Module import dagger.Provides import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import io.ktor.client.HttpClient import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.json.Json import javax.inject.Qualifier import javax.inject.Singleton @@ -87,6 +91,24 @@ object RepositoryModules { ): CustomQuoteRepository { return CustomQuoteRepository(database.dao(), dispatcher) } + + @Singleton + @Provides + fun provideOpenAIUsageRepository( + @OpenAIDataStore openaiDataStore: DataStoreDelegate, + openAIUsageDatabase: OpenAIUsageDatabase, + @IoDispatcher dispatcher: CoroutineDispatcher, + httpClient: HttpClient, + json: Json, + ): OpenAIRepository { + return OpenAIRepository( + openaiDataStore, + openAIUsageDatabase.dao(), + dispatcher, + httpClient, + json + ) + } } @Module @@ -130,6 +152,12 @@ object DatabaseModules { fun provideFortuneQuoteDatabase(@ApplicationContext context: Context): FortuneQuoteDatabase { return FortuneQuoteDatabase.getDatabase(context) } + + @Singleton + @Provides + fun provideOpenAIUsageDatabase(@ApplicationContext context: Context): OpenAIUsageDatabase { + return OpenAIUsageDatabase.getDatabase(context) + } } @EntryPoint diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index daec5e0..3692394 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -175,7 +175,7 @@ 生成 API Key API 域名 验证 - 已用 $%1$.2f, 限额 $%2$.2f + Token 用量: %d OpenAI 在您的 IP 所在地区不受支持 OpenAI API key 未设置 OpenAI API Key 无效 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1619aea..641cde3 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -150,7 +150,7 @@ 生成 API Key API 域名 驗證 - 已用 $%1$.2f, 限額 $%2$.2f + Token 用量: %d OpenAI 在您的 IP 所在地區不受支持 OpenAI API key 未設置 OpenAI API Key 無效 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c1accb3..9958b96 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -161,7 +161,7 @@ Generate API Key API Host VALIDATE - Usage $%1$.2f, Limit $%2$.2f + Token usage: %d OpenAI is not supported in your IP\'s region OpenAI API key not set Invalid OpenAI API key