From 9e2ca319de07f90553e7b55f5ac98a23f5c257e5 Mon Sep 17 00:00:00 2001 From: skythinker Date: Mon, 4 Dec 2023 18:00:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EGPT=20Vision=E6=94=AF?= =?UTF-8?q?=E6=8C=81=EF=BC=8C=E6=94=AF=E6=8C=81=E6=8B=8D=E7=85=A7=E3=80=81?= =?UTF-8?q?=E5=9B=BE=E5=BA=93=E3=80=81=E5=88=86=E4=BA=AB=EF=BC=9B=E4=B8=BB?= =?UTF-8?q?=E7=95=8C=E9=9D=A2=E6=B7=BB=E5=8A=A0=E6=A8=A1=E5=9E=8B=E9=80=89?= =?UTF-8?q?=E6=A1=86=EF=BC=9B=E5=85=81=E8=AE=B8=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E6=A8=A1=E5=9E=8B=EF=BC=9B=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E5=AF=B9=E8=AF=9D=E6=B6=88=E6=81=AF=E7=9B=B8=E5=85=B3=E9=80=BB?= =?UTF-8?q?=E8=BE=91=EF=BC=9B=E4=BC=98=E5=8C=96=E9=83=A8=E5=88=86=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=EF=BC=9B=E7=89=88=E6=9C=AC=E5=8F=B7=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA1.7.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/deploymentTargetDropDown.xml | 17 ++ .idea/misc.xml | 5 + README.md | 45 ++- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 19 +- .../gptassistant/ChatApiClient.java | 150 +++++++--- .../gptassistant/GlobalDataHolder.java | 25 +- .../skythinker/gptassistant/MainActivity.java | 278 +++++++++++++++--- .../gptassistant/TabConfActivity.java | 58 +++- app/src/main/res/drawable/image.png | Bin 0 -> 4827 bytes app/src/main/res/drawable/image_enabled.png | Bin 0 -> 4542 bytes app/src/main/res/layout/activity_main.xml | 75 +++-- app/src/main/res/layout/activity_tab_conf.xml | 74 ++++- .../res/layout/ask_accessibility_dialog.xml | 4 +- .../main/res/layout/image_method_dialog.xml | 55 ++++ .../main/res/layout/image_preview_dialog.xml | 98 ++++++ .../res/layout/main_model_spinner_item.xml | 11 + .../layout/model_spinner_dropdown_item.xml | 9 + app/src/main/res/values/strings.xml | 1 - app/src/main/res/xml/file_provider_paths.xml | 5 + readme_img/vision.gif | Bin 0 -> 8770418 bytes 21 files changed, 794 insertions(+), 139 deletions(-) create mode 100644 .idea/deploymentTargetDropDown.xml create mode 100644 app/src/main/res/drawable/image.png create mode 100644 app/src/main/res/drawable/image_enabled.png create mode 100644 app/src/main/res/layout/image_method_dialog.xml create mode 100644 app/src/main/res/layout/image_preview_dialog.xml create mode 100644 app/src/main/res/layout/main_model_spinner_item.xml create mode 100644 app/src/main/res/layout/model_spinner_dropdown_item.xml create mode 100644 app/src/main/res/xml/file_provider_paths.xml create mode 100644 readme_img/vision.gif diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..e016282 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c9463f4..0bc5797 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -9,6 +9,7 @@ + @@ -16,6 +17,10 @@ + + + + diff --git a/README.md b/README.md index 61b5821..331e436 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@
- 国内可用 · 免费使用 · 语音交互 · 支持联网 + 免费聊天 · 语音交互 · 支持联网 · 支持识图
@@ -27,8 +27,6 @@

- - --- ## 介绍 @@ -37,6 +35,7 @@ - 支持用户预设**问题模板**,支持**连续对话**,支持`gpt-3.5-turbo`、`gpt-4`等模型 - **支持联网**,允许GPT获取在线网页 +- 支持拍照或从相册中**上传图片**到GPT Vision模型 - 通过无障碍功能捕获音量键事件,实现在**任意界面唤起** - 支持从**全局上下文菜单**(选中文本后弹出的系统菜单)中直接唤起 - 支持通过状态栏**快捷按钮**唤起 @@ -100,13 +99,25 @@ **四、支持连续对话** -激活上方的对话图标,即可保留当前会话,进行连续对话 +激活上方的对话图标,即可保留当前会话,进行连续对话(点击左侧的头像图标可以对单条对话进行删除、重试等操作)
-**五、支持GPT联网** +**五、支持上传图片到Vision** + +当选择的模型中含有`vision`时(如`gpt-4-vision-preview`),输入框左侧会出现图片按钮,点击后可以拍照或从相册中选择图片 + +从其他应用中分享图片时,也可以选择本程序,将图片添加到输入框 + +
+ +
+ +> 注:Vision模型一般无法免费使用(如Chatanywhere),有需要的用户可以考虑付费服务 + +**六、支持GPT联网** 本程序实现了OpenAI的Function接口,允许GPT发起联网请求,程序会向GPT自动返回所需的网页数据,使GPT具有联网能力(需先在设置中开启联网选项) @@ -120,8 +131,10 @@ > 注1:上图均为使用`gpt-3.5-turbo`模型的测试结果,建议在提问前加入“百度搜索”、“在线获取”、“从xxx获取”等字样引导GPT,以获得更好的联网效果 - +> > 注2:由于需要将网页内容发送给GPT,联网时会产生大量Token消耗,`gpt-4`模型请谨慎使用 +> +> 注3:`gpt-4-vision-preview`模型暂不支持联网 --- @@ -211,6 +224,10 @@ A: 网页加载超时(15s)、需要登录、需要验证等原因都可能导致 ### 其他问题 +**Q: 为什么列表中没有我需要的模型?** + +A: 软件仅内置了少数常用模型,你可以在设置中添加自定义模型(以英文分号分隔),添加后即会出现在列表中 + **Q: GPT返回的内容中表格和图片无法正常显示?** A: 所使用的Markdown渲染器无法在测试中产生稳定的结果,因此暂不支持表格和图片 @@ -227,12 +244,13 @@ A: 排除网络因素,该错误一般由OpenAI接口产生,可能由于其 - **2023.09.13** 支持连续对话、GPT-4、百度长语音识别,上下文菜单唤起 - **2023.10.06** 添加华为HMS语音识别 - **2023.11.06** 添加联网功能 +- **2023.12.04** 添加Vision识图功能 --- ## TODO -- 对话保存功能、删除部分对话功能 +- 保存对话功能 - 支持渲染Markdown表格、图片等 - 允许GPT控制手机其他功能 @@ -244,13 +262,14 @@ A: 排除网络因素,该错误一般由OpenAI接口产生,可能由于其 | 机型 | 系统版本 | Android 版本 | 本程序版本 | | :--: | :-----: | :----------: | :-------: | -| 荣耀 7C | EMUI 8.0.0 | Android 8 | 1.6.1 | -| 荣耀 20 | HarmonyOS 3.0.0 | Android 10 | 1.6.1 | -| 华为 Mate 30 | HarmonyOS 3.0.0 | Android 12 | 1.4.0 | +| 荣耀 7C | EMUI 8.0.0 | Android 8 | 1.7.0 | +| 荣耀 20 | HarmonyOS 3.0.0 | Android 10 | 1.7.0 | +| 华为 Mate 30 | HarmonyOS 3.0.0 | Android 12 | 1.6.0 | +| 华为 Mate 30 | HarmonyOS 4.0 | Android 12 | 1.7.0 | | 荣耀 Magic 4 | MagicOS 7.0 | Android 13 | 1.2.0 | | 红米 K20 Pro | MIUI 12.5.6 | Android 11 | 1.5.0 | -| 红米 K60 Pro | MIUI 14.0.23 | Android 13 | 1.6.0 | -| Pixel 2 (模拟器) | Android 12 | Android 12 | 1.2.0 | +| 红米 K60 Pro | MIUI 14.0.23 | Android 13 | 1.7.0 | +| Pixel 2 (模拟器) | Android 12 | Android 12 | 1.7.0 | --- @@ -262,7 +281,7 @@ A: 排除网络因素,该错误一般由OpenAI接口产生,可能由于其 ## 隐私说明 -本程序不会以任何方式收集用户的个人信息,语音输入会直接发送给华为或百度API,提问会直接发送给OpenAI API,不会经过任何中间服务器 +本程序不会以任何方式收集用户的个人信息,语音输入会直接发送给华为或百度API,提问会直接发送给OpenAI API,不会经过其他中间服务器 --- diff --git a/app/build.gradle b/app/build.gradle index 52df536..f0f6d55 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,7 +11,7 @@ android { minSdk 26 targetSdk 32 versionCode 1 - versionName "1.6.2" + versionName "1.7.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -57,6 +57,6 @@ dependencies { implementation"io.noties.markwon:linkify:$markwon_version" implementation"io.noties.markwon:syntax-highlight:$markwon_version" annotationProcessor 'io.noties:prism4j-bundler:2.0.0' - implementation group: 'com.unfbx', name: 'chatgpt-java', version: '1.1.0' + implementation group: 'com.unfbx', name: 'chatgpt-java', version: '1.1.3' implementation 'com.huawei.hms:ml-computer-voice-asr:3.12.0.301' } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 72f4069..47b32a6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -70,12 +70,29 @@ - + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/skythinker/gptassistant/ChatApiClient.java b/app/src/main/java/com/skythinker/gptassistant/ChatApiClient.java index 97e30f3..5610ac0 100644 --- a/app/src/main/java/com/skythinker/gptassistant/ChatApiClient.java +++ b/app/src/main/java/com/skythinker/gptassistant/ChatApiClient.java @@ -6,10 +6,15 @@ import androidx.annotation.Nullable; import com.unfbx.chatgpt.OpenAiStreamClient; +import com.unfbx.chatgpt.entity.chat.BaseChatCompletion; +import com.unfbx.chatgpt.entity.chat.ChatCompletionWithPicture; +import com.unfbx.chatgpt.entity.chat.Content; import com.unfbx.chatgpt.entity.chat.FunctionCall; import com.unfbx.chatgpt.entity.chat.Functions; +import com.unfbx.chatgpt.entity.chat.ImageUrl; import com.unfbx.chatgpt.entity.chat.Message; import com.unfbx.chatgpt.entity.chat.ChatCompletion; +import com.unfbx.chatgpt.entity.chat.MessagePicture; import com.unfbx.chatgpt.entity.chat.Parameters; import java.io.IOException; @@ -39,6 +44,28 @@ public enum ChatRole { FUNCTION } + public static class ChatMessage { + public ChatRole role; + public String contentText; + public String contentImageBase64; + public String functionName; + public ChatMessage(ChatRole role) { + this.role = role; + } + public ChatMessage setText(String text) { + this.contentText = text; + return this; + } + public ChatMessage setImage(String base64) { + this.contentImageBase64 = base64; + return this; + } + public ChatMessage setFunction(String name) { + this.functionName = name; + return this; + } + } + String url = ""; String apiKey = ""; String model = ""; @@ -63,19 +90,19 @@ public ChatApiClient(String url, String apiKey, String model, OnReceiveListener setApiInfo(url, apiKey); } - public void sendPrompt(String systemPrompt, String userPrompt) { - if(systemPrompt == null && userPrompt == null) { - listener.onError("模板和问题内容均为空"); - return; - } - - sendPromptList(Arrays.asList( - Pair.create(ChatRole.SYSTEM, systemPrompt), - Pair.create(ChatRole.USER, userPrompt) - )); - } +// public void sendPrompt(String systemPrompt, String userPrompt) { +// if(systemPrompt == null && userPrompt == null) { +// listener.onError("模板和问题内容均为空"); +// return; +// } +// +// sendPromptList(Arrays.asList( +// Pair.create(ChatRole.SYSTEM, systemPrompt), +// Pair.create(ChatRole.USER, userPrompt) +// )); +// } - public void sendPromptList(List> promptList) { + public void sendPromptList(List promptList) { if(url.isEmpty()) { listener.onError("请在设置中填写服务器地址"); return; @@ -87,44 +114,75 @@ public void sendPromptList(List> promptList) { return; } - ArrayList messages = new ArrayList<>(); - for(Pair prompt : promptList) { - if(prompt.first == ChatRole.SYSTEM) { - messages.add(Message.builder().role(Message.Role.SYSTEM).content(prompt.second).build()); - } else if(prompt.first == ChatRole.USER) { - messages.add(Message.builder().role(Message.Role.USER).content(prompt.second).build()); - } else if(prompt.first == ChatRole.ASSISTANT) { - if(prompt.second.startsWith("[Function]")) { - int sepIndex = prompt.second.indexOf("\n"); - String funcName = prompt.second.substring("[Function]".length(), sepIndex); - String arguments = prompt.second.substring(sepIndex + 1); - FunctionCall functionCall = FunctionCall.builder() - .name(funcName) - .arguments(arguments) - .build(); - messages.add(Message.builder().role(Message.Role.ASSISTANT).functionCall(functionCall).build()); - } else { - messages.add(Message.builder().role(Message.Role.ASSISTANT).content(prompt.second).build()); + BaseChatCompletion chatCompletion = null; + + if(!model.contains("vision")) { + ArrayList messageList = new ArrayList<>(); + for (ChatMessage message : promptList) { + if (message.role == ChatRole.SYSTEM) { + messageList.add(Message.builder().role(Message.Role.SYSTEM).content(message.contentText).build()); + } else if (message.role == ChatRole.USER) { + messageList.add(Message.builder().role(Message.Role.USER).content(message.contentText).build()); + } else if (message.role == ChatRole.ASSISTANT) { + if (message.functionName != null) { + FunctionCall functionCall = FunctionCall.builder() + .name(message.functionName) + .arguments(message.contentText) + .build(); + messageList.add(Message.builder().role(Message.Role.ASSISTANT).functionCall(functionCall).build()); + } else { + messageList.add(Message.builder().role(Message.Role.ASSISTANT).content(message.contentText).build()); + } + } else if (message.role == ChatRole.FUNCTION) { + messageList.add(Message.builder().role(Message.Role.FUNCTION).name(message.functionName).content(message.contentText).build()); } - } else if(prompt.first == ChatRole.FUNCTION) { - int sepIndex = prompt.second.indexOf("\n"); - String funcName = prompt.second.substring(0, sepIndex); - String reply = prompt.second.substring(sepIndex + 1); - messages.add(Message.builder().role(Message.Role.FUNCTION).name(funcName).content(reply).build()); } - } - ChatCompletion chatCompletion; - if(!functions.isEmpty()) { - chatCompletion = ChatCompletion.builder() - .messages(messages) - .model(model) - .functions(functions) - .functionCall("auto") - .build(); + if (!functions.isEmpty()) { + chatCompletion = ChatCompletion.builder() + .messages(messageList) + .model(model) + .functions(functions) + .functionCall("auto") + .build(); + } else { + chatCompletion = ChatCompletion.builder() + .messages(messageList) + .model(model) + .build(); + } } else { - chatCompletion = ChatCompletion.builder() - .messages(messages) + ArrayList messageList = new ArrayList<>(); + for (ChatMessage message : promptList) { + List contentList = new ArrayList<>(); + if (message.contentText != null) { + contentList.add(Content.builder().type(Content.Type.TEXT.getName()).text(message.contentText).build()); + } + if(message.contentImageBase64 != null) { + ImageUrl imageUrl = ImageUrl.builder().url("data:image/jpeg;base64," + message.contentImageBase64).build(); + contentList.add(Content.builder().type(Content.Type.IMAGE_URL.getName()).imageUrl(imageUrl).build()); + } + if (message.role == ChatRole.SYSTEM) { + messageList.add(MessagePicture.builder().role(Message.Role.SYSTEM).content(contentList).build()); + } else if (message.role == ChatRole.USER) { + messageList.add(MessagePicture.builder().role(Message.Role.USER).content(contentList).build()); + } else if (message.role == ChatRole.ASSISTANT) { + if (message.functionName != null) { + FunctionCall functionCall = FunctionCall.builder() + .name(message.functionName) + .arguments(message.contentText) + .build(); + messageList.add(MessagePicture.builder().role(Message.Role.ASSISTANT).functionCall(functionCall).build()); + } else { + messageList.add(MessagePicture.builder().role(Message.Role.ASSISTANT).content(contentList).build()); + } + } else if (message.role == ChatRole.FUNCTION) { + messageList.add(MessagePicture.builder().role(Message.Role.FUNCTION).name(message.functionName).content(contentList).build()); + } + } + + chatCompletion = ChatCompletionWithPicture.builder() + .messages(messageList) .model(model) .build(); } diff --git a/app/src/main/java/com/skythinker/gptassistant/GlobalDataHolder.java b/app/src/main/java/com/skythinker/gptassistant/GlobalDataHolder.java index fd2e148..8a8deda 100644 --- a/app/src/main/java/com/skythinker/gptassistant/GlobalDataHolder.java +++ b/app/src/main/java/com/skythinker/gptassistant/GlobalDataHolder.java @@ -11,6 +11,7 @@ import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; public class GlobalDataHolder { @@ -23,6 +24,7 @@ public class GlobalDataHolder { private static String gptApiHost; private static String gptApiKey; private static String gptModel; + private static List customModels = null; private static boolean checkAccessOnStart; private static boolean defaultEnableTts; private static boolean defaultEnableMultiChat; @@ -30,6 +32,7 @@ public class GlobalDataHolder { private static boolean enableInternetAccess; private static int webMaxCharCount; private static boolean onlyLatestWebResult; + private static boolean limitVisionSize; private static SharedPreferences sp = null; public static void init(Context context) { @@ -46,6 +49,7 @@ public static void init(Context context) { loadMultiChatSetting(); loadSelectedTab(); loadFunctionSetting(); + loadVisionSetting(); } public static List getTabDataList() { @@ -109,16 +113,20 @@ public static void loadGptApiInfo() { gptApiHost = sp.getString("gpt_api_host", ""); gptApiKey = sp.getString("gpt_api_key", ""); gptModel = sp.getString("gpt_model", "gpt-3.5-turbo"); + customModels = new ArrayList<>(Arrays.asList(sp.getString("custom_models", "gpt-4-1106-preview;gpt-4-vision-preview").split(";"))); + customModels.removeIf(String::isEmpty); } - public static void saveGptApiInfo(String host, String key, String model) { + public static void saveGptApiInfo(String host, String key, String model, List customModelList) { gptApiHost = host; gptApiKey = key; gptModel = model; + customModels = customModelList; SharedPreferences.Editor editor = sp.edit(); editor.putString("gpt_api_host", gptApiHost); editor.putString("gpt_api_key", gptApiKey); editor.putString("gpt_model", gptModel); + editor.putString("custom_models", String.join(";", customModels)); editor.apply(); } @@ -183,6 +191,17 @@ public static void saveFunctionSetting(boolean enableInternet, int maxCharCount, editor.apply(); } + public static void loadVisionSetting() { + limitVisionSize = sp.getBoolean("limit_vision_size", false); + } + + public static void saveVisionSetting(boolean limitSize) { + limitVisionSize = limitSize; + SharedPreferences.Editor editor = sp.edit(); + editor.putBoolean("limit_vision_size", limitVisionSize); + editor.apply(); + } + public static boolean getAsrUseBaidu() { return asrUseBaidu; } public static String getAsrAppId() { return asrAppId; } @@ -199,6 +218,8 @@ public static void saveFunctionSetting(boolean enableInternet, int maxCharCount, public static String getGptModel() { return gptModel; } + public static List getCustomModels() { return customModels; } + public static boolean getCheckAccessOnStart() { return checkAccessOnStart; } public static boolean getDefaultEnableTts() { return defaultEnableTts; } @@ -212,4 +233,6 @@ public static void saveFunctionSetting(boolean enableInternet, int maxCharCount, public static int getWebMaxCharCount() { return webMaxCharCount; } public static boolean getOnlyLatestWebResult() { return onlyLatestWebResult; } + + public static boolean getLimitVisionSize() { return limitVisionSize; } } diff --git a/app/src/main/java/com/skythinker/gptassistant/MainActivity.java b/app/src/main/java/com/skythinker/gptassistant/MainActivity.java index 128e386..580493e 100644 --- a/app/src/main/java/com/skythinker/gptassistant/MainActivity.java +++ b/app/src/main/java/com/skythinker/gptassistant/MainActivity.java @@ -12,22 +12,30 @@ import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Color; import android.graphics.Typeface; import android.graphics.drawable.PaintDrawable; import android.net.Uri; import android.os.Bundle; import android.os.Handler; +import android.provider.MediaStore; import android.speech.tts.TextToSpeech; +import android.text.Spannable; +import android.text.SpannableString; import android.text.method.LinkMovementMethod; +import android.text.style.ImageSpan; +import android.util.Base64; import android.util.Log; -import android.util.Pair; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.ImageButton; @@ -35,16 +43,24 @@ import android.widget.LinearLayout; import android.widget.PopupWindow; import android.widget.ScrollView; +import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.cardview.widget.CardView; import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileNotFoundException; import java.net.URLDecoder; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Locale; @@ -61,6 +77,7 @@ import io.noties.prism4j.annotations.PrismBundle; import com.skythinker.gptassistant.ChatApiClient.ChatRole; +import com.skythinker.gptassistant.ChatApiClient.ChatMessage; @SuppressLint({"UseCompatLoadingForDrawables", "JavascriptInterface", "SetTextI18n"}) @PrismBundle(includeAll = true) @@ -69,7 +86,8 @@ public class MainActivity extends Activity { private int selectedTab = 0; private TextView tvGptReply; private EditText etUserInput; - private ImageButton btSend; + private ImageButton btSend, btImage; + private Spinner spModels; private ScrollView svChatArea; private LinearLayout llChatList; private Handler handler = new Handler(); @@ -95,13 +113,16 @@ public class MainActivity extends Activity { private int ttsSentenceEndIndex = 0; private boolean multiChat = false; - private List> multiChatList = new ArrayList<>(); + private List multiChatList = new ArrayList<>(); AsrClientBase asrClient = null; AsrClientBase.IAsrCallback asrCallback = null; WebScraper webScraper = null; + Bitmap selectedImageBitmap = null; + Uri photoUri = null; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -143,21 +164,44 @@ protected void onCreate(Bundle savedInstanceState) { tvGptReply.setMovementMethod(LinkMovementMethod.getInstance()); etUserInput = findViewById(R.id.et_user_input); + btSend = findViewById(R.id.bt_send); + btImage = findViewById(R.id.bt_image); + svChatArea = findViewById(R.id.sv_chat_list); + llChatList = findViewById(R.id.ll_chat_list); + Intent activityIntent = getIntent(); if(activityIntent != null){ String action = activityIntent.getAction(); - if(action != null && action.equals("android.intent.action.PROCESS_TEXT")){ + if(Intent.ACTION_PROCESS_TEXT.equals(action)) { String text = activityIntent.getStringExtra(Intent.EXTRA_PROCESS_TEXT); if(text != null){ etUserInput.setText(text); } + } else if(Intent.ACTION_SEND.equals(action)) { + String type = activityIntent.getType(); + if(type != null && type.startsWith("image/")) { + Uri imageUri = activityIntent.getParcelableExtra(Intent.EXTRA_STREAM); + if(imageUri != null) { + try { + Bitmap bitmap = (Bitmap) BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri)); + selectedImageBitmap = bitmap; + if(GlobalDataHolder.getLimitVisionSize()) { + if (bitmap.getWidth() < bitmap.getHeight()) + selectedImageBitmap = resizeBitmap(bitmap, 512, 2048); + else + selectedImageBitmap = resizeBitmap(bitmap, 2048, 512); + } + btImage.setImageResource(R.drawable.image_enabled); + if(!GlobalDataHolder.getGptModel().contains("vision")) + Toast.makeText(this, "请选择支持vision的模型以发送图片", Toast.LENGTH_LONG).show(); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + } } } - btSend = findViewById(R.id.bt_send); - svChatArea = findViewById(R.id.sv_chat_list); - llChatList = findViewById(R.id.ll_chat_list); - if(GlobalDataHolder.getSelectedTab() != -1 && GlobalDataHolder.getSelectedTab() < GlobalDataHolder.getTabDataList().size()) selectedTab = GlobalDataHolder.getSelectedTab(); updateTabListView(); @@ -217,25 +261,24 @@ public void onFinished(boolean completed) { int referenceCount = 0; if(completed) { int questionIndex = multiChatList.size() - 1; - while(questionIndex >= 0 && multiChatList.get(questionIndex).first != ChatRole.USER) { + while(questionIndex >= 0 && multiChatList.get(questionIndex).role != ChatRole.USER) { questionIndex--; } for(int i = questionIndex + 1; i < multiChatList.size(); i++) { - if(multiChatList.get(i).first == ChatRole.FUNCTION - && multiChatList.get(i-1).first == ChatRole.ASSISTANT - && multiChatList.get(i-1).second.startsWith("[Function]")) { - String funcRequest = multiChatList.get(i-1).second; - int sepIndex = funcRequest.indexOf("\n"); - String funcName = funcRequest.substring("[Function]".length(), sepIndex); + if(multiChatList.get(i).role == ChatRole.FUNCTION + && multiChatList.get(i-1).role == ChatRole.ASSISTANT + && multiChatList.get(i-1).functionName != null) { + String funcName = multiChatList.get(i-1).functionName; + String funcArgs = multiChatList.get(i-1).contentText; if(funcName.equals("get_html_text")) { - String url = new JSONObject(funcRequest.substring(sepIndex + 1)).getStr("url"); + String url = new JSONObject(funcArgs).getStr("url"); referenceStr += String.format("[[%s]](%s) ", ++referenceCount, url); } } } } try { - multiChatList.add(new Pair<>(ChatRole.ASSISTANT, chatApiBuffer)); + multiChatList.add(new ChatMessage(ChatRole.ASSISTANT).setText(chatApiBuffer)); ((LinearLayout) tvGptReply.getParent()).setTag(multiChatList.get(multiChatList.size() - 1)); btSend.setImageResource(R.drawable.send_btn); markwon.setMarkdown(tvGptReply, chatApiBuffer); @@ -269,8 +312,7 @@ public void onError(String message) { @Override public void onFunctionCall(String name, String arg) { Log.d("FunctionCall", String.format("%s: %s", name, arg)); - multiChatList.add(new Pair<>(ChatRole.ASSISTANT, - String.format("[Function]%s\n%s", name, arg))); + multiChatList.add(new ChatMessage(ChatRole.ASSISTANT).setFunction(name).setText(arg)); if (name.equals("get_html_text")) { try { JSONObject argJson = new JSONObject(arg); @@ -344,6 +386,51 @@ public void onLoadFail(String message) { } }); + btImage.setOnClickListener(view -> { + if (selectedImageBitmap != null) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + LayoutInflater inflater = LayoutInflater.from(this); + View dialogView = inflater.inflate(R.layout.image_preview_dialog, null); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getWindow().setContentView(dialogView); + ((ImageView) dialogView.findViewById(R.id.iv_image_preview)).setImageBitmap(selectedImageBitmap); + ((TextView) dialogView.findViewById(R.id.tv_image_preview_size)).setText(String.format("%s x %s", selectedImageBitmap.getWidth(), selectedImageBitmap.getHeight())); + dialogView.findViewById(R.id.bt_image_preview_cancel).setOnClickListener(view1 -> dialog.dismiss()); + dialogView.findViewById(R.id.bt_image_preview_del).setOnClickListener(view1 -> { + dialog.dismiss(); + selectedImageBitmap = null; + btImage.setImageResource(R.drawable.image); + }); + dialogView.findViewById(R.id.bt_image_preview_reselect).setOnClickListener(view1 -> { + dialogView.findViewById(R.id.bt_image_preview_del).performClick(); + btImage.performClick(); + }); + } else { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + LayoutInflater inflater = LayoutInflater.from(this); + View dialogView = inflater.inflate(R.layout.image_method_dialog, null); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getWindow().setContentView(dialogView); + dialogView.findViewById(R.id.bt_take_photo).setOnClickListener(view1 -> { + dialog.dismiss(); + photoUri = FileProvider.getUriForFile(MainActivity.this, BuildConfig.APPLICATION_ID + ".provider", new File(getCacheDir(), "photo.jpg")); + Intent intent=new Intent(); + intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); + startActivityForResult(intent, 1); + }); + dialogView.findViewById(R.id.bt_select_from_album).setOnClickListener(view1 -> { + dialog.dismiss(); + Intent intent = new Intent(Intent.ACTION_PICK); + intent.setType("image/*"); + startActivityForResult(intent, 2); + }); + dialogView.findViewById(R.id.bt_image_cancel).setOnClickListener(view1 -> dialog.dismiss()); + } + }); + etUserInput.setOnLongClickListener(view -> { etUserInput.setText(""); return true; @@ -406,6 +493,9 @@ public void onLoadFail(String message) { ((CardView) findViewById(R.id.cv_tts_off)).setForeground(getDrawable(R.drawable.tts_off_enable)); } + updateModelSpinner(); + updateImageButtonVisible(); + isAlive = true; requestPermission(); @@ -520,6 +610,8 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { GlobalDataHolder.saveSelectedTab(selectedTab); updateTabListView(); + updateModelSpinner(); + updateImageButtonVisible(); chatApiClient.setApiInfo(GlobalDataHolder.getGptApiHost(), GlobalDataHolder.getGptApiKey()); chatApiClient.setModel(GlobalDataHolder.getGptModel()); @@ -531,6 +623,21 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { } setFunctions(); + } else if((requestCode == 1 || requestCode == 2) && resultCode == RESULT_OK) { + Uri uri = requestCode == 1 ? photoUri : data.getData(); + try { + Bitmap bitmap = (Bitmap) BitmapFactory.decodeStream(getContentResolver().openInputStream(uri)); + selectedImageBitmap = bitmap; + if(GlobalDataHolder.getLimitVisionSize()) { + if (bitmap.getWidth() < bitmap.getHeight()) + selectedImageBitmap = resizeBitmap(bitmap, 512, 2048); + else + selectedImageBitmap = resizeBitmap(bitmap, 2048, 512); + } + btImage.setImageResource(R.drawable.image_enabled); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } } } @@ -543,6 +650,53 @@ private void scrollChatAreaToBottom() { }); } + private void updateImageButtonVisible() { + if(GlobalDataHolder.getGptModel().contains("vision")) + btImage.setVisibility(View.VISIBLE); + else + btImage.setVisibility(View.GONE); + } + + private void updateModelSpinner() { + Spinner spModels = findViewById(R.id.sp_main_model); + List models = new ArrayList<>(Arrays.asList(getResources().getStringArray(R.array.models))); + models.addAll(GlobalDataHolder.getCustomModels()); + ArrayAdapter modelsAdapter = new ArrayAdapter(this, R.layout.main_model_spinner_item, models) { + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + TextView tv = (TextView) super.getDropDownView(position, convertView, parent); + if(spModels.getSelectedItemPosition() == position) { + tv.setTypeface(Typeface.DEFAULT, Typeface.BOLD); + tv.setBackgroundColor(ContextCompat.getColor(MainActivity.this, R.color.tag_background_unselected)); + } else { + tv.setTypeface(Typeface.DEFAULT, Typeface.NORMAL); + tv.setBackgroundColor(Color.WHITE); + } + return tv; + } + }; + modelsAdapter.setDropDownViewResource(R.layout.model_spinner_dropdown_item); + spModels.setAdapter(modelsAdapter); + spModels.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + public void onItemSelected(AdapterView adapterView, View view, int i, long l) { + GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), GlobalDataHolder.getGptApiKey(), adapterView.getItemAtPosition(i).toString(), GlobalDataHolder.getCustomModels()); + chatApiClient.setModel(GlobalDataHolder.getGptModel()); + updateImageButtonVisible(); + modelsAdapter.notifyDataSetChanged(); + } + public void onNothingSelected(AdapterView adapterView) { } + }); + for(int i = 0; i < modelsAdapter.getCount(); i++) { + if(modelsAdapter.getItem(i).equals(GlobalDataHolder.getGptModel())) { + spModels.setSelection(i); + break; + } + if(i == modelsAdapter.getCount() - 1) { + spModels.setSelection(0); + } + } + } + private void updateTabListView() { LinearLayout tabList = findViewById(R.id.tabs_layout); tabList.removeAllViews(); @@ -574,7 +728,7 @@ private void updateTabListView() { } } - private LinearLayout addChatView(ChatRole role, String content) { + private LinearLayout addChatView(ChatRole role, String content, String imageBase64) { ViewGroup.MarginLayoutParams iconParams = new ViewGroup.MarginLayoutParams(dpToPx(30), dpToPx(30)); iconParams.setMargins(dpToPx(4), dpToPx(12), dpToPx(4), dpToPx(12)); @@ -597,7 +751,18 @@ private LinearLayout addChatView(ChatRole role, String content) { ivIcon.setLayoutParams(iconParams); TextView tvContent = new TextView(this); - tvContent.setText(content); + SpannableString spannableString = null; + if(imageBase64 != null) { + spannableString = new SpannableString(content + "\n "); + Bitmap bitmap = base64ToBitmap(imageBase64); + int maxSize = dpToPx(100); + bitmap = resizeBitmap(bitmap, maxSize, maxSize); + ImageSpan imageSpan = new ImageSpan(this, bitmap); + spannableString.setSpan(imageSpan, content.length()+1, content.length() + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + spannableString = new SpannableString(content); + } + tvContent.setText(spannableString); tvContent.setTextSize(16); tvContent.setTextColor(Color.BLACK); tvContent.setLayoutParams(contentParams); @@ -618,12 +783,12 @@ private LinearLayout addChatView(ChatRole role, String content) { cvDelete.setForeground(getDrawable(R.drawable.clear_btn)); cvDelete.setOnClickListener(view -> { popupWindow.dismiss(); - Pair chat = (Pair) llOuter.getTag(); + ChatMessage chat = (ChatMessage) llOuter.getTag(); if(chat != null) { int index = multiChatList.indexOf(chat); multiChatList.remove(chat); - while(--index > 0 && (multiChatList.get(index).first == ChatRole.FUNCTION - || multiChatList.get(index).second.startsWith("[Function]get_html_text"))) + while(--index > 0 && (multiChatList.get(index).role == ChatRole.FUNCTION + || multiChatList.get(index).functionName != null && multiChatList.get(index).functionName.equals("get_html_text"))) multiChatList.remove(index); } if(tvContent == tvGptReply) { @@ -654,7 +819,18 @@ private LinearLayout addChatView(ChatRole role, String content) { cvEdit.setForeground(getDrawable(R.drawable.edit_btn)); cvEdit.setOnClickListener(view -> { popupWindow.dismiss(); - etUserInput.setText(tvContent.getText().toString()); + ChatMessage chat = (ChatMessage) llOuter.getTag(); + String text = chat.contentText; + if(chat.contentImageBase64 != null) { + if(text.endsWith("\n ")) + text = text.substring(0, text.length() - 2); + selectedImageBitmap = base64ToBitmap(chat.contentImageBase64); + btImage.setImageResource(R.drawable.image_enabled); + } else { + selectedImageBitmap = null; + btImage.setImageResource(R.drawable.image); + } + etUserInput.setText(text); cvDelBelow.performClick(); }); llPopup.addView(cvEdit); @@ -663,7 +839,15 @@ private LinearLayout addChatView(ChatRole role, String content) { cvRetry.setForeground(getDrawable(R.drawable.retry_btn)); cvRetry.setOnClickListener(view -> { popupWindow.dismiss(); - String text = tvContent.getText().toString(); + ChatMessage chat = (ChatMessage) llOuter.getTag(); + String text = chat.contentText; + if(chat.contentImageBase64 != null) { + if(text.endsWith("\n ")) + text = text.substring(0, text.length() - 2); + selectedImageBitmap = base64ToBitmap(chat.contentImageBase64); + } else { + selectedImageBitmap = null; + } cvDelBelow.performClick(); sendQuestion(text); }); @@ -712,9 +896,17 @@ private void sendQuestion(String input){ if(!template.contains("%input%")) template += "%input%"; String question = template.replace("%input%", userInput); - multiChatList.add(new Pair<>(ChatRole.USER, question)); + multiChatList.add(new ChatMessage(ChatRole.USER).setText(question)); }else { - multiChatList.add(new Pair<>(ChatRole.USER, userInput)); + multiChatList.add(new ChatMessage(ChatRole.USER).setText(userInput)); + } + + if(selectedImageBitmap != null) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + selectedImageBitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos); + byte[] bytes = baos.toByteArray(); + String base64 = Base64.encodeToString(bytes, Base64.NO_WRAP); + multiChatList.get(multiChatList.size() - 1).setImage(base64); } if(llChatList.getChildCount() > 0 && llChatList.getChildAt(0) instanceof TextView){ @@ -722,17 +914,17 @@ private void sendQuestion(String input){ } if(multiChat && multiChatList.size() > 0 && llChatList.getChildCount() > 0){ - String firstUserInput = multiChatList.get(0).second; + String firstUserInput = multiChatList.get(0).contentText; ((TextView) ((LinearLayout) llChatList.getChildAt(0)).getChildAt(1)).setText(firstUserInput); } if(GlobalDataHolder.getOnlyLatestWebResult()) { for (int i = 0; i < multiChatList.size(); i++) { - Pair chatItem = multiChatList.get(i); - if (chatItem.first == ChatRole.FUNCTION) { + ChatMessage chatItem = multiChatList.get(i); + if (chatItem.role == ChatRole.FUNCTION) { multiChatList.remove(i); i--; - if(i > 0 && multiChatList.get(i).first == ChatRole.ASSISTANT) { + if(i > 0 && multiChatList.get(i).role == ChatRole.ASSISTANT) { multiChatList.remove(i); i--; } @@ -740,8 +932,8 @@ private void sendQuestion(String input){ } } - LinearLayout llInput = addChatView(ChatRole.USER, multiChat ? multiChatList.get(multiChatList.size() - 1).second : userInput); - LinearLayout llReply = addChatView(ChatRole.ASSISTANT, "正在等待回复..."); + LinearLayout llInput = addChatView(ChatRole.USER, multiChat ? multiChatList.get(multiChatList.size() - 1).contentText : userInput, multiChatList.get(multiChatList.size() - 1).contentImageBase64); + LinearLayout llReply = addChatView(ChatRole.ASSISTANT, "正在等待回复...", null); llInput.setTag(multiChatList.get(multiChatList.size() - 1)); @@ -752,13 +944,15 @@ private void sendQuestion(String input){ chatApiBuffer = ""; ttsSentenceEndIndex = 0; chatApiClient.sendPromptList(multiChatList); + btImage.setImageResource(R.drawable.image); + selectedImageBitmap = null; btSend.setImageResource(R.drawable.cancel_btn); } private void postSendFunctionReply(String funcName, String reply) { handler.post(() -> { Log.d("FunctionCall", "postSendFunctionReply: " + funcName); - multiChatList.add(new Pair<>(ChatRole.FUNCTION, String.format("%s\n%s", funcName, reply))); + multiChatList.add(new ChatMessage(ChatRole.FUNCTION).setFunction(funcName).setText(reply)); chatApiClient.sendPromptList(multiChatList); }); } @@ -767,6 +961,20 @@ private int dpToPx(int dp) { return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()); } + private Bitmap resizeBitmap(Bitmap bitmap, int maxWidth, int maxHeight) { + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + float scale = 1; + if(width > maxWidth || height > maxHeight) + scale = Math.min((float)maxWidth / width, (float)maxHeight / height); + return Bitmap.createScaledBitmap(bitmap, (int)(width * scale), (int)(height * scale), true); + } + + private Bitmap base64ToBitmap(String base64) { + byte[] bytes = Base64.decode(base64, Base64.NO_WRAP); + return BitmapFactory.decodeByteArray(bytes, 0, bytes.length); + } + public static boolean isAlive() { return isAlive; } diff --git a/app/src/main/java/com/skythinker/gptassistant/TabConfActivity.java b/app/src/main/java/com/skythinker/gptassistant/TabConfActivity.java index 635957b..6a19c77 100644 --- a/app/src/main/java/com/skythinker/gptassistant/TabConfActivity.java +++ b/app/src/main/java/com/skythinker/gptassistant/TabConfActivity.java @@ -1,5 +1,8 @@ package com.skythinker.gptassistant; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.recyclerview.widget.ItemTouchHelper; import androidx.recyclerview.widget.LinearLayoutManager; @@ -13,6 +16,7 @@ import android.content.Intent; import android.content.IntentFilter; import android.graphics.Color; +import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; import android.os.Handler; @@ -21,6 +25,7 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; @@ -36,6 +41,7 @@ import org.json.JSONObject; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -116,7 +122,7 @@ public void afterTextChanged(Editable editable) { host += "/"; } } - GlobalDataHolder.saveGptApiInfo(host, GlobalDataHolder.getGptApiKey(), GlobalDataHolder.getGptModel()); + GlobalDataHolder.saveGptApiInfo(host, GlobalDataHolder.getGptApiKey(), GlobalDataHolder.getGptModel(), GlobalDataHolder.getCustomModels()); } }); @@ -125,38 +131,57 @@ public void afterTextChanged(Editable editable) { public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } public void afterTextChanged(Editable editable) { - GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), editable.toString().trim(), GlobalDataHolder.getGptModel()); + GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), editable.toString().trim(), GlobalDataHolder.getGptModel(), GlobalDataHolder.getCustomModels()); } }); - ArrayAdapter modelsAdapter = ArrayAdapter.createFromResource(this, R.array.models, R.layout.model_spinner_item); - modelsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + List models = new ArrayList<>(Arrays.asList(getResources().getStringArray(R.array.models))); + models.addAll(GlobalDataHolder.getCustomModels()); + ArrayAdapter modelsAdapter = new ArrayAdapter(this, R.layout.model_spinner_item, models) { + @Override + public View getDropDownView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + TextView tv = (TextView) super.getDropDownView(position, convertView, parent); + if(((Spinner) findViewById(R.id.sp_model_conf)).getSelectedItemPosition() == position) { + tv.setTypeface(Typeface.DEFAULT, Typeface.BOLD); + tv.setBackgroundColor(ContextCompat.getColor(TabConfActivity.this, R.color.tag_background_unselected)); + } else { + tv.setTypeface(Typeface.DEFAULT, Typeface.NORMAL); + tv.setBackgroundColor(Color.WHITE); + } + return tv; + } + }; + modelsAdapter.setDropDownViewResource(R.layout.model_spinner_dropdown_item); ((Spinner) findViewById(R.id.sp_model_conf)).setAdapter(modelsAdapter); ((Spinner) findViewById(R.id.sp_model_conf)).setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { public void onItemSelected(AdapterView adapterView, View view, int i, long l) { - String model = adapterView.getItemAtPosition(i).toString(); - if(model.equals("自定义")) { - ((LinearLayout) findViewById(R.id.et_custom_model_conf).getParent()).setVisibility(View.VISIBLE); - ((EditText) findViewById(R.id.et_custom_model_conf)).setText(GlobalDataHolder.getGptModel()); - } else { - ((LinearLayout) findViewById(R.id.et_custom_model_conf).getParent()).setVisibility(View.GONE); - GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), GlobalDataHolder.getGptApiKey(), adapterView.getItemAtPosition(i).toString()); - } + GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), GlobalDataHolder.getGptApiKey(), adapterView.getItemAtPosition(i).toString(), GlobalDataHolder.getCustomModels()); + modelsAdapter.notifyDataSetChanged(); } public void onNothingSelected(AdapterView adapterView) { } }); for(int i = 0; i < modelsAdapter.getCount(); i++) { - if(modelsAdapter.getItem(i).toString().equals(GlobalDataHolder.getGptModel()) || i == modelsAdapter.getCount() - 1) { + if(modelsAdapter.getItem(i).equals(GlobalDataHolder.getGptModel())) { ((Spinner) findViewById(R.id.sp_model_conf)).setSelection(i); break; } + if(i == modelsAdapter.getCount() - 1) { + ((Spinner) findViewById(R.id.sp_model_conf)).setSelection(0); + } } + ((EditText) findViewById(R.id.et_custom_model_conf)).setText(String.join(";", GlobalDataHolder.getCustomModels())); ((EditText) findViewById(R.id.et_custom_model_conf)).addTextChangedListener(new TextWatcher() { public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { } public void afterTextChanged(Editable editable) { - GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), GlobalDataHolder.getGptApiKey(), editable.toString().trim()); + List modelList = new ArrayList<>(Arrays.asList(editable.toString().trim().split(";"))); + modelList.removeIf(String::isEmpty); + GlobalDataHolder.saveGptApiInfo(GlobalDataHolder.getGptApiHost(), GlobalDataHolder.getGptApiKey(), GlobalDataHolder.getGptModel(), modelList); + models.clear(); + models.addAll(Arrays.asList(getResources().getStringArray(R.array.models))); + models.addAll(modelList); + modelsAdapter.notifyDataSetChanged(); } }); @@ -223,6 +248,11 @@ public void afterTextChanged(Editable editable) { } }); + ((Switch) findViewById(R.id.sw_limit_vision_size_conf)).setChecked(GlobalDataHolder.getLimitVisionSize()); + ((Switch) findViewById(R.id.sw_limit_vision_size_conf)).setOnCheckedChangeListener((compoundButton, checked) -> { + GlobalDataHolder.saveVisionSetting(checked); + }); + (findViewById(R.id.tv_set_tts_conf)).setOnClickListener(view -> { Intent intent = new Intent("com.android.settings.TTS_SETTINGS"); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); diff --git a/app/src/main/res/drawable/image.png b/app/src/main/res/drawable/image.png new file mode 100644 index 0000000000000000000000000000000000000000..a2328157591b85673b44dfb71777be44645a5066 GIT binary patch literal 4827 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/image_enabled.png b/app/src/main/res/drawable/image_enabled.png new file mode 100644 index 0000000000000000000000000000000000000000..30b03c3b7ed594b246874f43fb1cede774889930 GIT binary patch literal 4542 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 W{J<5faQjO`24HyCR1dA|68Aq>9ZY@z literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 8ab7d23..685d8f1 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -32,12 +32,34 @@ android:layout_marginRight="10dp" android:layout_marginBottom="10dp" android:gravity="right" - android:orientation="horizontal"> + android:orientation="horizontal" + android:paddingLeft="10dp"> + + + + + + app:contentPadding="0dp"> - + android:orientation="horizontal" + android:paddingHorizontal="5dp"> + + + + + + + + android:textAlignment="textEnd" + tools:ignore="RtlCompat" /> - + android:layout_marginRight="20dp" + android:orientation="vertical"> + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/image_method_dialog.xml b/app/src/main/res/layout/image_method_dialog.xml new file mode 100644 index 0000000..6290120 --- /dev/null +++ b/app/src/main/res/layout/image_method_dialog.xml @@ -0,0 +1,55 @@ + + + + + + + +