diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..4bf918a --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..e1eea1d --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 9ec55cd..5df2168 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -18,10 +18,30 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + // configure signing + signingConfigs { + release { + storeFile file('key.jks') + storePassword 'password' + keyAlias 'key' + keyPassword 'password' + } + } + + signingConfigs { + myConfig { + keyAlias 'key0' + keyPassword 'chatgpt' + storeFile file('key') + storePassword 'chatgpt' + } + } + buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + signingConfig signingConfigs.myConfig } } compileOptions { @@ -45,6 +65,7 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.6.0' implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.preference:preference:1.2.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' @@ -80,6 +101,12 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofit_ver" implementation "com.squareup.retrofit2:converter-gson:$retrofit_ver" + // okhttp + implementation 'com.squareup.okhttp3:okhttp:4.9.3' + implementation 'com.squareup.okhttp3:logging-interceptor:4.9.3' + // okhttp sse + implementation 'com.squareup.okhttp3:okhttp-sse:4.9.3' + // ViewPager2 implementation "androidx.viewpager2:viewpager2:1.0.0" @@ -90,5 +117,8 @@ dependencies { implementation 'androidx.activity:activity-ktx:1.6.1' implementation 'androidx.fragment:fragment-ktx:1.5.5' + // eventbus + implementation 'org.greenrobot:eventbus:3.2.0' + } tasks.register("prepareKotlinBuildScriptModel"){} \ No newline at end of file diff --git a/app/key b/app/key new file mode 100644 index 0000000..1af8f05 Binary files /dev/null and b/app/key differ diff --git a/app/release/app-release.apk b/app/release/app-release.apk new file mode 100644 index 0000000..58a548b Binary files /dev/null and b/app/release/app-release.apk differ diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json new file mode 100644 index 0000000..1667a2c --- /dev/null +++ b/app/release/output-metadata.json @@ -0,0 +1,20 @@ +{ + "version": 3, + "artifactType": { + "type": "APK", + "kind": "Directory" + }, + "applicationId": "com.nohjunh.test", + "variantName": "release", + "elements": [ + { + "type": "SINGLE", + "filters": [], + "attributes": [], + "versionCode": 1, + "versionName": "1.0", + "outputFile": "app-release.apk" + } + ], + "elementType": "File" +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 37871c3..71998f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,7 +2,7 @@ - + + android:usesCleartextTraffic="true" + tools:targetApi="31"> + + + + android:windowSoftInputMode="adjustResize"> diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..08a3b03 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/nohjunh/test/App.kt b/app/src/main/java/com/nohjunh/test/App.kt index e5f3af6..7b2eb6f 100644 --- a/app/src/main/java/com/nohjunh/test/App.kt +++ b/app/src/main/java/com/nohjunh/test/App.kt @@ -6,10 +6,10 @@ import timber.log.Timber class App : Application() { - // Context -> Global init { instance = this } + companion object { private var instance : App? = null fun context() : Context { @@ -20,7 +20,9 @@ class App : Application() { // Timber setting override fun onCreate() { super.onCreate() - Timber.plant(Timber.DebugTree()) + if (BuildConfig.DEBUG) { + Timber.plant(Timber.DebugTree()) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt b/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt index 2ec87ad..a7c0242 100644 --- a/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt +++ b/app/src/main/java/com/nohjunh/test/adapter/ContentAdapter.kt @@ -1,25 +1,27 @@ package com.nohjunh.test.adapter -import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ImageButton import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.RecyclerView import com.nohjunh.test.R import com.nohjunh.test.database.entity.ContentEntity +import timber.log.Timber -class ContentAdapter(val context : Context, private val dataSet : List) : RecyclerView.Adapter() { +class ContentAdapter : RecyclerView.Adapter() { + + private val dataSet: MutableList = mutableListOf() companion object { - private const val Gpt = 1 - private const val User = 2 + const val Gpt = 1 + const val User = 2 + private const val STEAM_PAYLOAD = "steam_payload" } interface DelChatLayoutClick { - fun onLongClick(view : View, position: Int) + fun onLongClick(view: View, position: Int) } var delChatLayoutClick : DelChatLayoutClick? = null @@ -47,7 +49,16 @@ class ContentAdapter(val context : Context, private val dataSet : List) { + if (payloads.isEmpty()) { + onBindViewHolder(holder, position) + } else { + if (payloads[0].toString() == STEAM_PAYLOAD) { + holder.contentTV.text = dataSet[position].content + } + } } override fun getItemCount(): Int { @@ -62,4 +73,36 @@ class ContentAdapter(val context : Context, private val dataSet : List) { + dataSet.clear() + dataSet.addAll(it) + notifyDataSetChanged() + } + + fun getLastContent(): String { + return steamBuilder.toString() + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt b/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt index 290d1b4..081c1b6 100644 --- a/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt +++ b/app/src/main/java/com/nohjunh/test/database/ChatDatabase.kt @@ -4,10 +4,12 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase import com.nohjunh.test.database.dao.ContentDAO import com.nohjunh.test.database.entity.ContentEntity -@Database(entities = [ContentEntity::class], version = 2) +@Database(entities = [ContentEntity::class], version = 3, exportSchema = false) abstract class ChatDatabase : RoomDatabase() { abstract fun contentDAO() : ContentDAO @@ -25,11 +27,19 @@ abstract class ChatDatabase : RoomDatabase() { ChatDatabase::class.java, "chatDatabase" ) - .fallbackToDestructiveMigration() +// .fallbackToDestructiveMigration() + .addMigrations(MIGRATION_2_3) .build() INSTANCE = instance instance } } + + private val MIGRATION_2_3 = object : Migration(2, 3) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE ContentTable ADD COLUMN type INTEGER NOT NULL DEFAULT 0") + database.execSQL("ALTER TABLE ContentTable ADD COLUMN time INTEGER NOT NULL DEFAULT 0") + } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt b/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt index 7c44b78..c4a89f6 100644 --- a/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt +++ b/app/src/main/java/com/nohjunh/test/database/entity/ContentEntity.kt @@ -15,4 +15,17 @@ data class ContentEntity( @ColumnInfo(name = "gptOrUser") var gptOrUser : Int -) \ No newline at end of file +) { + @ColumnInfo(name = "type") + var type: Int = 0 + @ColumnInfo(name = "time") + var time: Long = 0 + + companion object { + const val Gpt = 1 + const val User = 2 + + const val TYPE_CONVERSATION = 0 + const val TYPE_SYSTEM = 1 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/model/GptR.java b/app/src/main/java/com/nohjunh/test/model/GptR.java new file mode 100644 index 0000000..2d7dd45 --- /dev/null +++ b/app/src/main/java/com/nohjunh/test/model/GptR.java @@ -0,0 +1,158 @@ +package com.nohjunh.test.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class GptR { + + @SerializedName("id") + private String id; + @SerializedName("object") + private String object; + @SerializedName("created") + private Integer created; + @SerializedName("model") + private String model; + @SerializedName("usage") + private UsageDTO usage; + @SerializedName("choices") + private List choices; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public Integer getCreated() { + return created; + } + + public void setCreated(Integer created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public UsageDTO getUsage() { + return usage; + } + + public void setUsage(UsageDTO usage) { + this.usage = usage; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public static class UsageDTO { + @SerializedName("prompt_tokens") + private Integer promptTokens; + @SerializedName("completion_tokens") + private Integer completionTokens; + @SerializedName("total_tokens") + private Integer totalTokens; + + public Integer getPromptTokens() { + return promptTokens; + } + + public void setPromptTokens(Integer promptTokens) { + this.promptTokens = promptTokens; + } + + public Integer getCompletionTokens() { + return completionTokens; + } + + public void setCompletionTokens(Integer completionTokens) { + this.completionTokens = completionTokens; + } + + public Integer getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(Integer totalTokens) { + this.totalTokens = totalTokens; + } + } + + public static class ChoicesDTO { + @SerializedName("message") + private MessageDTO message; + @SerializedName("finish_reason") + private String finishReason; + @SerializedName("index") + private Integer index; + + public MessageDTO getMessage() { + return message; + } + + public void setMessage(MessageDTO message) { + this.message = message; + } + + public String getFinishReason() { + return finishReason; + } + + public void setFinishReason(String finishReason) { + this.finishReason = finishReason; + } + + public Integer getIndex() { + return index; + } + + public void setIndex(Integer index) { + this.index = index; + } + + public static class MessageDTO { + @SerializedName("role") + private String role; + @SerializedName("content") + private String content; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + } + } +} diff --git a/app/src/main/java/com/nohjunh/test/model/GptResponse.kt b/app/src/main/java/com/nohjunh/test/model/GptResponse.kt deleted file mode 100644 index 398a274..0000000 --- a/app/src/main/java/com/nohjunh/test/model/GptResponse.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.nohjunh.test.model - -import com.google.gson.JsonArray - -data class GptResponse ( - val choices : JsonArray - -) \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/model/SteamRsp.java b/app/src/main/java/com/nohjunh/test/model/SteamRsp.java new file mode 100644 index 0000000..3e5a9c1 --- /dev/null +++ b/app/src/main/java/com/nohjunh/test/model/SteamRsp.java @@ -0,0 +1,105 @@ +package com.nohjunh.test.model; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class SteamRsp { + + @SerializedName("id") + private String id; + @SerializedName("object") + private String object; + @SerializedName("created") + private long created; + @SerializedName("model") + private String model; + @SerializedName("choices") + private List choices; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getObject() { + return object; + } + + public void setObject(String object) { + this.object = object; + } + + public long getCreated() { + return created; + } + + public void setCreated(long created) { + this.created = created; + } + + public String getModel() { + return model; + } + + public void setModel(String model) { + this.model = model; + } + + public List getChoices() { + return choices; + } + + public void setChoices(List choices) { + this.choices = choices; + } + + public static class ChoicesDTO { + @SerializedName("delta") + private DeltaDTO delta; + @SerializedName("index") + private int index; + + public DeltaDTO getDelta() { + return delta; + } + + public void setDelta(DeltaDTO delta) { + this.delta = delta; + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public static class DeltaDTO { + @SerializedName("role") + private String role; + @SerializedName("content") + private String content; + + public String getRole() { + return role == null ? "" : role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContent() { + return content == null ? "" : content; + } + + public void setContent(String content) { + this.content = content; + } + } + } +} diff --git a/app/src/main/java/com/nohjunh/test/model/event/EventData.kt b/app/src/main/java/com/nohjunh/test/model/event/EventData.kt new file mode 100644 index 0000000..b3bc4eb --- /dev/null +++ b/app/src/main/java/com/nohjunh/test/model/event/EventData.kt @@ -0,0 +1,18 @@ +package com.nohjunh.test.model.event + +data class SteamDataEvent( + val data: String, + val type: Int = TYPE_STEAM +) { + companion object { + const val TYPE_STEAM = 0 + const val TYPE_STEAM_START = 1 + const val TYPE_STEAM_END = 2 + } + + fun isSteamStart() = type == TYPE_STEAM_START + + fun isSteamEnd() = type == TYPE_STEAM_END + + fun isSteam() = type == TYPE_STEAM +} \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/network/Apis.kt b/app/src/main/java/com/nohjunh/test/network/Apis.kt index a8f1806..f220fd4 100644 --- a/app/src/main/java/com/nohjunh/test/network/Apis.kt +++ b/app/src/main/java/com/nohjunh/test/network/Apis.kt @@ -1,18 +1,13 @@ package com.nohjunh.test.network -import com.google.gson.JsonObject -import com.nohjunh.test.model.GptResponse +import com.nohjunh.test.model.GptR +import okhttp3.RequestBody import retrofit2.http.Body -import retrofit2.http.Headers import retrofit2.http.POST interface Apis { - @Headers( - "Content-Type:application/json", - "Authorization:Bearer API") - @POST("v1/completions") - suspend fun postRequest( - @Body json : JsonObject - ) : GptResponse + + @POST(RetrofitInstance.CHAT_URL_PATH) + suspend fun postRequest(@Body json: RequestBody): GptR } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt b/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt index 5e8290d..2111ee1 100644 --- a/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt +++ b/app/src/main/java/com/nohjunh/test/network/RetrofitInstance.kt @@ -1,8 +1,20 @@ package com.nohjunh.test.network +import com.google.gson.Gson +import com.nohjunh.test.BuildConfig +import com.nohjunh.test.model.SteamRsp +import com.nohjunh.test.model.event.SteamDataEvent import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.Response +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.sse.EventSource +import okhttp3.sse.EventSourceListener +import okhttp3.sse.EventSources +import org.greenrobot.eventbus.EventBus import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import timber.log.Timber import java.util.concurrent.TimeUnit @@ -12,10 +24,34 @@ object RetrofitInstance { .connectTimeout(1, TimeUnit.MINUTES) .readTimeout(1, TimeUnit.MINUTES) .writeTimeout(1, TimeUnit.MINUTES) + .addInterceptor(HttpLoggingInterceptor { + if (BuildConfig.DEBUG) { + Timber.d(it) + } + }.apply { + level = HttpLoggingInterceptor.Level.HEADERS + }) + .addInterceptor { chain -> + val request = chain.request() + val newRequest = request.newBuilder() + .addHeader("Content-Type", "application/json") + .addHeader("Authorization", "Bearer $token") + .build() + chain.proceed(newRequest) + } .build() private const val BASE_URL = "https://api.openai.com/" + const val CHAT_URL_PATH = "v1/chat/completions" + + fun getSteamRequest(): Request.Builder { + return Request.Builder() + .url(BASE_URL + CHAT_URL_PATH) + } + + var token = "" + private val client = Retrofit .Builder() .baseUrl(BASE_URL) @@ -27,4 +63,44 @@ object RetrofitInstance { return client } + private val eventFactory by lazy { EventSources.createFactory(okHttpClient) } + private val gson by lazy { Gson() } + + private val sseListener by lazy { + object : EventSourceListener() { + override fun onClosed(eventSource: EventSource) { + Timber.d("Event close ${eventSource.request().url}") + } + + override fun onEvent(eventSource: EventSource, id: String?, type: String?, data: String) { + super.onEvent(eventSource, id, type, data) + Timber.d("Event data $data id $id type $type") + if (data == "[DONE]") { + EventBus.getDefault().post(SteamDataEvent("", SteamDataEvent.TYPE_STEAM_END)) + } else { + val streamRsp = gson.fromJson(data, SteamRsp::class.java) + if (streamRsp.choices.isEmpty()) return + val delta = streamRsp.choices.first().delta + val content = delta.content + if (content.isEmpty()) return + EventBus.getDefault().post(SteamDataEvent(content, if(delta.role.isEmpty()) SteamDataEvent.TYPE_STEAM else SteamDataEvent.TYPE_STEAM_START)) + } + } + + override fun onFailure(eventSource: EventSource, t: Throwable?, response: Response?) { + super.onFailure(eventSource, t, response) + Timber.d("Event fail ${eventSource.request().url} $t $response") + } + + override fun onOpen(eventSource: EventSource, response: Response) { + super.onOpen(eventSource, response) + Timber.d("Event open ${eventSource.request().url}") + } + } + } + + fun sendRequestSteam(request: Request) { + eventFactory.newEventSource(request, sseListener) + } + } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt b/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt index aece5df..616f9aa 100644 --- a/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt +++ b/app/src/main/java/com/nohjunh/test/repository/DatabaseRepository.kt @@ -11,7 +11,14 @@ class DatabaseRepository { fun getContentData() = database.contentDAO().getContentData() - fun insertContent(content : String, gptOrUser : Int) = database.contentDAO().insertContent(ContentEntity(0, content, gptOrUser)) + fun insertContent(content: String, gptOrUser: Int, msgType: Int): ContentEntity { + val bean = ContentEntity(0, content, gptOrUser).apply { + type = msgType + time = System.currentTimeMillis() + } + database.contentDAO().insertContent(bean) + return bean + } fun deleteSelectedContent(id : Int) = database.contentDAO().deleteSelectedContent(id) diff --git a/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt b/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt index d46d3a6..75da4cb 100644 --- a/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt +++ b/app/src/main/java/com/nohjunh/test/repository/NetWorkRepository.kt @@ -1,13 +1,21 @@ package com.nohjunh.test.repository -import com.google.gson.JsonObject import com.nohjunh.test.network.Apis import com.nohjunh.test.network.RetrofitInstance -import org.json.JSONObject +import okhttp3.RequestBody class NetWorkRepository { - private val chatGPTClient = RetrofitInstance.getInstance().create(Apis::class.java) + private val chatGPTClient by lazy { RetrofitInstance.getInstance().create(Apis::class.java) } - suspend fun postResponse(jsonData : JsonObject) = chatGPTClient.postRequest(jsonData) + suspend fun postResponse(jsonData: RequestBody) = chatGPTClient.postRequest(jsonData) + + fun setToken(token: String) { + RetrofitInstance.token = token + } + + fun sendSteamRequest(body: RequestBody) { + val request = RetrofitInstance.getSteamRequest().post(body).build() + RetrofitInstance.sendRequestSteam(request) + } } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/repository/SpRepository.kt b/app/src/main/java/com/nohjunh/test/repository/SpRepository.kt new file mode 100644 index 0000000..84093c4 --- /dev/null +++ b/app/src/main/java/com/nohjunh/test/repository/SpRepository.kt @@ -0,0 +1,40 @@ +package com.nohjunh.test.repository + +import android.content.Context + +class SpRepository(context: Context) { + + private val sp = context.getSharedPreferences("config", Context.MODE_PRIVATE) + + fun isFirstOpen(): Boolean { + return sp.getBoolean("isFirstOpen", true) + } + + fun setFirstOpen(isFirstOpen: Boolean) { + sp.edit().putBoolean("isFirstOpen", isFirstOpen).apply() + } + + fun getGptModel(): String { + return sp.getString("gptModel", "gpt-3.5-turbo-0301") ?: "gpt-3.5-turbo-0301" + } + + fun setGptModel(gptModel: String) { + sp.edit().putString("gptModel", gptModel).apply() + } + + fun getToken(): String { + return sp.getString("token", "") ?: "" + } + + fun setToken(token: String) { + sp.edit().putString("token", token).apply() + } + + fun getSendBySteam(): Boolean { + return sp.getBoolean("sendBySteam", true) + } + + fun setSendBySteam(sendBySteam: Boolean) { + sp.edit().putBoolean("sendBySteam", sendBySteam).apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/view/MainActivity.kt b/app/src/main/java/com/nohjunh/test/view/MainActivity.kt index 8384fb5..782e774 100644 --- a/app/src/main/java/com/nohjunh/test/view/MainActivity.kt +++ b/app/src/main/java/com/nohjunh/test/view/MainActivity.kt @@ -1,13 +1,12 @@ package com.nohjunh.test.view -import android.content.DialogInterface import android.os.Build.VERSION.SDK_INT -import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.view.View -import android.widget.ScrollView +import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager @@ -15,24 +14,28 @@ import coil.Coil import coil.ImageLoader import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder -import coil.load +import com.google.android.material.textfield.TextInputLayout +import com.nohjunh.test.BuildConfig import com.nohjunh.test.R import com.nohjunh.test.adapter.ContentAdapter import com.nohjunh.test.database.entity.ContentEntity import com.nohjunh.test.databinding.ActivityMainBinding +import com.nohjunh.test.model.event.SteamDataEvent +import com.nohjunh.test.view.settings.SettingsActivity import com.nohjunh.test.viewModel.MainViewModel -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import timber.log.Timber class MainActivity : AppCompatActivity() { - private var branch : Int = 1 // # 1 -> First time loading - private lateinit var binding : ActivityMainBinding - private val viewModel : MainViewModel by viewModels() - private var contentDataList = ArrayList() + private var branch: Int = 1 // # 1 -> First time loading + private lateinit var binding: ActivityMainBinding + private val viewModel: MainViewModel by viewModels() + + private val layoutManager = LinearLayoutManager(this).apply { stackFromEnd = true } + private val adapter = ContentAdapter() override fun onCreate(savedInstanceState: Bundle?) { @@ -41,9 +44,11 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) + binding.RVContainer.layoutManager = layoutManager + binding.RVContainer.adapter = adapter + viewModel.checkToken() - /* coil GIF 확장 라이브러리 */ - val imageLoader = this?.let { + val imageLoader = this.let { ImageLoader.Builder(it) .components { if (SDK_INT >= 28) { @@ -54,22 +59,14 @@ class MainActivity : AppCompatActivity() { } .build() } - if (imageLoader != null) { - Coil.setImageLoader(imageLoader) - } + Coil.setImageLoader(imageLoader) binding.loading.visibility = View.INVISIBLE - binding.loading.load(R.drawable.loading3) - - // 로딩 되었을 때 바로 content가 보이도록 - viewModel.getContentData() - viewModel.contentList.observe(this, Observer { - contentDataList.clear() - for (entity in it) { - contentDataList.add(entity) - } - setContentListRV(branch) - }) + viewModel.contentList.observe(this) { + Timber.d("contentList: ${it.size}") + adapter.submitList(it) + layoutManager.scrollToPosition(adapter.itemCount - 1) + } viewModel.deleteCheck.observe(this, Observer { if (it == true) { @@ -86,52 +83,114 @@ class MainActivity : AppCompatActivity() { branch = 2 }) + viewModel.showLoading.observe(this) { + if (it == true) { + binding.loading.visibility = View.VISIBLE + } else { + binding.loading.visibility = View.INVISIBLE + } + } + + viewModel.showDeleteGuide.observe(this) { +// if (it == true) { +// showAsk(getString(R.string.ask_title_notice), getString(R.string.ask_msg_delete_guide), { +// viewModel.resetFirstOpen() +// }) +// } + } + viewModel.chatGptNewMsg.observe(this) { + if (it != null) { + adapter.addMsg(it) + layoutManager.scrollToPosition(adapter.itemCount - 1) + } + } + binding.sendBtn.setOnClickListener { binding.loading.visibility = View.VISIBLE - - viewModel.postResponse(binding.EDView.text.toString()) - viewModel.insertContent(binding.EDView.text.toString(), 2) // 1: Gpt, 2: User - binding.EDView.setText("") + val msg = binding.EDView.text.toString().trim() + if (msg.isEmpty()) { + viewModel.insertContent(getString(R.string.chat_tips), ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM) // 1: Gpt, 2: User + return@setOnClickListener + } + if (!BuildConfig.DEBUG) { + binding.EDView.setText("") + } branch = 2 - viewModel.getContentData() + viewModel.insertContent(msg, ContentEntity.User, ContentEntity.TYPE_CONVERSATION, false) // 1: Gpt, 2: User + adapter.addMsg(ContentEntity(0, msg, ContentEntity.User)) + + if (viewModel.sendBySteam) { + adapter.addChatGpt("waiting for response...") + } + layoutManager.scrollToPosition(adapter.itemCount - 1) + viewModel.postResponse(msg) } + binding.ivSetting.setOnClickListener { +// showInputDialog() + SettingsActivity.start(this) + } + viewModel.getContentData() + if (BuildConfig.DEBUG) { + binding.EDView.setText("介绍一下你自己") + } + + EventBus.getDefault().register(this) } - private fun setContentListRV(branch : Int) { - val contentAdapter = ContentAdapter(this, contentDataList) - binding.RVContainer.adapter = contentAdapter - binding.RVContainer.layoutManager = LinearLayoutManager(this).apply { - // 맨 밑부터 보이게 - stackFromEnd = true - } - // 맨 밑부터 보이게 - //binding.RVContainer.scrollToPosition(contentDataList.size-1) - CoroutineScope(Dispatchers.Main).launch { - delay(100) - binding.SVContainer.fullScroll(ScrollView.FOCUS_DOWN); - if (branch != 1) { - binding.EDView.requestFocus() + private fun showInputDialog() { + val inputView = layoutInflater.inflate(R.layout.input_layout, null) + + val builder = AlertDialog.Builder(this) + builder.setTitle(getString(R.string.ask_title_setting)) + builder.setMessage(getString(R.string.ask_msg_setting)) + .setCancelable(false) + .setView(inputView) + builder.setPositiveButton(getString(R.string.ask_ok)) { dialog, which -> + val etInput = inputView.findViewById(R.id.etInput) + val text = etInput.editText?.text.toString().trim() + if (text.isEmpty()) { + Toast.makeText(this, getString(R.string.api_key_empty_error), Toast.LENGTH_SHORT).show() + return@setPositiveButton } + viewModel.setupApiKey(text) + viewModel.insertContent(getString(R.string.api_key_inserted), ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM) + } + builder.setNegativeButton(getString(R.string.ask_cancel)) { dialog, which -> + dialog.dismiss() } - // onClick 구현 - contentAdapter.delChatLayoutClick = object : ContentAdapter.DelChatLayoutClick { - override fun onLongClick(view : View, position: Int) { - Timber.tag("삭제버튼클릭").e("${contentDataList[position].id}") - // alertDialog - val builder = AlertDialog.Builder(this@MainActivity) - builder.setTitle("대화컨텐츠 삭제") - .setMessage("한번 삭제된 대화는 복구할 수 없습니다.") - .setPositiveButton("확인", - DialogInterface.OnClickListener { dialog, id -> - viewModel.deleteSelectedContent(contentDataList[position].id) - }) - .setNegativeButton("취소", - DialogInterface.OnClickListener { dialog, id -> - }) - // 다이얼로그를 띄워주기 - builder.show() + builder.show() + } + + fun showAsk(title: String, message: String, positiveAction: () -> Unit, negativeAction: (() -> Unit)? = null) { + val builder = AlertDialog.Builder(this@MainActivity) + builder.setCancelable(false) + builder.setTitle(title) + .setMessage(message) + .setPositiveButton(R.string.ask_ok) { _, _ -> + positiveAction() + } + .setNegativeButton(R.string.ask_cancel) { _, _ -> + negativeAction?.invoke() } + builder.show() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun receiveSteamData(data: SteamDataEvent) { + Timber.d("receiveSteamData: ${data.data} type ${data.type}") + if (data.isSteam()) { + adapter.appendLast(data.data) + layoutManager.scrollToPosition(adapter.itemCount - 1) + } else if (data.isSteamStart()) { +// adapter.addLast("waiting for response...") + } else if (data.isSteamEnd()) { + viewModel.insertContent(adapter.getLastContent(), ContentEntity.Gpt, ContentEntity.TYPE_CONVERSATION, false) } } + + override fun onDestroy() { + super.onDestroy() + EventBus.getDefault().unregister(this) + } } \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/view/settings/SettingsActivity.kt b/app/src/main/java/com/nohjunh/test/view/settings/SettingsActivity.kt new file mode 100644 index 0000000..11893da --- /dev/null +++ b/app/src/main/java/com/nohjunh/test/view/settings/SettingsActivity.kt @@ -0,0 +1,30 @@ +package com.nohjunh.test.view.settings + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import com.nohjunh.test.R + +class SettingsActivity : AppCompatActivity() { + + companion object { + @JvmStatic + fun start(context: Context) { + val starter = Intent(context, SettingsFragment::class.java) + context.startActivity(starter) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.settings_activity) + if (savedInstanceState == null) { + supportFragmentManager + .beginTransaction() + .replace(R.id.settings, SettingsFragment.newInstance()) + .commit() + } + supportActionBar?.setDisplayHomeAsUpEnabled(true) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/view/settings/SettingsFragment.kt b/app/src/main/java/com/nohjunh/test/view/settings/SettingsFragment.kt new file mode 100644 index 0000000..c9e1ef2 --- /dev/null +++ b/app/src/main/java/com/nohjunh/test/view/settings/SettingsFragment.kt @@ -0,0 +1,17 @@ +package com.nohjunh.test.view.settings + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import com.nohjunh.test.R + +class SettingsFragment : PreferenceFragmentCompat() { + + companion object { + fun newInstance() = SettingsFragment() + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.root_preferences, rootKey) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt b/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt index ebf86d4..57ef579 100644 --- a/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt +++ b/app/src/main/java/com/nohjunh/test/viewModel/MainViewModel.kt @@ -1,24 +1,35 @@ package com.nohjunh.test.viewModel +import android.app.Application +import android.util.Log +import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.gson.Gson -import com.google.gson.JsonObject +import com.nohjunh.test.App +import com.nohjunh.test.BuildConfig +import com.nohjunh.test.R import com.nohjunh.test.database.entity.ContentEntity -import com.nohjunh.test.model.GptText +import com.nohjunh.test.model.event.SteamDataEvent import com.nohjunh.test.repository.DatabaseRepository import com.nohjunh.test.repository.NetWorkRepository +import com.nohjunh.test.repository.SpRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import org.json.JSONObject +import kotlinx.coroutines.withContext +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.RequestBody.Companion.toRequestBody +import org.greenrobot.eventbus.EventBus import timber.log.Timber +import kotlin.random.Random -class MainViewModel : ViewModel() { +class MainViewModel(application: Application) : AndroidViewModel(application) { private val databaseRepository = DatabaseRepository() private val netWorkRepository = NetWorkRepository() + private val spRepository = SpRepository(application) private var _contentList = MutableLiveData>() val contentList : LiveData> @@ -32,26 +43,90 @@ class MainViewModel : ViewModel() { val gptInsertCheck : LiveData get() = _gptInsertCheck - fun postResponse(query : String) = viewModelScope.launch { - val jsonObject: JsonObject? = JsonObject().apply{ - // params - addProperty("model", "text-davinci-003") - addProperty("prompt", query) - addProperty("temperature", 0) - addProperty("max_tokens", 500) - addProperty("top_p", 1) - addProperty("frequency_penalty", 0.0) - addProperty("presence_penalty", 0.0) + private var _showLoading = MutableLiveData(false) + val showLoading : LiveData + get() = _showLoading + + private var _showDeleteGuide = MutableLiveData(false) + val showDeleteGuide : LiveData + get() = _showDeleteGuide + + private var _chatGptNewMsg = MutableLiveData() + val chatGptNewMsg : LiveData + get() = _chatGptNewMsg + + private val gson by lazy { Gson() } + var sendBySteam = false + + companion object { + private const val TAG = "MainViewModel" + } + + init { + _showDeleteGuide.postValue(spRepository.isFirstOpen()) + sendBySteam = spRepository.getSendBySteam() + } + + fun checkToken() { + val token = spRepository.getToken() + if (token.isNotEmpty()) { + netWorkRepository.setToken(token) + } else { + viewModelScope.launch { + Log.d(TAG, "checkToken: thread: ${Thread.currentThread().name}") + withContext(Dispatchers.IO) { + _chatGptNewMsg.postValue(databaseRepository.insertContent(getApplication().getString(R.string.api_key_empty_error), ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM)) + } + } } - val response = netWorkRepository.postResponse(jsonObject!!) - Timber.tag("응답결과").e("${response.choices.get(0)}") - // json -> object 는 fromJson - // object -> json 은 toJson - val gson = Gson() - val tempjson = gson.toJson(response.choices.get(0)) - val tempgson = gson.fromJson(tempjson, GptText::class.java) - Timber.tag("가공결과").e("${tempgson.text}") - insertContent(tempgson.text.toString(), 1) + } + + fun postResponse(query: String) = viewModelScope.launch { + val jsonObj = mutableMapOf() + + jsonObj["model"] = "gpt-3.5-turbo-0301" + jsonObj["messages"] = listOf(mapOf("role" to "user", "content" to query)) + jsonObj["temperature"] = 1 + jsonObj["max_tokens"] = 1000 + jsonObj["top_p"] = 1 + jsonObj["stream"] = sendBySteam + + withContext(Dispatchers.IO) { + val body = gson.toJson(jsonObj).toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull()) + if (sendBySteam) { +// if (BuildConfig.DEBUG) { +// val text = "我是一个能够为您提供智能化语言交互服务的AI助手。我被称为OpenAI GPT-3,可以帮助您回答各种问题、提供不同主题的建议和信息等。" +// delay(Random.nextLong(1000, 2000)) +// EventBus.getDefault().post(SteamDataEvent("", SteamDataEvent.TYPE_STEAM_START)) +// for (c in text.toCharArray()) { +// EventBus.getDefault().post(SteamDataEvent(c.toString(), SteamDataEvent.TYPE_STEAM)) +// delay(Random.nextLong(50, 500)) +// } +// EventBus.getDefault().post(SteamDataEvent("", SteamDataEvent.TYPE_STEAM_END)) +// } else { + netWorkRepository.sendSteamRequest(body) +// } + } else { + _showLoading.postValue(true) + val msg = try { + val response = netWorkRepository.postResponse(body) + if (response.choices.isEmpty()) { + val content = getApplication().getString(R.string.chat_error_no_answers) + databaseRepository.insertContent(content, ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM) + } else { + val content = response.choices.first().message.content + databaseRepository.insertContent(content, ContentEntity.Gpt, ContentEntity.TYPE_CONVERSATION) + } + } catch (e: Exception) { + Timber.e(e) + val content = getApplication().getString(R.string.chat_error_catch) + e.message + databaseRepository.insertContent(content, ContentEntity.Gpt, ContentEntity.TYPE_SYSTEM) + } + _chatGptNewMsg.postValue(msg) + _showLoading.postValue(false) + } + } + _showLoading.postValue(false) } fun getContentData() = viewModelScope.launch(Dispatchers.IO) { @@ -60,16 +135,25 @@ class MainViewModel : ViewModel() { _gptInsertCheck.postValue(false) } - fun insertContent(content : String, gptOrUser : Int) = viewModelScope.launch(Dispatchers.IO) { - databaseRepository.insertContent(content, gptOrUser) - if (gptOrUser == 1) { - _gptInsertCheck.postValue(true) + fun insertContent(content: String, gptOrUser: Int, type: Int = ContentEntity.TYPE_CONVERSATION, addToView: Boolean = true) = viewModelScope.launch(Dispatchers.IO) { + val insertContent = databaseRepository.insertContent(content, gptOrUser, type) + if (addToView) { + _chatGptNewMsg.postValue(insertContent) } } - fun deleteSelectedContent(id : Int) = viewModelScope.launch(Dispatchers.IO) { + fun deleteSelectedContent(id: Int) = viewModelScope.launch(Dispatchers.IO) { databaseRepository.deleteSelectedContent(id) _deleteCheck.postValue(true) } + fun resetFirstOpen() { + spRepository.setFirstOpen(false) + _showDeleteGuide.postValue(false) + } + + fun setupApiKey(text: String) { + spRepository.setToken(text) + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d1..0000000 --- a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/baseline_settings_24.xml b/app/src/main/res/drawable/baseline_settings_24.xml new file mode 100644 index 0000000..b240b83 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/cornerlayout.xml b/app/src/main/res/drawable/cornerlayout.xml index 81e1af6..267e25d 100644 --- a/app/src/main/res/drawable/cornerlayout.xml +++ b/app/src/main/res/drawable/cornerlayout.xml @@ -3,11 +3,7 @@ android:padding="10dp" android:shape="rectangle" > - + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..1a4c602 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4318d4b..d233f9c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,5 @@ - - - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" /> - - - - - + - + - + - + - + \ No newline at end of file diff --git a/app/src/main/res/layout/gpt_content_item.xml b/app/src/main/res/layout/gpt_content_item.xml index c05bbc5..c7b6d57 100644 --- a/app/src/main/res/layout/gpt_content_item.xml +++ b/app/src/main/res/layout/gpt_content_item.xml @@ -43,12 +43,13 @@ android:id="@+id/rvItemTV" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="TESTTESTTEST" + tools:text="ChatGPT Response" android:textColor="@color/black" - android:textSize="15dp" + android:textSize="15sp" android:fontFamily="@font/nanumgothicbold" android:layout_marginLeft="15dp" android:layout_marginRight="15dp" + android:textIsSelectable="true" android:layout_marginBottom="20dp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/input_layout.xml b/app/src/main/res/layout/input_layout.xml new file mode 100644 index 0000000..fdcedcf --- /dev/null +++ b/app/src/main/res/layout/input_layout.xml @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 0000000..de6591a --- /dev/null +++ b/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/user_content_item.xml b/app/src/main/res/layout/user_content_item.xml index f495f64..7cea7d9 100644 --- a/app/src/main/res/layout/user_content_item.xml +++ b/app/src/main/res/layout/user_content_item.xml @@ -23,19 +23,20 @@ android:id="@+id/rvItemTV" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="TESTTESTTEST" + android:textIsSelectable="true" android:textColor="@color/black" - android:textSize="15dp" + android:textSize="15sp" android:fontFamily="@font/nanumgothicbold" android:layout_marginLeft="15dp" android:layout_marginRight="15dp" app:layout_constraintStart_toStartOf="parent" + tools:text="User Input Message" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index eca70cf..7353dbd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a15df32 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..c2a74d6 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..04fec5f Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..02febac Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..2492b04 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4ec00a6 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15e570a Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..0ab1a2c Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4111434 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..332e77f Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..6cf9ed4 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,12 @@ + + + + Reply + Reply to all + + + + reply + reply_all + + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..d63f5a8 --- /dev/null +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #0095E2 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fde7ba5..c0ba22e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,34 @@ - test + 快聊 + 您想说些什么? + ChatGPT + 确定 + 取消 + 删除对话 + 一旦删除的对话将无法恢复。 + 提示 + 长按对话内容可删除 + API Key + 设置 + 请输入您的 API Key + API Key 必须要设置才能使用,请点击右上角设置按钮进行设置。 + API Key 成功设置,重启后使用。 + 对不起,ChatGPT 无法回答这个问题 + 对不起,ChatGPT 遇到问题了,错误信息: + 您可以跟我对话,历史、人文、科技、甚至是段子、笑话都可以。 + SettingsActivity + + + Messages + Sync + + + Your signature + Default reply action + + + Sync email periodically + Download incoming attachments + Automatically download attachments for incoming emails + Only download attachments when manually requested \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index ceb60e5..e28c171 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -11,6 +11,7 @@ @color/black ?attr/colorPrimaryVariant + ?attr/colorPrimaryVariant true diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml new file mode 100644 index 0000000..67158db --- /dev/null +++ b/app/src/main/res/xml/root_preferences.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/settings.gradle b/settings.gradle index 901f756..12d311f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -12,5 +12,5 @@ dependencyResolutionManagement { mavenCentral() } } -rootProject.name = "test" +rootProject.name = "ChatGPTAndroid" include ':app'