From 003ab9184049bc210d44b750b0659d359729ddf1 Mon Sep 17 00:00:00 2001 From: white-yj8109 <19310195525@163.com> Date: Wed, 21 Jan 2026 09:07:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=EF=BC=9A=E7=BF=BB=E8=AF=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/tool/TranslateUtils.java | 162 ++++++++++++++++++++++++++++ src/ui/NoteEditActivity.java | 200 ++++++++++++++++++++++++++++++++++- 2 files changed, 361 insertions(+), 1 deletion(-) create mode 100644 src/tool/TranslateUtils.java diff --git a/src/tool/TranslateUtils.java b/src/tool/TranslateUtils.java new file mode 100644 index 0000000..6d74a64 --- /dev/null +++ b/src/tool/TranslateUtils.java @@ -0,0 +1,162 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.os.Build; +import android.text.TextUtils; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.MessageDigest; +import java.util.Locale; + +public class TranslateUtils { + private static final String TAG = "TranslateUtils"; + + + private static final String YOUDAO_APP_KEY = "3abfa533dbdc44d1"; + private static final String YOUDAO_APP_SECRET = "aliNHKWhhTlaLjRAkOce4cHTubriEl0c"; + private static final String YOUDAO_URL = "https://openapi.youdao.com/api"; + + public static boolean isOnline(Context ctx) { + ConnectivityManager cm = (ConnectivityManager) ctx.getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm == null) return false; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + Network network = cm.getActiveNetwork(); + if (network == null) return false; + + NetworkCapabilities capabilities = cm.getNetworkCapabilities(network); + return capabilities != null && (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) + || capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)); + } else { + // 兼容低版本 + NetworkInfo ni = cm.getActiveNetworkInfo(); + return ni != null && ni.isConnected(); + } + } + + public static String translateParagraph(String text, String targetLang) { + if (TextUtils.isEmpty(YOUDAO_APP_KEY) || TextUtils.isEmpty(YOUDAO_APP_SECRET)) { + Log.w(TAG, "Youdao app key/secret not configured"); + return null; + } + try { + Log.d(TAG, "Starting translation: text=" + text + ", targetLang=" + targetLang); + + String q = text; + String from = "auto"; + String to = targetLang == null ? "en" : targetLang; + String salt = String.valueOf(System.currentTimeMillis()); + String sign = md5(YOUDAO_APP_KEY + q + salt + YOUDAO_APP_SECRET); + + StringBuilder sb = new StringBuilder(); + sb.append("appKey=").append(urlEncode(YOUDAO_APP_KEY)); + sb.append("&q=").append(urlEncode(q)); + sb.append("&salt=").append(urlEncode(salt)); + sb.append("&from=").append(urlEncode(from)); + sb.append("&to=").append(urlEncode(to)); + sb.append("&sign=").append(urlEncode(sign)); + + URL url = new URL(YOUDAO_URL); + Log.d(TAG, "Connecting to: " + YOUDAO_URL); + + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + conn.setRequestMethod("POST"); + conn.setConnectTimeout(10000); + conn.setReadTimeout(10000); + conn.setDoOutput(true); + conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); + + OutputStreamWriter writer = new OutputStreamWriter(conn.getOutputStream(), "UTF-8"); + writer.write(sb.toString()); + writer.flush(); + writer.close(); + + int code = conn.getResponseCode(); + Log.d(TAG, "Response code: " + code); + + if (code != 200) { + Log.w(TAG, "Youdao response code:" + code); + // 读取错误响应 + try (BufferedReader br = new BufferedReader(new InputStreamReader(conn.getErrorStream(), "UTF-8"))) { + StringBuilder errorResp = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + errorResp.append(line); + } + Log.w(TAG, "Error response: " + errorResp.toString()); + } catch (Exception e) { + Log.w(TAG, "Failed to read error response", e); + } + return null; + } + + BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream(), "UTF-8")); + StringBuilder resp = new StringBuilder(); + String line; + while ((line = br.readLine()) != null) { + resp.append(line); + } + br.close(); + conn.disconnect(); + + Log.d(TAG, "Response: " + resp.toString()); + + JSONObject json = new JSONObject(resp.toString()); + if (json.has("translation")) { + JSONArray arr = json.getJSONArray("translation"); + if (arr.length() > 0) { + String result = arr.getString(0); + Log.d(TAG, "Translation result: " + result); + return result; + } + } + // fallback: try webdict or basic + if (json.has("web") && json.getJSONArray("web").length() > 0) { + JSONObject w = json.getJSONArray("web").getJSONObject(0); + if (w.has("value")) { + JSONArray v = w.getJSONArray("value"); + if (v.length() > 0) { + String result = v.getString(0); + Log.d(TAG, "Web dict fallback result: " + result); + return result; + } + } + } + Log.w(TAG, "No translation found in response"); + return null; + } catch (Exception e) { + Log.w(TAG, "translate error: " + e.getMessage(), e); + return null; + } + } + + private static String urlEncode(String s) throws Exception { + return java.net.URLEncoder.encode(s, "UTF-8"); + } + + private static String md5(String s) throws Exception { + MessageDigest md = MessageDigest.getInstance("MD5"); + byte[] bytes = md.digest(s.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte b : bytes) { + String hex = Integer.toHexString(b & 0xff); + if (hex.length() == 1) sb.append('0'); + sb.append(hex); + } + return sb.toString(); + } +} diff --git a/src/ui/NoteEditActivity.java b/src/ui/NoteEditActivity.java index 6119667..0c5039e 100644 --- a/src/ui/NoteEditActivity.java +++ b/src/ui/NoteEditActivity.java @@ -44,6 +44,11 @@ import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; +import android.app.ProgressDialog; +import android.os.AsyncTask; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.Spinner; import android.view.WindowManager; import android.widget.CheckBox; import android.widget.CompoundButton; @@ -62,6 +67,7 @@ import net.micode.notes.model.WorkingNote.NoteSettingChangedListener; import net.micode.notes.tool.DataUtils; import net.micode.notes.tool.ResourceParser; import net.micode.notes.tool.ResourceParser.TextAppearanceResources; +import net.micode.notes.tool.TranslateUtils; import net.micode.notes.ui.DateTimePickerDialog.OnDateTimeSetListener; import net.micode.notes.ui.NoteEditText.OnTextViewChangeListener; import net.micode.notes.widget.NoteWidgetProvider_2x; @@ -149,6 +155,12 @@ public class NoteEditActivity extends Activity implements OnClickListener, private LinearLayout mEditTextList; + // translation state + private boolean mHasTranslation = false; + private String mOriginalContent = null; + private ProgressDialog mProgressDialog; + private String mTargetLangCode = "en"; // default + private String mUserQuery; private Pattern mPattern; //新增字符统计方法,排除空白字符 @@ -504,7 +516,32 @@ public class NoteEditActivity extends Activity implements OnClickListener, if(clearSettingState()) { return; } - + if (mHasTranslation) { + AlertDialog.Builder b = new AlertDialog.Builder(this); + b.setTitle(R.string.translate_confirm_keep_title); + b.setPositiveButton(R.string.translate_keep, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + saveNote(); + finish(); + } + }); + b.setNeutralButton(R.string.translate_discard, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + if (mOriginalContent != null) { + mNoteEditor.setText(mOriginalContent); + mHasTranslation = false; + } + saveNote(); + finish(); + } + }); + b.setNegativeButton(R.string.translate_cancel_action, null); + b.show(); + return; + } + saveNote();//保存笔记 super.onBackPressed(); } @@ -585,6 +622,9 @@ public class NoteEditActivity extends Activity implements OnClickListener, getWorkingText(); sendTo(this, mWorkingNote.getContent()); break; + case R.id.menu_translate: + showTranslateDialog(); + break; case R.id.menu_send_to_desktop://发送到桌面 sendToDesktop(); break; @@ -621,6 +661,164 @@ public class NoteEditActivity extends Activity implements OnClickListener, context.startActivity(intent); } + + private void showTranslateDialog() { + View v = LayoutInflater.from(this).inflate(R.layout.translate_dialog, null); + final Spinner spinner = (Spinner) v.findViewById(R.id.spinner_target_lang); + final String[] langNames = new String[] {"英语", "中文", "日语", "韩语", "法语", "德语", "西班牙语"}; + final String[] langCodes = new String[] {"en", "zh-CHS", "ja", "ko", "fr", "de", "es"}; + ArrayAdapter adapter = new ArrayAdapter(this, + android.R.layout.simple_spinner_item, langNames); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + + AlertDialog.Builder b = new AlertDialog.Builder(this); + b.setTitle(R.string.translate_dialog_title); + b.setView(v); + final AlertDialog d = b.create(); + + Button btnCancel = (Button) v.findViewById(R.id.btn_cancel_translate); + Button btnConfirm = (Button) v.findViewById(R.id.btn_confirm_translate); + btnCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + d.dismiss(); + } + }); + btnConfirm.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View view) { + int pos = spinner.getSelectedItemPosition(); + String code = "en"; + switch (pos) { + case 1: code = "zh-CHS"; break; + case 2: code = "ja"; break; + case 3: code = "ko"; break; + case 4: code = "fr"; break; + case 5: code = "de"; break; + case 6: code = "es"; break; + default: code = "en"; break; + } + d.dismiss(); + startTranslate(code); + } + }); + d.show(); + } + + private void startTranslate(String targetLang) { + if (!TranslateUtils.isOnline(this)) { + showToast(R.string.translate_offline_hint); + return; + } + // backup original content + if (!mHasTranslation) { + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 清单模式下获取文本内容 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + mOriginalContent = sb.toString(); + } else { + // 普通模式下获取文本内容 + mOriginalContent = mNoteEditor.getText().toString(); + } + } + mTargetLangCode = targetLang; + mProgressDialog = ProgressDialog.show(this, "", getString(R.string.translate_progress), true, false); + + // 根据当前模式获取文本内容 + String content; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mEditTextList.getChildCount(); i++) { + View view = mEditTextList.getChildAt(i); + NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text); + if (!TextUtils.isEmpty(edit.getText())) { + if (((CheckBox) view.findViewById(R.id.cb_edit_item)).isChecked()) { + sb.append(TAG_CHECKED).append(" ").append(edit.getText()).append("\n"); + } else { + sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n"); + } + } + } + content = sb.toString(); + } else { + content = mNoteEditor.getText().toString(); + } + + new TranslateTask().execute(content); + } + + private class TranslateTask extends AsyncTask { + @Override + protected String doInBackground(String... params) { + String content = params[0] == null ? "" : params[0]; + String[] paragraphs = content.split("\\n"); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < paragraphs.length; i++) { + String p = paragraphs[i]; + sb.append(p); + sb.append('\n'); + if (!p.trim().isEmpty()) { + String t = TranslateUtils.translateParagraph(p, mTargetLangCode); + if (t != null) { + sb.append(t); + sb.append('\n'); + } else { + sb.append("[翻译失败]"); + sb.append('\n'); + } + } + } + return sb.toString(); + } + + @Override + protected void onPostExecute(String result) { + if (mProgressDialog != null && mProgressDialog.isShowing()) { + mProgressDialog.dismiss(); + } + if (result != null) { + mHasTranslation = true; + if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) { + // 清单模式下,将翻译结果转换为清单格式 + mEditTextList.removeAllViews(); + String[] items = result.split("\n"); + int index = 0; + for (String item : items) { + if(!TextUtils.isEmpty(item)) { + mEditTextList.addView(getListItem(item, index)); + index++; + } + } + mEditTextList.addView(getListItem("", index)); + // 修改 + View focused = mEditTextList.getChildAt(index); + focused.findViewById(R.id.et_edit_text).requestFocus(); + } else { + // 普通模式下,直接显示翻译结果 + mNoteEditor.setText(result); + // scroll to first translation: find first occurrence of "[翻译失败]" or second line + int idx = result.indexOf('\n'); + if (idx >= 0 && idx + 1 < result.length()) { + mNoteEditor.setSelection(idx + 1); + } + } + } else { + showToast(R.string.error_sync_network); + } + } + } private void createNewNote() { // Firstly, save current editing notes saveNote(); -- 2.34.1 From f982882f214bfafd1bbda3e9d5a89f278acf2541 Mon Sep 17 00:00:00 2001 From: white-yj8109 <19310195525@163.com> Date: Wed, 21 Jan 2026 09:25:08 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BF=BB=E8=AF=91?= =?UTF-8?q?=E6=96=87=E6=9C=AC=E5=AD=97=E4=BD=93=E9=A2=9C=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/NoteEditActivity.java | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/src/ui/NoteEditActivity.java b/src/ui/NoteEditActivity.java index 0c5039e..2a09ae7 100644 --- a/src/ui/NoteEditActivity.java +++ b/src/ui/NoteEditActivity.java @@ -806,12 +806,32 @@ public class NoteEditActivity extends Activity implements OnClickListener, View focused = mEditTextList.getChildAt(index); focused.findViewById(R.id.et_edit_text).requestFocus(); } else { - // 普通模式下,直接显示翻译结果 - mNoteEditor.setText(result); - // scroll to first translation: find first occurrence of "[翻译失败]" or second line - int idx = result.indexOf('\n'); - if (idx >= 0 && idx + 1 < result.length()) { - mNoteEditor.setSelection(idx + 1); + // 普通模式下,直接显示翻译结果 + SpannableString spannable = new SpannableString(result); + // 分析文本结构:原文和翻译交替出现 + // 格式:原文1\n翻译1\n原文2\n翻译2\n... + String[] lines = result.split("\n"); + int currentPosition = 0; + boolean isTranslation = false; + + for (String line : lines) { + if (!TextUtils.isEmpty(line)) { + if (isTranslation) { + // 设置翻译结果为浅灰色 + int start = currentPosition; + int end = currentPosition + line.length(); + spannable.setSpan(new ForegroundColorSpan(Color.parseColor("#999999")), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + isTranslation = !isTranslation; + } + currentPosition += line.length() + 1; // +1 for newline + } + + mNoteEditor.setText(spannable); + // scroll to first translation + int firstTranslationIdx = result.indexOf('\n'); + if (firstTranslationIdx >= 0 && firstTranslationIdx + 1 < result.length()) { + mNoteEditor.setSelection(firstTranslationIdx + 1); } } } else { -- 2.34.1