新增功能:翻译 #26

Merged
p6vxzahlf merged 2 commits from wangyijia_branch into master 1 month ago

@ -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();
}
}

@ -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,184 @@ 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<String> adapter = new ArrayAdapter<String>(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<String, Integer, String> {
@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 {
// 普通模式下,直接显示翻译结果
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 {
showToast(R.string.error_sync_network);
}
}
}
private void createNewNote() {
// Firstly, save current editing notes
saveNote();

Loading…
Cancel
Save