Files
flomo-ai/app/src/main/java/com/example/flomo_ai/SecondActivity.kt

455 lines
18 KiB
Kotlin

package com.example.flomo_ai
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.text.method.PasswordTransformationMethod
import android.util.Log
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import android.widget.RadioGroup
import android.widget.RadioButton
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import com.example.flomo_ai.ui.theme.ThemeManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import org.json.JSONObject
import java.util.*
// Data classes for the new settings structure
data class HeaderConfig(val key: String, val value: String)
data class PromptConfig(val id: String, val title: String, val content: String, val expanded: Boolean = false)
data class ButtonConfig(val id: String, val label: String, val action: String, val apiUrl: String? = null, val apiMethod: String? = null, val apiBodyTemplate: String? = null, val expanded: Boolean = false)
class SecondActivity : AppCompatActivity() {
// View references
private lateinit var etBaseUrl: EditText
private lateinit var etApiKey: EditText
private lateinit var btnToggleApiKey: ImageButton
private lateinit var etModel: EditText
private lateinit var llHeadersList: LinearLayout
private lateinit var btnAddHeader: Button
private lateinit var layoutHeaderContent: LinearLayout
private lateinit var ivHeaderArrow: ImageView
private lateinit var layoutHeaderToggle: LinearLayout
// Prompt view references
private lateinit var llPromptList: LinearLayout
private lateinit var btnAddPrompt: Button
private lateinit var layoutPromptContent: LinearLayout
private lateinit var ivPromptArrow: ImageView
private lateinit var layoutPromptToggle: LinearLayout
// Theme view references
private lateinit var rgThemeMode: RadioGroup
private lateinit var rbThemeFollowSystem: RadioButton
private lateinit var rbThemeLight: RadioButton
private lateinit var rbThemeDark: RadioButton
// Data storage
private var headerConfigs = mutableListOf<HeaderConfig>()
private var promptConfigs = mutableListOf<PromptConfig>()
private var buttonConfigs = mutableListOf<ButtonConfig>()
// API config (for backward compatibility with existing API calls)
private lateinit var apiConfig: APIConfig
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
ThemeManager.applySavedTheme(this)
Log.d("SecondActivity", "onCreate: Starting SecondActivity")
try {
setContentView(R.layout.activity_second)
Log.d("SecondActivity", "onCreate: Layout set")
} catch (e: Exception) {
Log.e("SecondActivity", "onCreate: Error setting layout", e)
throw e
}
try {
// Initialize views
initViews()
Log.d("SecondActivity", "onCreate: Views initialized")
// Load existing configurations
loadConfigurations()
Log.d("SecondActivity", "onCreate: Configurations loaded")
// Setup UI based on loaded data
setupUI()
Log.d("SecondActivity", "onCreate: UI setup completed")
// Back button functionality
findViewById<ImageButton>(R.id.btnBack).setOnClickListener {
Log.d("SecondActivity", "Back button clicked")
finish()
}
// Home button functionality
findViewById<Button>(R.id.btnHome).setOnClickListener {
Log.d("SecondActivity", "Home button clicked")
// Create intent to go back to MainActivity
val intent = Intent(this, MainActivity::class.java)
// Clear the activity stack to start fresh
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish()
}
// Add header button
btnAddHeader.setOnClickListener {
Log.d("SecondActivity", "Add header button clicked")
addHeaderEntry()
}
// Add prompt button
btnAddPrompt.setOnClickListener {
Log.d("SecondActivity", "Add prompt button clicked")
addPromptEntry()
}
Log.d("SecondActivity", "onCreate: Completed successfully")
} catch (e: Exception) {
Log.e("SecondActivity", "onCreate: Error during initialization", e)
throw e
}
}
private fun initViews() {
Log.d("SecondActivity", "initViews: Starting")
try {
etBaseUrl = findViewById(R.id.etBaseUrl)
etApiKey = findViewById(R.id.etApiKey)
btnToggleApiKey = findViewById(R.id.btnToggleApiKey)
etModel = findViewById(R.id.etModel)
// Header Section
llHeadersList = findViewById(R.id.llHeadersList)
btnAddHeader = findViewById(R.id.btnAddHeader)
layoutHeaderContent = findViewById(R.id.layoutHeaderContent)
ivHeaderArrow = findViewById(R.id.ivHeaderArrow)
layoutHeaderToggle = findViewById(R.id.layoutHeaderToggle)
// Prompt Section
llPromptList = findViewById(R.id.llPromptList)
btnAddPrompt = findViewById(R.id.btnAddPrompt)
layoutPromptContent = findViewById(R.id.layoutPromptContent)
ivPromptArrow = findViewById(R.id.ivPromptArrow)
layoutPromptToggle = findViewById(R.id.layoutPromptToggle)
// Theme Section
rgThemeMode = findViewById(R.id.rgThemeMode)
rbThemeFollowSystem = findViewById(R.id.rbThemeFollowSystem)
rbThemeLight = findViewById(R.id.rbThemeLight)
rbThemeDark = findViewById(R.id.rbThemeDark)
Log.d("SecondActivity", "initViews: All views found")
// Setup API key toggle
btnToggleApiKey.setOnClickListener {
Log.d("SecondActivity", "API key toggle clicked")
val isPassword = etApiKey.transformationMethod is PasswordTransformationMethod
etApiKey.transformationMethod = if (isPassword) null else PasswordTransformationMethod()
// Move cursor to end
etApiKey.setSelection(etApiKey.text.length)
// Update icon based on state
if (isPassword) {
btnToggleApiKey.setImageResource(android.R.drawable.ic_menu_view) // Show eye
} else {
btnToggleApiKey.setImageResource(android.R.drawable.ic_lock_idle_lock) // Show lock
}
}
// Setup Header Toggle (Fold/Unfold)
layoutHeaderToggle.setOnClickListener {
Log.d("SecondActivity", "Header toggle clicked")
val isExpanded = layoutHeaderContent.visibility == View.VISIBLE
if (isExpanded) {
layoutHeaderContent.visibility = View.GONE
ivHeaderArrow.rotation = 0f // Point right
} else {
layoutHeaderContent.visibility = View.VISIBLE
ivHeaderArrow.rotation = 90f // Point down
}
}
// Setup Prompt Toggle (Fold/Unfold)
layoutPromptToggle.setOnClickListener {
Log.d("SecondActivity", "Prompt toggle clicked")
val isExpanded = layoutPromptContent.visibility == View.VISIBLE
if (isExpanded) {
layoutPromptContent.visibility = View.GONE
ivPromptArrow.rotation = 0f // Point right
} else {
layoutPromptContent.visibility = View.VISIBLE
ivPromptArrow.rotation = 90f // Point down
}
}
Log.d("SecondActivity", "initViews: Completed")
} catch (e: Exception) {
Log.e("SecondActivity", "initViews: Error finding views", e)
throw e
}
}
private fun loadConfigurations() {
Log.d("SecondActivity", "loadConfigurations: Starting")
// Load shared preferences
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
val json = sharedPrefs.getString("configs", null)
Log.d("SecondActivity", "loadConfigurations: JSON loaded: ${json?.substring(0, minOf(json.length, 100))}")
if (json != null) {
try {
// Try to load as new format first
Log.d("SecondActivity", "loadConfigurations: Trying to parse as SettingsData")
val settings = Gson().fromJson(json, SettingsData::class.java)
Log.d("SecondActivity", "loadConfigurations: SettingsData parsed successfully")
headerConfigs = settings.headerConfigs?.toMutableList() ?: mutableListOf()
promptConfigs = settings.promptConfigs?.toMutableList() ?: mutableListOf()
buttonConfigs = settings.buttonConfigs?.toMutableList() ?: mutableListOf()
// Load LLM config
etBaseUrl.setText(settings.llmConfig?.baseUrl ?: "https://api.openai.com/v1")
etApiKey.setText(settings.llmConfig?.apiKey ?: "")
etModel.setText(settings.llmConfig?.model ?: "gpt-4o")
// Update API key visibility based on whether it has text
updateApiKeyVisibility()
} catch (e: Exception) {
Log.e("SecondActivity", "loadConfigurations: Error parsing SettingsData", e)
// If new format fails, try to load old format for migration
try {
Log.d("SecondActivity", "loadConfigurations: Trying to parse as List<APIConfig>")
val type = object : TypeToken<List<APIConfig>>() {}.type
val oldConfigs = Gson().fromJson<List<APIConfig>>(json, type)
if (oldConfigs.isNotEmpty()) {
val oldConfig = oldConfigs[0]
etBaseUrl.setText(oldConfig.url)
etApiKey.setText(oldConfig.key)
etModel.setText(oldConfig.model)
updateApiKeyVisibility()
}
} catch (e2: Exception) {
Log.e("SecondActivity", "loadConfigurations: Error parsing List<APIConfig>", e2)
// If both fail, use defaults
etBaseUrl.setText("https://api.openai.com/v1")
etModel.setText("gpt-4o")
updateApiKeyVisibility()
}
}
} else {
// No saved config, use defaults
Log.d("SecondActivity", "loadConfigurations: No saved config, using defaults")
etBaseUrl.setText("https://api.openai.com/v1")
etModel.setText("gpt-4o")
updateApiKeyVisibility()
}
Log.d("SecondActivity", "loadConfigurations: Completed")
}
private fun updateApiKeyVisibility() {
val isEmpty = etApiKey.text.toString().isEmpty()
etApiKey.transformationMethod = if (isEmpty) null else PasswordTransformationMethod()
// Keep cursor at end
etApiKey.setSelection(etApiKey.text.length)
}
private fun setupUI() {
// Setup headers
llHeadersList.removeAllViews()
if (headerConfigs.isEmpty()) {
addHeaderEntry() // Add one empty entry by default
} else {
for (header in headerConfigs) {
addHeaderEntry(header.key, header.value)
}
}
// Setup prompts
llPromptList.removeAllViews()
if (promptConfigs.isEmpty()) {
addPromptEntry() // Add one empty entry by default
} else {
for (prompt in promptConfigs) {
addPromptEntry(prompt.title, prompt.content)
}
}
// Setup theme
setupTheme()
}
private fun setupTheme() {
// Get saved theme mode
val themeMode = ThemeManager.getThemeMode(this)
// Set the correct radio button
when (themeMode) {
ThemeManager.THEME_FOLLOW_SYSTEM -> rbThemeFollowSystem.isChecked = true
ThemeManager.THEME_LIGHT -> rbThemeLight.isChecked = true
ThemeManager.THEME_DARK -> rbThemeDark.isChecked = true
else -> rbThemeFollowSystem.isChecked = true
}
// Set up radio group listener
rgThemeMode.setOnCheckedChangeListener { _, checkedId ->
val newMode = when (checkedId) {
R.id.rbThemeFollowSystem -> ThemeManager.THEME_FOLLOW_SYSTEM
R.id.rbThemeLight -> ThemeManager.THEME_LIGHT
R.id.rbThemeDark -> ThemeManager.THEME_DARK
else -> ThemeManager.THEME_FOLLOW_SYSTEM
}
// Save and apply the new theme
ThemeManager.setThemeMode(this, newMode)
Log.d("SecondActivity", "Theme mode changed to: ${ThemeManager.getThemeModeName(newMode)}")
// Recreate activity to apply theme changes
recreate()
}
}
private fun addHeaderEntry(key: String = "", value: String = "") {
val view = layoutInflater.inflate(R.layout.header_entry, null)
val etKey = view.findViewById<EditText>(R.id.etHeaderKey)
val etValue = view.findViewById<EditText>(R.id.etHeaderValue)
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemoveHeader)
etKey.setText(key)
etValue.setText(value)
btnRemove.setOnClickListener {
llHeadersList.removeView(view)
}
llHeadersList.addView(view)
}
private fun addPromptEntry(title: String = "", content: String = "") {
val view = layoutInflater.inflate(R.layout.prompt_entry, null)
val etTitle = view.findViewById<EditText>(R.id.etPromptTitle)
val etContent = view.findViewById<EditText>(R.id.etPromptContent)
val btnRemove = view.findViewById<ImageButton>(R.id.btnRemovePrompt)
etTitle.setText(title)
etContent.setText(content)
btnRemove.setOnClickListener {
llPromptList.removeView(view)
}
llPromptList.addView(view)
}
// Save all configurations when leaving or explicitly saving
override fun onPause() {
super.onPause()
saveConfigurations()
}
private fun saveConfigurations() {
// Update header configs from UI
headerConfigs.clear()
for (i in 0 until llHeadersList.childCount) {
val view = llHeadersList.getChildAt(i)
val key = view.findViewById<EditText>(R.id.etHeaderKey).text.toString()
val value = view.findViewById<EditText>(R.id.etHeaderValue).text.toString()
if (key.isNotBlank() && value.isNotBlank()) {
headerConfigs.add(HeaderConfig(key, value))
}
}
// Update prompt configs from UI
promptConfigs.clear()
for (i in 0 until llPromptList.childCount) {
val view = llPromptList.getChildAt(i)
val title = view.findViewById<EditText>(R.id.etPromptTitle).text.toString()
val content = view.findViewById<EditText>(R.id.etPromptContent).text.toString()
if (title.isNotBlank() && content.isNotBlank()) {
promptConfigs.add(PromptConfig(id = "prompt_$i", title = title, content = content))
}
}
// Note: Buttons are removed from this UI as per new design.
// We keep the list empty for data compatibility.
buttonConfigs.clear()
// Save LLM config
val llmConfig = LLMConfig(
baseUrl = etBaseUrl.text.toString(),
apiKey = etApiKey.text.toString(),
model = etModel.text.toString()
)
// Save everything
val settingsData = SettingsData(
llmConfig = llmConfig,
headerConfigs = headerConfigs,
promptConfigs = promptConfigs,
buttonConfigs = buttonConfigs
)
val json = Gson().toJson(settingsData)
val sharedPrefs = getSharedPreferences("APIConfigs", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("configs", json).apply()
// Also update the legacy APIConfig for backward compatibility
apiConfig = APIConfig(
System.currentTimeMillis(),
"llm-config",
etBaseUrl.text.toString(),
etApiKey.text.toString(),
"",
etModel.text.toString()
)
}
// Legacy APIConfig class for backward compatibility with existing code
data class APIConfig(
val id: Long,
val name: String,
val url: String,
val key: String,
val secretKey: String,
val model: String
)
// New data classes for settings structure
data class LLMConfig(
val baseUrl: String,
val apiKey: String,
val model: String
)
data class SettingsData(
val llmConfig: LLMConfig?,
val headerConfigs: List<HeaderConfig>?,
val promptConfigs: List<PromptConfig>?,
val buttonConfigs: List<ButtonConfig>?
)
}