();
-
+ private View mToolbar;
static {
// 初始化背景选择按钮映射
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
@@ -185,7 +188,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private View mFontSizeSelector; // 字号选择器
- private EditText mNoteEditor; // 笔记编辑器
+ private RichEditText mNoteEditor; // 笔记编辑器
private View mNoteEditorPanel; // 笔记编辑器面板
@@ -235,10 +238,19 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return;
}
+ // 获取便签ID
+ long noteId = getIntent().getLongExtra("note_id", 0);
+
+ // 检查密码
+ checkNotePassword(noteId);
initResources();
//根据id获取添加图片按钮
final ImageButton add_img_btn = (ImageButton) findViewById(R.id.add_img_btn);
+ final ImageButton deleteImageButton = (ImageButton) findViewById(R.id.btn_delete_image);
+ deleteImageButton.setOnClickListener(v -> {
+ deleteImageFromNote(); // 调用删除图片方法
+ });
//为点击图片按钮设置监听器
add_img_btn.setOnClickListener(new View.OnClickListener() {
@Override
@@ -300,6 +312,51 @@ public class NoteEditActivity extends Activity implements OnClickListener,
startSpeechToText();
}
});
+ // 初始化富文本编辑器
+ mNoteEditor = (RichEditText) findViewById(R.id.note_edit_view);
+ mToolbar = findViewById(R.id.toolbar); // 工具栏布局
+ // 工具栏始终可见
+ mToolbar.setVisibility(View.VISIBLE);
+ // 设置光标监听
+ setupCursorListener();
+ // 初始化工具栏按钮
+ initFormatButtons();
+ }
+ private void initFormatButtons() {
+ // 加粗按钮
+ findViewById(R.id.btn_bold).setOnClickListener(v -> {
+ mNoteEditor.toggleBold();
+ });
+
+ // 斜体按钮
+ findViewById(R.id.btn_italic).setOnClickListener(v -> {
+ mNoteEditor.toggleItalic();
+ });
+
+ // 下划线按钮
+ findViewById(R.id.btn_underline).setOnClickListener(v -> {
+ mNoteEditor.toggleUnderline();
+ });
+
+ // 删除线按钮
+ findViewById(R.id.btn_strikethrough).setOnClickListener(v -> {
+ mNoteEditor.toggleStrikethrough();
+ });
+
+ // 项目符号按钮
+ findViewById(R.id.btn_bullet).setOnClickListener(v -> {
+ mNoteEditor.toggleBullet();
+ });
+
+ // 文字颜色按钮
+ findViewById(R.id.btn_text_color).setOnClickListener(v -> {
+ showColorPicker();
+ });
+ }
+ private void showColorPicker() {
+ new ColorPickerDialog(this, color -> {
+ mNoteEditor.setTextColor(color);
+ }).show();
}
private void copyAssetsFolder(String assetDir, File outDir) throws IOException {
@@ -445,6 +502,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
finish();
return false;
}
+
// 设置笔记状态改变的监听器
mWorkingNote.setOnSettingStatusChangedListener(this);
return true;
@@ -458,32 +516,35 @@ public class NoteEditActivity extends Activity implements OnClickListener,
@Override
protected void onResume() {
super.onResume();
-
+ // 避免重复验证
+ if (!isPatternVerified()) {
+ checkNotePassword(mWorkingNote.getNoteId());
+ }
+ // 加载富文本内容
+ String htmlContent = mWorkingNote.getRichTextContent(); // 从 WorkingNote 加载 HTML 内容
+ if (!TextUtils.isEmpty(htmlContent)) {
+ mNoteEditor.fromHtml(htmlContent); // 加载到编辑器
+ }
// 初始化笔记界面
initNoteScreen();
// 获取图片路径并插入图片
String imagePath = mWorkingNote.getImagePathFromDatabase();
String audioPath = mWorkingNote.getAudioPathFromDatabase();
- if (imagePath != null) {
- // 从文件加载图像
- Bitmap bitmap = mWorkingNote.loadImageFromFile(imagePath);
- if (bitmap != null) {
- // 创建一个 ImageSpan
- ImageSpan imageSpan = new ImageSpan(this, bitmap);
- SpannableString spannableString = new SpannableString(" "); // 使用占位符
- spannableString.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- // 找到笔记编辑器并插入图片
- NoteEditText noteEditText = (NoteEditText) findViewById(R.id.note_edit_view);
- int index = noteEditText.getSelectionStart(); // 获取光标位置
- noteEditText.getEditableText().insert(index, spannableString);
- } else {
- Log.e(TAG, "Failed to load image from file: " + imagePath);
+ if (!TextUtils.isEmpty(imagePath)) {
+ File imageFile = new File(imagePath);
+ if (imageFile.exists()) {
+ Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
+ if (bitmap != null) {
+ ImageSpan imageSpan = new ImageSpan(this, bitmap);
+ SpannableString spannableString = new SpannableString(" ");
+ spannableString.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ mNoteEditor.getEditableText().insert(mNoteEditor.getSelectionStart(), spannableString);
+ }
}
- }
- else {
- Log.e(TAG, "Image path is null. Can't load image.");
+ } else {
+ Log.d(TAG, "新建便签没有图片,跳过加载");
}
}
private MediaPlayer mediaPlayer;
@@ -509,7 +570,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
Toast.makeText(this, "Unable to play audio", Toast.LENGTH_SHORT).show();
}
}
+ private boolean isPatternVerified = false;
+ private boolean isPatternVerified() {
+ return isPatternVerified;
+ }
/**
* 初始化笔记界面的函数。
* 该函数设置笔记编辑器的外观,根据笔记类型切换到相应的模式,设置背景和头部信息,
@@ -663,7 +728,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
// 初始化编辑器和相关组件
- mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
+ mNoteEditor = (RichEditText) findViewById(R.id.note_edit_view);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
// 设置背景选择器按钮点击监听器
@@ -696,6 +761,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
@Override
protected void onPause() {
super.onPause();
+ String htmlContent = mNoteEditor.toHtml(); // 将富文本内容转换为 HTML
+ mWorkingNote.setRichTextContent(htmlContent); // 保存到 WorkingNote
// 保存笔记数据
if (saveNote()) {
Log.d(TAG, "Note data was saved with length:" + mWorkingNote.getContent().length());
@@ -904,6 +971,11 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 删除提醒设置
mWorkingNote.setAlertDate(0, false);
break;
+ case R.id.menu_set_gesture:
+ Intent intent = new Intent(this, NoteGesturePasswordActivity.class);
+ intent.putExtra("note_id", mWorkingNote.getNoteId());
+ startActivityForResult(intent, REQUEST_SET_PATTERN);
+ break;
default:
break;
}
@@ -1193,6 +1265,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
* @param index 列表项索引。
* @param hasText 列表项是否包含文本。
*/
+ private final Handler mHandler = new Handler();
+ private final Runnable updateToolbarRunnable = this::updateToolbarPosition;
public void onTextChange(int index, boolean hasText) {
if (index >= mEditTextList.getChildCount()) {
Log.e(TAG, "Wrong index, should not happen");
@@ -1204,6 +1278,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
}
+ mHandler.removeCallbacks(updateToolbarRunnable);
+ mHandler.postDelayed(updateToolbarRunnable, 100); // 延迟 100ms 更新工具栏位置
}
/**
@@ -1234,29 +1310,41 @@ public class NoteEditActivity extends Activity implements OnClickListener,
* @return 是否有已选中的列表项。
*/
private boolean getWorkingText() {
- boolean hasChecked = false;
- 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");
- hasChecked = true;
- } else {
- sb.append(TAG_UNCHECKED).append(" ").append(edit.getText()).append("\n");
- }
- }
+ if (mNoteEditor.getVisibility() == View.VISIBLE) {
+ // 获取编辑器中的 HTML 格式内容
+ String htmlContent = mNoteEditor.toHtml();
+
+ if (htmlContent != null) {
+ // 设置到 WorkingNote 中
+ mWorkingNote.setRichTextContent(htmlContent);
+ return true; // 成功获取内容
+ } else {
+ return false; // 获取内容失败
}
- mWorkingNote.setWorkingText(sb.toString());
} else {
- mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ StringBuilder content = new StringBuilder();
+ int count = mEditTextList.getChildCount();
+
+ for (int i = 0; i < count; i++) {
+ NoteEditText edit = (NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text);
+ content.append(edit.getText());
+
+ if (i != count - 1) {
+ content.append("\n");
+ }
+ }
+
+ // 设置文本内容到 WorkingNote
+ if (content.length() > 0) {
+ mWorkingNote.setWorkingText(content.toString());
+ return true; // 成功获取内容
+ } else {
+ return false; // 内容为空或失败
+ }
}
- return hasChecked;
}
+
/**
* 保存笔记。
* 更新笔记内容并保存。
@@ -1265,6 +1353,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*/
private boolean saveNote() {
getWorkingText();
+
boolean saved = mWorkingNote.saveNote();
if (saved) {
// 设置结果为成功,以便外部调用者知道保存操作的状态
@@ -1485,6 +1574,27 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
Log.e(TAG, "Activity result error, requestCode: " + requestCode + ", resultCode: " + resultCode);
}
+ if (requestCode == REQUEST_VERIFY_PATTERN) {
+ // 不要将 RESULT_OK (-1) 视为错误
+ if (resultCode == RESULT_OK) {
+ isPatternVerified = true;
+ Log.d(TAG, "Pattern verification successful");
+ // 继续正常操作
+ return;
+ } else {
+ Log.d(TAG, "Pattern verification failed");
+ finish();
+ return;
+ }
+ }
+
+ // 处理其他 Activity 结果
+ try {
+ super.onActivityResult(requestCode, resultCode, intent);
+ } catch (Exception e) {
+ Log.e(TAG, "Error in super.onActivityResult", e);
+ }
+
}
@@ -1509,10 +1619,34 @@ public class NoteEditActivity extends Activity implements OnClickListener,
spannableString.setSpan(imageSpan, 0, 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// 将图片插入到EditText中
- NoteEditText noteEditText = (NoteEditText) findViewById(R.id.note_edit_view);
+ RichEditText noteEditText = (RichEditText) findViewById(R.id.note_edit_view);
int index = noteEditText.getSelectionStart(); // 获取光标所在位置
noteEditText.getEditableText().insert(index, spannableString);
}
+ private void deleteImageFromNote() {
+ // 从编辑器中移除图片
+ Editable editable = mNoteEditor.getEditableText();
+ ImageSpan[] imageSpans = editable.getSpans(0, editable.length(), ImageSpan.class);
+
+ for (ImageSpan imageSpan : imageSpans) {
+ int start = editable.getSpanStart(imageSpan);
+ int end = editable.getSpanEnd(imageSpan);
+ editable.removeSpan(imageSpan); // 删除图片的 ImageSpan
+ editable.delete(start, end); // 删除图片占位符
+ }
+
+ // 清空数据库中的图片路径
+ mWorkingNote.deleteImagePathFromDatabase();
+
+ // 更新富文本内容到数据库
+ String htmlContent = mNoteEditor.toHtml();
+ mWorkingNote.setRichTextContent(htmlContent);
+ mWorkingNote.saveNote();
+
+ // 提示用户删除成功
+ Toast.makeText(this, "图片已删除", Toast.LENGTH_SHORT).show();
+ }
+
private MediaRecorder mediaRecorder;
@@ -1547,7 +1681,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
String audioPath = audioFilePath;
// 插入到光标位置
- NoteEditText noteEditText = (NoteEditText) findViewById(R.id.note_edit_view);
+ RichEditText noteEditText = (RichEditText) findViewById(R.id.note_edit_view);
int cursorPosition = noteEditText.getSelectionStart(); // 获取光标位置
SpannableString spannableString = new SpannableString("[Audio]");
spannableString.setSpan(new ImageSpan(this, R.drawable.ic_audio), 0, 7, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
@@ -1639,7 +1773,81 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
}
+ private void checkNotePassword(long noteId) {
+ // 如果是新建便签,不需要验证密码
+ if (noteId == 0 || noteId == -1) {
+ return;
+ }
+
+ // 查询便签是否设置了密码
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
+ new String[] { Notes.NoteColumns.GESTURE_PASSWORD },
+ null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ String password = cursor.getString(0);
+ // 只有设置了密码的便签才需要验证
+ if (password != null && !password.isEmpty()) {
+ Intent intent = new Intent(this, NoteGestureVerifyActivity.class);
+ intent.putExtra("note_id", noteId);
+ startActivityForResult(intent, REQUEST_VERIFY_PATTERN);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error checking note password", e);
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ private void setupCursorListener() {
+ mNoteEditor.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ updateToolbarPosition();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+ });
+
+ mNoteEditor.setOnFocusChangeListener((v, hasFocus) -> {
+ if (hasFocus) {
+ updateToolbarPosition();
+ }
+ });
+ }
+
+ private void updateToolbarPosition() {
+ int[] cursorCoordinates = mNoteEditor.getCursorCoordinates();
+
+ // 获取工具栏的宽度和屏幕宽度
+ int screenWidth = getWindow().getDecorView().getWidth();
+ int toolbarWidth = mToolbar.getWidth();
+
+ // 工具栏的 X 坐标
+ int toolbarX = cursorCoordinates[0] - toolbarWidth / 2;
+ toolbarX = Math.max(0, Math.min(toolbarX, screenWidth - toolbarWidth)); // 确保工具栏在屏幕范围内
+
+ // 工具栏的 Y 坐标(光标下方偏移量 16dp)
+ int toolbarY = cursorCoordinates[1] + 16;
+
+ // 设置工具栏位置
+ mToolbar.setX(toolbarX);
+ mToolbar.setY(toolbarY);
+ }
+
}
+
+
+
diff --git a/Src/app/src/main/java/net/micode/notes/ui/NoteGesturePasswordActivity.java b/Src/app/src/main/java/net/micode/notes/ui/NoteGesturePasswordActivity.java
new file mode 100644
index 0000000..6fedba5
--- /dev/null
+++ b/Src/app/src/main/java/net/micode/notes/ui/NoteGesturePasswordActivity.java
@@ -0,0 +1,98 @@
+package net.micode.notes.ui;
+
+import android.app.Activity;
+import android.content.ContentUris;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+
+public class NoteGesturePasswordActivity extends Activity {
+ private LockPatternView lockPatternView;
+ private TextView gestureStatus;
+ private Button btnConfirm;
+ private String tempPattern;
+ private long noteId;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.note_gesture_password);
+
+ noteId = getIntent().getLongExtra("note_id", -1);
+ if (noteId == -1) {
+ finish();
+ return;
+ }
+
+ initViews();
+ }
+
+ private void initViews() {
+ lockPatternView = (LockPatternView) findViewById(R.id.lock_pattern_view);
+ gestureStatus = (TextView) findViewById(R.id.gesture_status);
+ btnConfirm = (Button) findViewById(R.id.btn_confirm);
+ btnConfirm.setEnabled(false);
+
+ // 修改这部分代码,使用新的接口
+ lockPatternView.setPatternListener(new LockPatternView.OnPatternListener() {
+ @Override
+ public void onPatternComplete(String pattern) {
+ if (pattern.length() < 4) {
+ gestureStatus.setText(R.string.gesture_too_short);
+ return;
+ }
+
+ if (tempPattern == null) {
+ tempPattern = pattern;
+ gestureStatus.setText(R.string.confirm_gesture_pattern);
+ btnConfirm.setEnabled(false);
+ } else {
+ if (tempPattern.equals(pattern)) {
+ btnConfirm.setEnabled(true);
+ gestureStatus.setText(R.string.gesture_matched);
+ } else {
+ tempPattern = null;
+ gestureStatus.setText(R.string.gesture_not_matched);
+ btnConfirm.setEnabled(false);
+ }
+ }
+ }
+ });
+
+ btnConfirm.setOnClickListener(v -> {
+ if (tempPattern != null) {
+ saveGesturePassword(tempPattern);
+ Toast.makeText(this, R.string.gesture_password_set, Toast.LENGTH_SHORT).show();
+ setResult(RESULT_OK);
+ finish();
+ }
+ });
+ }
+
+ private void saveGesturePassword(String pattern) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.GESTURE_PASSWORD, pattern);
+ getContentResolver().update(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
+ values, null, null);
+
+ // 添加返回密码到结果Intent
+ Intent resultIntent = new Intent();
+ resultIntent.putExtra("pattern", pattern);
+ setResult(RESULT_OK, resultIntent);
+ finish();
+ }
+
+ @Override
+ public void onBackPressed() {
+ setResult(RESULT_CANCELED);
+ super.onBackPressed();
+ }
+}
\ No newline at end of file
diff --git a/Src/app/src/main/java/net/micode/notes/ui/NoteGestureVerifyActivity.java b/Src/app/src/main/java/net/micode/notes/ui/NoteGestureVerifyActivity.java
new file mode 100644
index 0000000..c7e4758
--- /dev/null
+++ b/Src/app/src/main/java/net/micode/notes/ui/NoteGestureVerifyActivity.java
@@ -0,0 +1,231 @@
+package net.micode.notes.ui;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ContentUris;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.util.Log;
+import android.view.WindowManager;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+
+public class NoteGestureVerifyActivity extends Activity {
+ private static final String TAG = "NoteGestureVerifyActivity";
+ private static final int MAX_TRIES = 5;
+
+ private LockPatternView lockPatternView;
+ private TextView gestureStatus;
+ private ImageView lockIcon;
+ private TextView forgotPattern;
+ private int currentTries = 0;
+ private String savedPattern;
+ private long noteId;
+ private Handler handler = new Handler();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ // 设置全屏显示
+ getWindow().setFlags(
+ WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ setContentView(R.layout.note_gesture_verify);
+
+ initViews();
+ setupListeners();
+ loadSavedPattern();
+ }
+
+ private void initViews() {
+ lockPatternView = (LockPatternView) findViewById(R.id.lock_pattern_view);
+ gestureStatus = (TextView) findViewById(R.id.gesture_status);
+ lockIcon = (ImageView) findViewById(R.id.iv_lock_icon);
+ forgotPattern = (TextView) findViewById(R.id.tv_forgot_pattern);
+
+ // 设置初始状态
+ updateStatusText(getString(R.string.gesture_verify_hint));
+ }
+
+ private void setupListeners() {
+ lockPatternView.setPatternListener(new LockPatternView.OnPatternListener() {
+ @Override
+ public void onPatternComplete(String pattern) {
+ verifyPattern(pattern);
+ }
+ });
+
+ forgotPattern.setOnClickListener(v -> handleForgotPattern());
+ }
+
+ @SuppressLint("LongLogTag")
+ private void loadSavedPattern() {
+ Cursor cursor = null;
+ try {
+ noteId = getIntent().getLongExtra("note_id", -1);
+ if (noteId == -1) {
+ Log.e(TAG, "Invalid note ID");
+ finishWithError();
+ return;
+ }
+
+ // 从数据库读取手势密码
+ cursor = getContentResolver().query(
+ ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId),
+ new String[]{Notes.NoteColumns.GESTURE_PASSWORD},
+ null, null, null);
+
+ if (cursor != null && cursor.moveToFirst()) {
+ savedPattern = cursor.getString(0);
+ if (savedPattern == null || savedPattern.isEmpty()) {
+ Log.e(TAG, "No pattern set for this note");
+ finishWithSuccess(); // 如果没有设置密码,直接通过验证
+ return;
+ }
+ } else {
+ Log.e(TAG, "Note not found");
+ finishWithError();
+ return;
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error loading saved pattern", e);
+ finishWithError();
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ private void verifyPattern(String pattern) {
+ if (pattern.equals(savedPattern)) {
+ showSuccessState();
+ handler.postDelayed(this::finishWithSuccess, 500);
+ } else {
+ showErrorState();
+ }
+ }
+
+ private void showSuccessState() {
+ gestureStatus.setTextColor(getResources().getColor(R.color.gesture_success_color));
+ gestureStatus.setText(R.string.gesture_verify_success);
+ lockIcon.setImageTintList(ColorStateList.valueOf(
+ getResources().getColor(R.color.gesture_success_color)));
+
+ lockPatternView.showSuccess();
+
+ // 添加成功动画效果
+ lockIcon.animate()
+ .scaleX(1.2f)
+ .scaleY(1.2f)
+ .setDuration(200)
+ .withEndAction(() -> {
+ lockIcon.animate()
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(100)
+ .start();
+ })
+ .start();
+ }
+
+ private void showErrorState() {
+ currentTries++;
+ if (currentTries >= MAX_TRIES) {
+ finishWithError();
+ return;
+ }
+
+ gestureStatus.setTextColor(getResources().getColor(R.color.gesture_point_error));
+ gestureStatus.setText(getString(R.string.gesture_verify_error, MAX_TRIES - currentTries));
+ lockIcon.setImageTintList(ColorStateList.valueOf(
+ getResources().getColor(R.color.gesture_point_error)));
+
+ lockPatternView.showError();
+
+ // 添加错误动画效果
+ lockIcon.animate()
+ .translationX(20f)
+ .setDuration(50)
+ .withEndAction(() -> {
+ lockIcon.animate()
+ .translationX(-20f)
+ .setDuration(100)
+ .withEndAction(() -> {
+ lockIcon.animate()
+ .translationX(0f)
+ .setDuration(50)
+ .start();
+ })
+ .start();
+ })
+ .start();
+
+ // 重置状态
+ handler.postDelayed(() -> {
+ if (!isFinishing()) {
+ gestureStatus.setTextColor(getResources().getColor(R.color.gesture_text_color));
+ gestureStatus.setText(R.string.gesture_verify_hint);
+ lockIcon.setImageTintList(ColorStateList.valueOf(
+ getResources().getColor(R.color.gesture_icon_color)));
+ }
+ }, 1000);
+ }
+
+ private void handleForgotPattern() {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this)
+ .setTitle(R.string.gesture_forgot_title)
+ .setMessage(R.string.gesture_forgot_message)
+ .setPositiveButton(R.string.ok, (dialog, which) -> {
+ // 这里可以添加重置密码的逻辑
+ Toast.makeText(this, "请联系管理员重置密码", Toast.LENGTH_SHORT).show();
+ })
+ .setNegativeButton(R.string.cancel, null);
+
+ AlertDialog dialog = builder.create();
+ dialog.show();
+ }
+
+ private void finishWithSuccess() {
+ setResult(RESULT_OK);
+ finish();
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ }
+
+ private void finishWithError() {
+ Toast.makeText(this, "验证失败次数过多,请稍后重试", Toast.LENGTH_SHORT).show();
+ setResult(RESULT_CANCELED);
+ finish();
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ }
+
+ private void updateStatusText(String text) {
+ if (gestureStatus != null) {
+ gestureStatus.setText(text);
+ }
+ }
+
+ @Override
+ public void onBackPressed() {
+ // 返回时取消验证
+ setResult(RESULT_CANCELED);
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (handler != null) {
+ handler.removeCallbacksAndMessages(null);
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/Src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/Src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
index fb2f7ce..58cfb0a 100644
--- a/Src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
+++ b/Src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
@@ -65,6 +65,7 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.login.LoginActivity;
+import net.micode.notes.login.UserManagementActivity;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.tool.BackupUtils;
import net.micode.notes.tool.DataUtils;
@@ -1148,6 +1149,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case R.id.menu_search:
onSearchRequested(); // 触发搜索请求
break;
+ case R.id.menu_manage_users: {
+ Intent intent = new Intent(this, UserManagementActivity.class);
+ startActivity(intent);
+ break;
+ }
default:
break;
}
diff --git a/Src/app/src/main/java/net/micode/notes/ui/RichEditText.java b/Src/app/src/main/java/net/micode/notes/ui/RichEditText.java
new file mode 100644
index 0000000..ef97ffb
--- /dev/null
+++ b/Src/app/src/main/java/net/micode/notes/ui/RichEditText.java
@@ -0,0 +1,168 @@
+package net.micode.notes.ui;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.text.Editable;
+import android.text.Layout;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.BulletSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StrikethroughSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.AttributeSet;
+
+import android.graphics.Typeface;
+import android.widget.EditText;
+
+public class RichEditText extends EditText {
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ // 确保在初始化时,内容为空,避免默认插入图片或其他格式化内容
+ if (getText() == null || getText().length() == 0) {
+ setText(""); // 初始化为空文本
+ }
+ }
+
+ public RichEditText(Context context) {
+ super(context);
+ }
+
+ public RichEditText(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public RichEditText(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public void toggleBold() {
+ toggleStyle(Typeface.BOLD);
+ }
+
+ public void toggleItalic() {
+ toggleStyle(Typeface.ITALIC);
+ }
+
+ public void toggleUnderline() {
+ toggleSpan(new UnderlineSpan());
+ }
+
+ public void toggleStrikethrough() {
+ toggleSpan(new StrikethroughSpan());
+ }
+
+ public void toggleBullet() {
+ toggleSpan(new BulletSpan(20, Color.BLACK));
+ }
+
+ public void setTextColor(int color) {
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+
+ if (start < end) {
+ Editable editable = getText();
+ ForegroundColorSpan[] spans = editable.getSpans(start, end, ForegroundColorSpan.class);
+
+ for (ForegroundColorSpan span : spans) {
+ editable.removeSpan(span);
+ }
+
+ editable.setSpan(new ForegroundColorSpan(color), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ private void toggleStyle(int style) {
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+
+ if (start < end) {
+ Editable editable = getText();
+ StyleSpan[] spans = editable.getSpans(start, end, StyleSpan.class);
+ boolean hasStyle = false;
+
+ for (StyleSpan span : spans) {
+ if (span.getStyle() == style) {
+ editable.removeSpan(span);
+ hasStyle = true;
+ }
+ }
+
+ if (!hasStyle) {
+ editable.setSpan(new StyleSpan(style), start, end,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+
+ private void toggleSpan(Object span) {
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+
+ if (start < end) {
+ Editable editable = getText();
+ Object[] spans = editable.getSpans(start, end, span.getClass());
+ boolean hasSpan = spans.length > 0;
+
+ for (Object existingSpan : spans) {
+ editable.removeSpan(existingSpan);
+ }
+
+ if (!hasSpan) {
+ editable.setSpan(span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+ }
+ public int[] getCursorCoordinates() {
+ int[] location = new int[2];
+ getLocationOnScreen(location); // 获取 `RichEditText` 在屏幕上的位置
+
+ int cursorPosition = getSelectionStart();
+ Layout layout = getLayout();
+ if (layout != null) {
+ int line = layout.getLineForOffset(cursorPosition);
+ int baseline = layout.getLineBaseline(line);
+
+ int x = (int) layout.getPrimaryHorizontal(cursorPosition) + location[0];
+ int y = baseline + layout.getLineTop(line) + location[1];
+
+ return new int[]{x, y};
+ }
+ return location; // 如果布局未加载,返回编辑器位置
+ }
+ // 将富文本转换为HTML
+ public String toHtml() {
+ Editable text = getText();
+ if (text == null || text.length() == 0) {
+ return ""; // 如果内容为空,返回空字符串
+ }
+
+ // 使用 HtmlCompat 工具确保兼容性
+ String htmlContent = android.text.Html.toHtml(text);
+ return htmlContent
+ .replaceAll("", "") // 去除段落标签
+ .replaceAll("
", "")
+ .replaceAll(" ", " ") // 替换无意义的空格
+ .trim();
+ }
+
+ // 从HTML加载富文本
+ public void fromHtml(String html) {
+ if (html == null || html.isEmpty()) {
+ setText(""); // 如果 HTML 内容为空,设置为空字符串
+ return;
+ }
+
+ // 使用 HtmlCompat 工具解析 HTML,兼容中文和样式
+ setText(android.text.Html.fromHtml(html));
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/gesture_background.xml b/Src/app/src/main/res/drawable/gesture_background.xml
new file mode 100644
index 0000000..7b83433
--- /dev/null
+++ b/Src/app/src/main/res/drawable/gesture_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/gesture_button_background.xml b/Src/app/src/main/res/drawable/gesture_button_background.xml
new file mode 100644
index 0000000..6692a83
--- /dev/null
+++ b/Src/app/src/main/res/drawable/gesture_button_background.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/gesture_panel_background.xml b/Src/app/src/main/res/drawable/gesture_panel_background.xml
new file mode 100644
index 0000000..2e3c374
--- /dev/null
+++ b/Src/app/src/main/res/drawable/gesture_panel_background.xml
@@ -0,0 +1,17 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_delete.xml b/Src/app/src/main/res/drawable/ic_delete.xml
new file mode 100644
index 0000000..883bcaa
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_delete.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Src/app/src/main/res/drawable/ic_format_bold.xml b/Src/app/src/main/res/drawable/ic_format_bold.xml
new file mode 100644
index 0000000..c7e3c22
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_format_bold.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_format_bullet.xml b/Src/app/src/main/res/drawable/ic_format_bullet.xml
new file mode 100644
index 0000000..5d107f4
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_format_bullet.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_format_color.xml b/Src/app/src/main/res/drawable/ic_format_color.xml
new file mode 100644
index 0000000..bee425c
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_format_color.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_format_italic.xml b/Src/app/src/main/res/drawable/ic_format_italic.xml
new file mode 100644
index 0000000..1e07d3d
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_format_italic.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_format_strikethrough.xml b/Src/app/src/main/res/drawable/ic_format_strikethrough.xml
new file mode 100644
index 0000000..8f025fb
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_format_strikethrough.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_format_underline.xml b/Src/app/src/main/res/drawable/ic_format_underline.xml
new file mode 100644
index 0000000..84a3aab
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_format_underline.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/drawable/ic_lock_outline.xml b/Src/app/src/main/res/drawable/ic_lock_outline.xml
new file mode 100644
index 0000000..b22c676
--- /dev/null
+++ b/Src/app/src/main/res/drawable/ic_lock_outline.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/Src/app/src/main/res/layout/activity_login.xml b/Src/app/src/main/res/layout/activity_login.xml
index e777037..8222c88 100644
--- a/Src/app/src/main/res/layout/activity_login.xml
+++ b/Src/app/src/main/res/layout/activity_login.xml
@@ -6,16 +6,27 @@
android:orientation="vertical"
android:padding="16dp"
android:background="@color/background_color">
+
+
+ android:layout_marginTop="12dp" />