callback) {
+ executor.execute(() -> {
+ try {
+ // 检查是否是系统文件夹(禁止删除)
+ if (folderId <= 0) {
+ callback.onError(new IllegalArgumentException("System folder cannot be deleted"));
+ return;
+ }
+
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER);
+ values.put(NoteColumns.LOCAL_MODIFIED, 1);
+
+ Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, folderId);
+ int rows = contentResolver.update(uri, values, null, null);
+
+ if (rows > 0) {
+ callback.onSuccess(rows);
+ Log.d(TAG, "Successfully moved folder to trash: " + folderId);
+ } else {
+ callback.onError(new RuntimeException("No folder found with ID: " + folderId));
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to delete folder: " + folderId, e);
+ callback.onError(e);
+ }
+ });
+ }
+
/**
* 关闭Executor
*
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/FolderOperationDialogs.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/FolderOperationDialogs.java
new file mode 100644
index 0000000..8e33de0
--- /dev/null
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/FolderOperationDialogs.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright (c) 2025, Modern Notes Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.ui;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.InputFilter;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.TextView;
+
+import net.micode.notes.R;
+
+/**
+ * 文件夹操作对话框工具类
+ *
+ * 提供重命名和删除文件夹的对话框
+ *
+ */
+public class FolderOperationDialogs {
+
+ private static final int MAX_FOLDER_NAME_LENGTH = 50;
+
+ /**
+ * 显示重命名文件夹对话框
+ *
+ * @param activity Activity实例
+ * @param currentName 当前文件夹名称
+ * @param listener 重命名监听器
+ */
+ public static void showRenameDialog(Context activity, String currentName,
+ OnRenameListener listener) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.dialog_rename_folder_title);
+
+ // 创建输入框
+ final EditText input = new EditText(activity);
+ input.setText(currentName);
+ input.setHint(R.string.dialog_create_folder_hint);
+ input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(MAX_FOLDER_NAME_LENGTH)});
+ input.setSelection(input.getText().length()); // 光标移到末尾
+
+ builder.setView(input);
+
+ builder.setPositiveButton(R.string.menu_rename, (dialog, which) -> {
+ String newName = input.getText().toString().trim();
+ if (TextUtils.isEmpty(newName)) {
+ listener.onError(activity.getString(R.string.error_folder_name_empty));
+ return;
+ }
+ if (newName.length() > MAX_FOLDER_NAME_LENGTH) {
+ listener.onError(activity.getString(R.string.error_folder_name_too_long));
+ return;
+ }
+
+ listener.onRename(newName);
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ listener.onCancel();
+ });
+
+ builder.show();
+ }
+
+ /**
+ * 显示删除文件夹确认对话框
+ *
+ * @param activity Activity实例
+ * @param folderName 文件夹名称
+ * @param noteCount 文件夹中的笔记数量
+ * @param listener 删除监听器
+ */
+ public static void showDeleteFolderDialog(Context activity, String folderName,
+ int noteCount, OnDeleteListener listener) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(R.string.dialog_delete_folder_title);
+
+ // 创建自定义消息视图
+ LayoutInflater inflater = LayoutInflater.from(activity);
+ View messageView = inflater.inflate(R.layout.dialog_folder_delete, null);
+ TextView messageText = messageView.findViewById(R.id.tv_delete_message);
+
+ String message;
+ if (noteCount > 0) {
+ message = activity.getString(R.string.dialog_delete_folder_with_notes, folderName, noteCount);
+ } else {
+ message = activity.getString(R.string.dialog_delete_folder_empty, folderName);
+ }
+ messageText.setText(message);
+
+ builder.setView(messageView);
+
+ builder.setPositiveButton(R.string.menu_delete, (dialog, which) -> {
+ listener.onDelete();
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
+ listener.onCancel();
+ });
+
+ builder.show();
+ }
+
+ /**
+ * 重命名监听器接口
+ */
+ public interface OnRenameListener {
+ /**
+ * 重命名确认回调
+ *
+ * @param newName 新名称
+ */
+ void onRename(String newName);
+
+ /**
+ * 取消回调
+ */
+ default void onCancel() {
+ }
+
+ /**
+ * 错误回调
+ *
+ * @param errorMessage 错误消息
+ */
+ default void onError(String errorMessage) {
+ }
+ }
+
+ /**
+ * 删除监听器接口
+ */
+ public interface OnDeleteListener {
+ /**
+ * 删除确认回调
+ */
+ void onDelete();
+
+ /**
+ * 取消回调
+ */
+ default void onCancel() {
+ }
+ }
+}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java
index 2b9c202..c53f0ed 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteInfoAdapter.java
@@ -30,6 +30,7 @@ import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.ResourceParser;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
+import net.micode.notes.ui.SwipeMenuLayout;
import android.util.Log;
@@ -57,7 +58,17 @@ public class NoteInfoAdapter extends BaseAdapter {
private OnNoteButtonClickListener buttonClickListener;
private OnNoteItemClickListener itemClickListener;
private OnNoteItemLongClickListener itemLongClickListener;
-
+
+ // 滑动菜单监听器
+ private OnSwipeMenuClickListener swipeMenuClickListener;
+ private SwipeMenuLayout currentOpenMenu; // 当前打开的滑动菜单
+
+ // 是否回收站模式
+ private boolean isTrashMode = false;
+
+ // 是否多选模式
+ private boolean isMultiSelectMode = false;
+
/**
* 便签按钮点击事件回调接口
*/
@@ -84,7 +95,20 @@ public class NoteInfoAdapter extends BaseAdapter {
public interface OnNoteItemLongClickListener {
void onNoteItemLongClick(int position, long noteId);
}
-
+
+ /**
+ * 滑动菜单按钮点击监听器接口
+ */
+ public interface OnSwipeMenuClickListener {
+ void onSwipeEdit(long itemId);
+ void onSwipePin(long itemId);
+ void onSwipeMove(long itemId);
+ void onSwipeDelete(long itemId);
+ void onSwipeRename(long itemId);
+ void onSwipeRestore(long itemId);
+ void onSwipePermanentDelete(long itemId);
+ }
+
/**
* 构造函数
*
@@ -95,7 +119,55 @@ public class NoteInfoAdapter extends BaseAdapter {
this.notes = new ArrayList<>();
this.selectedIds = new HashSet<>();
}
-
+
+ /**
+ * 设置滑动菜单监听器
+ *
+ * @param listener 滑动菜单监听器
+ */
+ public void setOnSwipeMenuClickListener(OnSwipeMenuClickListener listener) {
+ this.swipeMenuClickListener = listener;
+ }
+
+ /**
+ * 设置回收站模式
+ *
+ * @param isTrashMode 是否回收站模式
+ */
+ public void setTrashMode(boolean isTrashMode) {
+ this.isTrashMode = isTrashMode;
+ notifyDataSetChanged();
+ }
+
+ /**
+ * 获取当前是否回收站模式
+ *
+ * @return 是否回收站模式
+ */
+ public boolean isTrashMode() {
+ return isTrashMode;
+ }
+
+ /**
+ * 设置多选模式
+ *
+ * @param isMultiSelectMode 是否多选模式
+ */
+ public void setMultiSelectMode(boolean isMultiSelectMode) {
+ this.isMultiSelectMode = isMultiSelectMode;
+ closeAllMenus();
+ notifyDataSetChanged();
+ }
+
+ /**
+ * 获取当前是否多选模式
+ *
+ * @return 是否多选模式
+ */
+ public boolean isMultiSelectMode() {
+ return isMultiSelectMode;
+ }
+
/**
* 设置便签列表
*
@@ -105,7 +177,7 @@ public class NoteInfoAdapter extends BaseAdapter {
this.notes = notes != null ? notes : new ArrayList<>();
notifyDataSetChanged();
}
-
+
/**
* 设置选中的便签 ID 集合
*
@@ -153,7 +225,7 @@ public class NoteInfoAdapter extends BaseAdapter {
public HashSet getSelectedIds() {
return selectedIds;
}
-
+
/**
* 切换选中状态
*
@@ -167,7 +239,7 @@ public class NoteInfoAdapter extends BaseAdapter {
}
notifyDataSetChanged();
}
-
+
/**
* 设置按钮点击监听器
*
@@ -184,79 +256,159 @@ public class NoteInfoAdapter extends BaseAdapter {
public void setOnNoteItemLongClickListener(OnNoteItemLongClickListener listener) {
this.itemLongClickListener = listener;
}
-
+
@Override
public int getCount() {
return notes.size();
}
-
+
@Override
public Object getItem(int position) {
return position >= 0 && position < notes.size() ? notes.get(position) : null;
}
-
+
@Override
public long getItemId(int position) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position);
return note != null ? note.getId() : -1;
}
-
+
+ /**
+ * 关闭所有已打开的滑动菜单
+ */
+ public void closeAllMenus() {
+ if (currentOpenMenu != null && currentOpenMenu.isMenuOpen()) {
+ currentOpenMenu.closeMenu();
+ currentOpenMenu = null;
+ }
+ }
+
@Override
public View getView(int position, View convertView, ViewGroup parent) {
Log.d("NoteInfoAdapter", "getView called, position: " + position + ", convertView: " + (convertView != null ? "REUSED" : "NEW"));
ViewHolder holder;
-
+
if (convertView == null) {
- convertView = inflater.inflate(R.layout.note_item, parent, false);
+ // 使用 SwipeMenuLayout 包装内容
+ convertView = inflater.inflate(R.layout.note_item_swipe, parent, false);
+ SwipeMenuLayout swipeLayout = (SwipeMenuLayout) convertView;
+
holder = new ViewHolder();
+ holder.swipeMenuLayout = swipeLayout;
holder.title = convertView.findViewById(R.id.tv_title);
holder.time = convertView.findViewById(R.id.tv_time);
holder.typeIcon = convertView.findViewById(R.id.iv_type_icon);
holder.checkBox = convertView.findViewById(android.R.id.checkbox);
holder.pinnedIcon = convertView.findViewById(R.id.iv_pinned_icon);
holder.lockIcon = convertView.findViewById(R.id.iv_lock_icon);
+ holder.swipeMenuNormal = convertView.findViewById(R.id.swipe_menu_normal);
+ holder.swipeMenuTrash = convertView.findViewById(R.id.swipe_menu_trash);
+
convertView.setTag(holder);
-
- convertView.setOnClickListener(v -> {
- Log.d("NoteInfoAdapter", "===== onClick TRIGGERED =====");
- ViewHolder currentHolder = (ViewHolder) v.getTag();
- if (currentHolder != null && itemClickListener != null) {
- Log.d("NoteInfoAdapter", "Calling itemClickListener");
- NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(currentHolder.position);
- if (note != null) {
- itemClickListener.onNoteItemClick(currentHolder.position, note.getId());
+
+ // 设置滑动菜单监听器
+ if (swipeMenuClickListener != null) {
+ swipeLayout.setOnMenuButtonClickListener(new SwipeMenuLayout.OnMenuButtonClickListener() {
+ @Override
+ public void onEdit(long itemId) {
+ swipeMenuClickListener.onSwipeEdit(itemId);
+ }
+
+ @Override
+ public void onPin(long itemId) {
+ swipeMenuClickListener.onSwipePin(itemId);
+ }
+
+ @Override
+ public void onMove(long itemId) {
+ swipeMenuClickListener.onSwipeMove(itemId);
+ }
+
+ @Override
+ public void onDelete(long itemId) {
+ swipeMenuClickListener.onSwipeDelete(itemId);
+ }
+
+ @Override
+ public void onRename(long itemId) {
+ swipeMenuClickListener.onSwipeRename(itemId);
+ }
+
+ @Override
+ public void onRestore(long itemId) {
+ swipeMenuClickListener.onSwipeRestore(itemId);
+ }
+
+ @Override
+ public void onPermanentDelete(long itemId) {
+ swipeMenuClickListener.onSwipePermanentDelete(itemId);
+ }
+ });
+
+ // 设置内容点击监听器
+ swipeLayout.setOnContentClickListener(itemId -> {
+ if (itemClickListener != null) {
+ // 根据位置查找对应的笔记
+ NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(holder.position);
+ if (note != null && note.getId() == itemId) {
+ itemClickListener.onNoteItemClick(holder.position, itemId);
+ }
+ }
+ });
+
+ // 设置内容长按监听器
+ swipeLayout.setOnContentLongClickListener(itemId -> {
+ if (itemLongClickListener != null) {
+ // 根据位置查找对应的笔记
+ NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(holder.position);
+ if (note != null && note.getId() == itemId) {
+ itemLongClickListener.onNoteItemLongClick(holder.position, itemId);
+ }
}
- }
- Log.d("NoteInfoAdapter", "===== onClick END =====");
- });
-
- convertView.setOnLongClickListener(v -> {
- Log.d("NoteInfoAdapter", "===== setOnLongClickListener TRIGGERED =====");
- Log.d("NoteInfoAdapter", "Event triggered on view: " + v.getClass().getSimpleName());
- ViewHolder currentHolder = (ViewHolder) v.getTag();
- if (currentHolder != null && itemLongClickListener != null) {
- Log.d("NoteInfoAdapter", "Calling itemLongClickListener");
- itemLongClickListener.onNoteItemLongClick(currentHolder.position, currentHolder.position < notes.size() ? notes.get(currentHolder.position).getId() : -1);
- } else {
- Log.e("NoteInfoAdapter", "itemLongClickListener is NULL!");
- }
- Log.d("NoteInfoAdapter", "===== setOnLongClickListener END =====");
- return true;
- });
+ });
+ }
+
+ // 点击内容区域时,关闭已打开的菜单
+ // 不设置convertView的setOnClickListener,避免干扰SwipeMenuLayout的事件处理
+ // SwipeMenuLayout会处理自身的触摸事件和按钮点击事件
+ // 长按事件也由SwipeMenuLayout处理,不再单独设置setOnLongClickListener
+
} else {
holder = (ViewHolder) convertView.getTag();
}
-
+
holder.position = position;
-
+
+ // 设置itemId到SwipeMenuLayout(使用单独的方法而非setTag)
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position);
if (note != null) {
+ holder.swipeMenuLayout.setItemId(note.getId());
+ // 多选模式下禁用滑动菜单
+ holder.swipeMenuLayout.setSwipeEnabled(!isMultiSelectMode);
+ }
+
+ // 根据模式设置菜单可见性
+ if (holder.swipeMenuNormal != null && holder.swipeMenuTrash != null) {
+ if (isTrashMode) {
+ holder.swipeMenuNormal.setVisibility(View.GONE);
+ holder.swipeMenuTrash.setVisibility(View.VISIBLE);
+ } else {
+ holder.swipeMenuNormal.setVisibility(View.VISIBLE);
+ holder.swipeMenuTrash.setVisibility(View.GONE);
+ }
+ }
+
+ // 设置内容视图的背景
+ View contentView = holder.swipeMenuLayout.getChildAt(0);
+ if (contentView != null && note != null) {
+ // ...设置内容...
+
String title = note.title;
if (title == null || title.trim().isEmpty()) {
title = "无标题";
}
holder.title.setText(title);
-
+
// 设置类型图标和时间显示
if (note.type == Notes.TYPE_FOLDER) {
// 文件夹
@@ -270,11 +422,11 @@ public class NoteInfoAdapter extends BaseAdapter {
holder.time.setVisibility(View.VISIBLE);
holder.time.setText(formatDate(note.modifiedDate));
}
-
+
int bgResId;
int totalCount = getCount();
int bgColorId = note.bgColorId;
-
+
if (totalCount == 1) {
bgResId = NoteItemBgResources.getNoteBgSingleRes(bgColorId);
} else if (position == 0) {
@@ -284,13 +436,13 @@ public class NoteInfoAdapter extends BaseAdapter {
} else {
bgResId = NoteItemBgResources.getNoteBgNormalRes(bgColorId);
}
-
- convertView.setBackgroundResource(bgResId);
+
+ contentView.setBackgroundResource(bgResId);
if (selectedIds.contains(note.getId())) {
- convertView.setActivated(true);
+ contentView.setActivated(true);
} else {
- convertView.setActivated(false);
+ contentView.setActivated(false);
}
Log.d("NoteInfoAdapter", "===== Setting checkbox visibility =====");
@@ -324,7 +476,7 @@ public class NoteInfoAdapter extends BaseAdapter {
return convertView;
}
-
+
/**
* 格式化日期
*
@@ -335,7 +487,7 @@ public class NoteInfoAdapter extends BaseAdapter {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault());
return sdf.format(new Date(timestamp));
}
-
+
/**
* ViewHolder 模式:优化 ListView 性能
*/
@@ -347,5 +499,10 @@ public class NoteInfoAdapter extends BaseAdapter {
CheckBox checkBox;
ImageView pinnedIcon;
int position;
+
+ // 滑动菜单相关
+ SwipeMenuLayout swipeMenuLayout;
+ View swipeMenuNormal;
+ View swipeMenuTrash;
}
}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
index 982f0ad..97e307a 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
@@ -76,6 +76,7 @@ public class NotesListActivity extends AppCompatActivity
implements NoteInfoAdapter.OnNoteButtonClickListener,
NoteInfoAdapter.OnNoteItemClickListener,
NoteInfoAdapter.OnNoteItemLongClickListener,
+ NoteInfoAdapter.OnSwipeMenuClickListener,
SidebarFragment.OnSidebarItemSelectedListener {
private static final String TAG = "NotesListActivity";
private static final int REQUEST_CODE_OPEN_NODE = 102;
@@ -194,6 +195,7 @@ public class NotesListActivity extends AppCompatActivity
adapter.setOnNoteButtonClickListener(this);
adapter.setOnNoteItemClickListener(this);
adapter.setOnNoteItemLongClickListener(this);
+ adapter.setOnSwipeMenuClickListener(this);
// 设置点击监听
binding.notesList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@@ -281,6 +283,11 @@ public class NotesListActivity extends AppCompatActivity
public void onChanged(List notes) {
updateAdapter(notes);
updateToolbarForNormalMode();
+
+ // 根据当前文件夹设置Adapter的模式(回收站/普通)
+ if (adapter != null) {
+ adapter.setTrashMode(viewModel.isTrashMode());
+ }
}
});
@@ -554,6 +561,10 @@ public class NotesListActivity extends AppCompatActivity
if (binding.btnNewNote != null) {
binding.btnNewNote.setVisibility(View.GONE);
}
+ // 禁用滑动菜单
+ if (adapter != null) {
+ adapter.setMultiSelectMode(true);
+ }
// 更新toolbar为多选模式
updateToolbarForMultiSelectMode();
}
@@ -572,6 +583,8 @@ public class NotesListActivity extends AppCompatActivity
if (adapter != null) {
adapter.setSelectedIds(new java.util.HashSet<>());
adapter.notifyDataSetChanged();
+ // 启用滑动菜单
+ adapter.setMultiSelectMode(false);
}
// 更新toolbar为普通模式
updateToolbarForNormalMode();
@@ -964,6 +977,98 @@ public class NotesListActivity extends AppCompatActivity
}
}
+ @Override
+ public void onRenameFolder(long folderId) {
+ // 获取当前文件夹信息
+ viewModel.getFolderInfo(folderId, new NotesRepository.Callback() {
+ @Override
+ public void onSuccess(NotesRepository.NoteInfo folderInfo) {
+ if (folderInfo != null) {
+ // 显示重命名对话框
+ FolderOperationDialogs.showRenameDialog(NotesListActivity.this,
+ folderInfo.title, new FolderOperationDialogs.OnRenameListener() {
+ @Override
+ public void onRename(String newName) {
+ viewModel.renameFolder(folderId, newName);
+ }
+
+ @Override
+ public void onError(String errorMessage) {
+ Toast.makeText(NotesListActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ Toast.makeText(NotesListActivity.this, "文件夹不存在", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ Toast.makeText(NotesListActivity.this, "获取文件夹信息失败: " + error.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ @Override
+ public void onDeleteFolder(long folderId) {
+ // 获取当前文件夹信息和笔记数量
+ viewModel.getFolderInfo(folderId, new NotesRepository.Callback() {
+ @Override
+ public void onSuccess(NotesRepository.NoteInfo folderInfo) {
+ if (folderInfo != null) {
+ // 显示删除确认对话框
+ FolderOperationDialogs.showDeleteFolderDialog(NotesListActivity.this,
+ folderInfo.title, folderInfo.noteCount, new FolderOperationDialogs.OnDeleteListener() {
+ @Override
+ public void onDelete() {
+ viewModel.deleteFolder(folderId);
+ }
+ });
+ } else {
+ Toast.makeText(NotesListActivity.this, "文件夹不存在", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ @Override
+ public void onError(Exception error) {
+ Toast.makeText(NotesListActivity.this, "获取文件夹信息失败: " + error.getMessage(), Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ /**
+ * 显示重命名笔记对话框
+ *
+ * @param note 笔记信息
+ */
+ private void showRenameNoteDialog(NotesRepository.NoteInfo note) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.dialog_rename_folder_title);
+
+ final EditText input = new EditText(this);
+ input.setText(note.title);
+ input.setHint("请输入笔记标题");
+ input.setSelection(input.getText().length());
+
+ builder.setView(input);
+
+ builder.setPositiveButton(R.string.menu_rename, (dialog, which) -> {
+ String newName = input.getText().toString().trim();
+ if (newName.isEmpty()) {
+ Toast.makeText(this, "笔记标题不能为空", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ if (newName.length() > 50) {
+ Toast.makeText(this, "笔记标题不能超过50个字符", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ viewModel.renameNote(note.getId(), newName);
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ }
+
/**
* 返回键按下事件处理
*
@@ -988,8 +1093,82 @@ public class NotesListActivity extends AppCompatActivity
viewModel.loadNotes(Notes.ID_ROOT_FOLDER);
}
} else {
- // 根文件夹:最小化应用
- moveTaskToBack(true);
+ // 根文件夹:调用父类方法处理返回键
+ super.onBackPressed();
+ }
+ }
+
+ // ==================== NoteInfoAdapter.OnSwipeMenuClickListener 实现 ====================
+
+ @Override
+ public void onSwipeEdit(long itemId) {
+ // 重命名笔记或文件夹
+ List notes = viewModel.getNotesLiveData().getValue();
+ if (notes != null) {
+ for (NotesRepository.NoteInfo note : notes) {
+ if (note.getId() == itemId) {
+ if (note.type == Notes.TYPE_FOLDER) {
+ // 文件夹重命名
+ onRenameFolder(itemId);
+ } else {
+ // 笔记重命名
+ showRenameNoteDialog(note);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSwipePin(long itemId) {
+ // 置顶/取消置顶笔记
+ viewModel.clearSelection();
+ viewModel.toggleNoteSelection(itemId, true);
+ viewModel.toggleSelectedNotesPin();
+ }
+
+ @Override
+ public void onSwipeMove(long itemId) {
+ // 移动笔记
+ viewModel.clearSelection();
+ viewModel.toggleNoteSelection(itemId, true);
+ showMoveMenu();
+ }
+
+ @Override
+ public void onSwipeDelete(long itemId) {
+ // 删除笔记
+ viewModel.clearSelection();
+ viewModel.toggleNoteSelection(itemId, true);
+ showDeleteDialog();
+ }
+
+ @Override
+ public void onSwipeRename(long itemId) {
+ // 重命名文件夹(如果是文件夹类型)
+ onRenameFolder(itemId);
+ }
+
+ @Override
+ public void onSwipeRestore(long itemId) {
+ // 恢复回收站笔记
+ viewModel.clearSelection();
+ viewModel.toggleNoteSelection(itemId, true);
+ viewModel.restoreSelectedNotes();
+ }
+
+ @Override
+ public void onSwipePermanentDelete(long itemId) {
+ // 永久删除回收站笔记
+ List notes = viewModel.getNotesLiveData().getValue();
+ if (notes != null) {
+ for (NotesRepository.NoteInfo note : notes) {
+ if (note.getId() == itemId) {
+ showDeleteForeverConfirmDialog(note);
+ break;
+ }
+ }
}
}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java
index 269f8e8..38b8d0f 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SidebarFragment.java
@@ -29,6 +29,7 @@ import android.view.animation.TranslateAnimation;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
@@ -121,6 +122,18 @@ public class SidebarFragment extends Fragment {
* 关闭侧栏
*/
void onCloseSidebar();
+
+ /**
+ * 重命名文件夹
+ * @param folderId 文件夹ID
+ */
+ void onRenameFolder(long folderId);
+
+ /**
+ * 删除文件夹
+ * @param folderId 文件夹ID
+ */
+ void onDeleteFolder(long folderId);
}
@Override
@@ -178,6 +191,7 @@ public class SidebarFragment extends Fragment {
binding.rvFolderTree.setLayoutManager(new LinearLayoutManager(requireContext()));
adapter = new FolderTreeAdapter(new ArrayList<>(), viewModel);
adapter.setOnFolderItemClickListener(this::handleFolderItemClick);
+ adapter.setOnFolderItemLongClickListener(this::handleFolderItemLongClick);
binding.rvFolderTree.setAdapter(adapter);
}
@@ -315,6 +329,46 @@ public class SidebarFragment extends Fragment {
}
}
+ /**
+ * 处理文件夹项长按
+ */
+ private void handleFolderItemLongClick(long folderId) {
+ android.util.Log.d(TAG, "handleFolderItemLongClick: folderId=" + folderId);
+ // 检查是否是系统文件夹(根文件夹、回收站等不允许重命名/删除)
+ if (folderId <= 0) {
+ android.util.Log.d(TAG, "System folder, ignoring long press");
+ return;
+ }
+
+ showFolderContextMenu(folderId);
+ }
+
+ /**
+ * 显示文件夹上下文菜单
+ */
+ private void showFolderContextMenu(long folderId) {
+ PopupMenu popup = new PopupMenu(requireContext(), binding.getRoot());
+ popup.getMenuInflater().inflate(R.menu.folder_context_menu, popup.getMenu());
+
+ popup.setOnMenuItemClickListener(item -> {
+ int itemId = item.getItemId();
+ if (itemId == R.id.action_rename && listener != null) {
+ listener.onRenameFolder(folderId);
+ return true;
+ } else if (itemId == R.id.action_delete && listener != null) {
+ listener.onDeleteFolder(folderId);
+ return true;
+ } else if (itemId == R.id.action_move) {
+ // TODO: 实现移动功能(阶段3)
+ Toast.makeText(requireContext(), "移动功能待实现", Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ return false;
+ });
+
+ popup.show();
+ }
+
// 双击检测专用变量(针对文件夹列表项)
private long lastFolderClickTime = 0;
private long lastClickedFolderId = -1;
@@ -388,6 +442,7 @@ public class SidebarFragment extends Fragment {
private List folderItems;
private FolderListViewModel viewModel;
private OnFolderItemClickListener folderItemClickListener;
+ private OnFolderItemLongClickListener folderItemLongClickListener;
public FolderTreeAdapter(List folderItems, FolderListViewModel viewModel) {
this.folderItems = folderItems;
@@ -402,12 +457,16 @@ public class SidebarFragment extends Fragment {
this.folderItemClickListener = listener;
}
+ public void setOnFolderItemLongClickListener(OnFolderItemLongClickListener listener) {
+ this.folderItemLongClickListener = listener;
+ }
+
@NonNull
@Override
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.sidebar_folder_item, parent, false);
- return new FolderViewHolder(view, folderItemClickListener);
+ return new FolderViewHolder(view, folderItemClickListener, folderItemLongClickListener);
}
@Override
@@ -430,10 +489,13 @@ public class SidebarFragment extends Fragment {
private TextView tvNoteCount;
private FolderTreeItem currentItem;
private final OnFolderItemClickListener folderItemClickListener;
+ private final OnFolderItemLongClickListener folderItemLongClickListener;
- public FolderViewHolder(@NonNull View itemView, OnFolderItemClickListener listener) {
+ public FolderViewHolder(@NonNull View itemView, OnFolderItemClickListener clickListener,
+ OnFolderItemLongClickListener longClickListener) {
super(itemView);
- this.folderItemClickListener = listener;
+ this.folderItemClickListener = clickListener;
+ this.folderItemLongClickListener = longClickListener;
indentView = itemView.findViewById(R.id.indent_view);
ivExpandIcon = itemView.findViewById(R.id.iv_expand_icon);
ivFolderIcon = itemView.findViewById(R.id.iv_folder_icon);
@@ -469,6 +531,15 @@ public class SidebarFragment extends Fragment {
folderItemClickListener.onFolderClick(item.folderId);
}
});
+
+ // 设置长按监听器
+ itemView.setOnLongClickListener(v -> {
+ if (folderItemLongClickListener != null) {
+ folderItemLongClickListener.onFolderLongClick(item.folderId);
+ return true;
+ }
+ return false;
+ });
}
}
}
@@ -480,6 +551,13 @@ public class SidebarFragment extends Fragment {
void onFolderClick(long folderId);
}
+ /**
+ * 文件夹项长按监听器接口
+ */
+ public interface OnFolderItemLongClickListener {
+ void onFolderLongClick(long folderId);
+ }
+
/**
* FolderTreeItem
* 文件夹树项数据模型
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SwipeDetector.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SwipeDetector.java
new file mode 100644
index 0000000..fff005b
--- /dev/null
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SwipeDetector.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (c) 2025, Modern Notes Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.ui;
+
+import android.content.Context;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * 原生滑动检测器
+ *
+ * 实现左滑操作按钮的检测
+ *
+ */
+public class SwipeDetector extends GestureDetector.SimpleOnGestureListener {
+
+ private static final float SWIPE_THRESHOLD = 100f; // 滑动阈值(像素)
+
+ // 滑动状态
+ private float downX;
+ private float downY;
+ private boolean isSwiping = false;
+
+ // 监听器
+ private SwipeListener swipeListener;
+
+ /**
+ * 滑动监听器接口
+ */
+ public interface SwipeListener {
+ /**
+ * 左滑开始
+ */
+ void onSwipeStart();
+
+ /**
+ * 滑动中
+ *
+ * @param distance 滑动距离(负值表示左滑)
+ */
+ void onSwipeMove(float distance);
+
+ /**
+ * 左滑结束
+ *
+ * @param distance 最终滑动距离
+ */
+ void onSwipeEnd(float distance);
+ }
+
+ /**
+ * 构造函数
+ *
+ * @param context 上下文
+ * @param listener 滑动监听器
+ */
+ public SwipeDetector(Context context, SwipeListener listener) {
+ this.swipeListener = listener;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ downX = e.getX();
+ downY = e.getY();
+ isSwiping = false;
+ return true;
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ if (!isSwiping) {
+ isSwiping = true;
+ if (swipeListener != null) {
+ swipeListener.onSwipeStart();
+ }
+ }
+
+ if (swipeListener != null) {
+ swipeListener.onSwipeMove(distanceX);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ if (swipeListener != null) {
+ swipeListener.onSwipeEnd(e2.getX() - downX);
+ }
+ return true;
+ }
+
+ /**
+ * 获取滑动距离(负值表示左滑)
+ *
+ * @param currentX 当前X坐标
+ * @return 滑动距离
+ */
+ public float getSwipeDistance(float currentX) {
+ return currentX - downX;
+ }
+
+ /**
+ * 是否达到滑动阈值
+ *
+ * @param distance 滑动距离
+ * @return true如果达到阈值
+ */
+ public boolean isThresholdReached(float distance) {
+ return Math.abs(distance) >= SWIPE_THRESHOLD;
+ }
+
+ /**
+ * 是否是左滑
+ *
+ * @param distance 滑动距离
+ * @return true如果是左滑
+ */
+ public boolean isSwipeLeft(float distance) {
+ return distance < 0;
+ }
+
+ /**
+ * 是否是右滑
+ *
+ * @param distance 滑动距离
+ * @return true如果是右滑
+ */
+ public boolean isSwipeRight(float distance) {
+ return distance > 0;
+ }
+
+ /**
+ * 重置状态
+ */
+ public void reset() {
+ downX = 0;
+ downY = 0;
+ isSwiping = false;
+ }
+}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SwipeMenuLayout.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SwipeMenuLayout.java
new file mode 100644
index 0000000..f26f649
--- /dev/null
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SwipeMenuLayout.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright (c) 2025, Modern Notes Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.micode.notes.ui;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.OvershootInterpolator;
+import android.view.animation.TranslateAnimation;
+import android.widget.FrameLayout;
+import android.widget.OverScroller;
+
+import androidx.core.view.GestureDetectorCompat;
+
+/**
+ * 支持滑动操作的布局
+ * 包装列表项,支持左滑显示操作按钮
+ */
+public class SwipeMenuLayout extends FrameLayout {
+
+ private static final String TAG = "SwipeMenuLayout";
+ private static final int MENU_WIDTH_DP = 240;
+ private static final int MIN_VELOCITY = 500;
+ private static final int MAX_OVERSCROLL = 100;
+
+ private View contentView;
+ private View menuView;
+ private int menuWidth;
+ private int screenWidth;
+
+ private OverScroller scroller;
+ private VelocityTracker velocityTracker;
+ private float lastX;
+ private float downX;
+ private float downY;
+ private long downTime;
+ private int currentState = STATE_CLOSE;
+ private float currentScrollX = 0;
+ private boolean isScrolling = false;
+ private boolean longPressTriggered = false;
+
+ private static final int STATE_CLOSE = 0;
+ private static final int STATE_OPEN = 1;
+ private static final int STATE_SWIPING = 2;
+ private static final float TOUCH_SLOP = 10f;
+ private static final int CLICK_TIME_THRESHOLD = 300;
+ private static final int LONG_PRESS_TIME_THRESHOLD = 500;
+
+ private Handler longPressHandler;
+ private Runnable longPressRunnable;
+
+ private OnMenuButtonClickListener menuButtonClickListener;
+
+ private OnContentClickListener contentClickListener;
+
+ private OnContentLongClickListener contentLongClickListener;
+
+ private long itemId;
+
+ private boolean swipeEnabled = true;
+
+ public interface OnMenuButtonClickListener {
+ void onEdit(long itemId);
+
+ void onPin(long itemId);
+
+ void onMove(long itemId);
+
+ void onDelete(long itemId);
+
+ void onRename(long itemId);
+
+ void onRestore(long itemId);
+
+ void onPermanentDelete(long itemId);
+ }
+
+ public interface OnContentLongClickListener {
+ void onContentLongClick(long itemId);
+ }
+
+ public interface OnContentClickListener {
+ void onContentClick(long itemId);
+ }
+
+ private boolean isFirstLayout = true;
+
+ public SwipeMenuLayout(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public SwipeMenuLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public SwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ private void init(Context context) {
+ scroller = new OverScroller(context);
+ velocityTracker = VelocityTracker.obtain();
+ menuWidth = (int) (MENU_WIDTH_DP * context.getResources().getDisplayMetrics().density);
+ longPressHandler = new Handler(Looper.getMainLooper());
+ longPressRunnable = () -> {
+ if (!isScrolling && !longPressTriggered) {
+ Log.d(TAG, "Long press triggered via Handler, itemId: " + itemId);
+ longPressTriggered = true;
+ if (contentLongClickListener != null) {
+ contentLongClickListener.onContentLongClick(itemId);
+ }
+ }
+ };
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ if (getChildCount() != 2) {
+ throw new IllegalStateException("SwipeMenuLayout must have exactly 2 children: content and menu");
+ }
+
+ contentView = getChildAt(0);
+ menuView = getChildAt(1);
+
+ setupMenuButtonListeners();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (isFirstLayout && contentView != null && menuView != null) {
+ screenWidth = right - left;
+ Log.d(TAG, "onLayout: screenWidth=" + screenWidth + ", menuWidth=" + menuWidth);
+
+ contentView.setTranslationX(0);
+ menuView.setTranslationX(screenWidth);
+ scroller.startScroll(0, 0, 0, 0);
+
+ isFirstLayout = false;
+ }
+ }
+
+ private void setupMenuButtonListeners() {
+ if (menuView instanceof ViewGroup) {
+ ViewGroup menuGroup = (ViewGroup) menuView;
+ int childCount = menuGroup.getChildCount();
+
+ Log.d(TAG, "setupMenuButtonListeners: menuGroup childCount=" + childCount);
+
+ // menuView 是 FrameLayout,包含两个 include 的布局
+ // 需要遍历每个菜单布局内部的按钮
+ for (int i = 0; i < childCount; i++) {
+ View menuLayout = menuGroup.getChildAt(i);
+ Log.d(TAG, "Menu layout " + i + ": " + menuLayout.getClass().getSimpleName() + ", visibility=" + menuLayout.getVisibility());
+
+ if (menuLayout instanceof ViewGroup) {
+ ViewGroup menuInnerGroup = (ViewGroup) menuLayout;
+ int buttonCount = menuInnerGroup.getChildCount();
+ Log.d(TAG, "Menu " + i + " has " + buttonCount + " buttons");
+
+ for (int j = 0; j < buttonCount; j++) {
+ View button = menuInnerGroup.getChildAt(j);
+ final View finalButton = button;
+ String tag = (String) button.getTag();
+ Log.d(TAG, "Button " + j + ": id=" + button.getId() + ", tag=" + tag);
+
+ button.setOnClickListener(v -> {
+ Log.d(TAG, "Button clicked: id=" + finalButton.getId() + ", tag=" + finalButton.getTag());
+ if (menuButtonClickListener != null) {
+ long itemId = getItemId();
+ Log.d(TAG, "menuButtonClickListener not null, itemId=" + itemId);
+ handleMenuButtonClick(finalButton, itemId);
+ } else {
+ Log.e(TAG, "menuButtonClickListener is NULL!");
+ }
+ closeMenu();
+ });
+ }
+ }
+ }
+ } else {
+ Log.e(TAG, "menuView is not a ViewGroup!");
+ }
+ }
+
+ private void handleMenuButtonClick(View button, long itemId) {
+ int id = button.getId();
+ String actionType = (String) button.getTag();
+ Log.d(TAG, "handleMenuButtonClick: id=" + id + ", actionType=" + actionType + ", itemId=" + itemId);
+ if (actionType != null && menuButtonClickListener != null) {
+ switch (actionType) {
+ case "edit":
+ Log.d(TAG, "Calling onEdit");
+ menuButtonClickListener.onEdit(itemId);
+ break;
+ case "pin":
+ Log.d(TAG, "Calling onPin");
+ menuButtonClickListener.onPin(itemId);
+ break;
+ case "move":
+ Log.d(TAG, "Calling onMove");
+ menuButtonClickListener.onMove(itemId);
+ break;
+ case "delete":
+ Log.d(TAG, "Calling onDelete");
+ menuButtonClickListener.onDelete(itemId);
+ break;
+ case "rename":
+ Log.d(TAG, "Calling onRename");
+ menuButtonClickListener.onRename(itemId);
+ break;
+ case "restore":
+ Log.d(TAG, "Calling onRestore");
+ menuButtonClickListener.onRestore(itemId);
+ break;
+ case "permanent_delete":
+ Log.d(TAG, "Calling onPermanentDelete");
+ menuButtonClickListener.onPermanentDelete(itemId);
+ break;
+ default:
+ Log.e(TAG, "Unknown actionType: " + actionType);
+ }
+ } else {
+ if (actionType == null) {
+ Log.e(TAG, "actionType is NULL!");
+ }
+ if (menuButtonClickListener == null) {
+ Log.e(TAG, "menuButtonClickListener is NULL in handleMenuButtonClick!");
+ }
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!swipeEnabled) {
+ return false;
+ }
+
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downX = ev.getX();
+ downY = ev.getY();
+ lastX = ev.getX();
+ downTime = System.currentTimeMillis();
+ isScrolling = false;
+ longPressTriggered = false;
+
+ // 安排长按检测任务
+ longPressHandler.removeCallbacks(longPressRunnable);
+ longPressHandler.postDelayed(longPressRunnable, LONG_PRESS_TIME_THRESHOLD);
+ break;
+ case MotionEvent.ACTION_MOVE:
+ float deltaX = ev.getX() - downX;
+ float deltaY = ev.getY() - downY;
+
+ // 检测滑动
+ if (Math.abs(deltaX) > TOUCH_SLOP * 2 && Math.abs(deltaX) > Math.abs(deltaY) * 2) {
+ isScrolling = true;
+ longPressHandler.removeCallbacks(longPressRunnable);
+ return true;
+ }
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ // 取消长按任务
+ longPressHandler.removeCallbacks(longPressRunnable);
+ break;
+ }
+ return false;
+ }
+
+ public boolean onTouchEvent(MotionEvent event) {
+ if (velocityTracker == null) {
+ velocityTracker = VelocityTracker.obtain();
+ }
+ velocityTracker.addMovement(event);
+
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ downX = event.getX();
+ downY = event.getY();
+ lastX = event.getX();
+ downTime = System.currentTimeMillis();
+ isScrolling = false;
+ longPressTriggered = false;
+
+ // 安排长按检测任务
+ longPressHandler.removeCallbacks(longPressRunnable);
+ longPressHandler.postDelayed(longPressRunnable, LONG_PRESS_TIME_THRESHOLD);
+ break;
+
+ case MotionEvent.ACTION_MOVE:
+ float dx = event.getX() - lastX;
+ float deltaX = event.getX() - downX;
+ float deltaY = event.getY() - downY;
+
+ if (Math.abs(deltaX) > TOUCH_SLOP * 2 && Math.abs(deltaX) > Math.abs(deltaY) * 2) {
+ // 检测到滑动,取消长按任务
+ isScrolling = true;
+ longPressHandler.removeCallbacks(longPressRunnable);
+ currentScrollX += dx;
+ applyScroll(currentScrollX, true);
+ }
+ lastX = event.getX();
+ break;
+
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ // 取消长按任务
+ longPressHandler.removeCallbacks(longPressRunnable);
+
+ long upTime = System.currentTimeMillis();
+ long duration = upTime - downTime;
+
+ if (isScrolling) {
+ handleTouchRelease();
+ } else if (!longPressTriggered && duration < LONG_PRESS_TIME_THRESHOLD) {
+ // 短按且未触发长按 = 点击
+ Log.d(TAG, "Content click detected, itemId: " + itemId);
+ if (contentClickListener != null) {
+ contentClickListener.onContentClick(itemId);
+ }
+ }
+
+ if (velocityTracker != null) {
+ velocityTracker.recycle();
+ velocityTracker = null;
+ }
+ isScrolling = false;
+ break;
+ }
+ return true;
+ }
+
+ public void openMenu() {
+ scroller = new OverScroller(getContext(), new OvershootInterpolator(0.5f));
+ smoothScrollTo(-menuWidth);
+ currentState = STATE_OPEN;
+ }
+
+ public void closeMenu() {
+ scroller = new OverScroller(getContext(), new OvershootInterpolator(0.5f));
+ smoothScrollTo(0);
+ currentState = STATE_CLOSE;
+ }
+
+ public void toggleMenu() {
+ if (currentState == STATE_OPEN) {
+ closeMenu();
+ } else {
+ openMenu();
+ }
+ }
+
+ private void applyScroll(float scrollX, boolean allowElastic) {
+ if (!allowElastic) {
+ if (scrollX > 0) scrollX = 0;
+ if (scrollX < -menuWidth) scrollX = -menuWidth;
+ } else {
+ if (scrollX > MAX_OVERSCROLL) {
+ scrollX = MAX_OVERSCROLL;
+ } else if (scrollX < -menuWidth - MAX_OVERSCROLL) {
+ scrollX = -menuWidth - MAX_OVERSCROLL;
+ }
+ }
+
+ contentView.setTranslationX(scrollX);
+ menuView.setTranslationX(scrollX + screenWidth);
+ currentScrollX = scrollX;
+ }
+
+ private void handleTouchRelease() {
+ velocityTracker.computeCurrentVelocity(1000);
+ float velocity = velocityTracker.getXVelocity();
+
+ int targetX;
+ if (Math.abs(velocity) > MIN_VELOCITY) {
+ if (velocity > 0) {
+ targetX = 0;
+ } else {
+ targetX = -menuWidth;
+ }
+ } else {
+ if (Math.abs(currentScrollX) < menuWidth / 2) {
+ targetX = 0;
+ } else {
+ targetX = -menuWidth;
+ }
+ }
+
+ smoothScrollTo(targetX);
+ currentState = (targetX == 0) ? STATE_CLOSE : STATE_OPEN;
+ }
+
+ private void smoothScrollTo(int x) {
+ scroller.startScroll((int) currentScrollX, 0, x - (int) currentScrollX, 0, 300);
+ invalidate();
+ postInvalidate();
+ }
+
+ @Override
+ public void computeScroll() {
+ if (scroller.computeScrollOffset()) {
+ currentScrollX = scroller.getCurrX();
+ applyScroll(currentScrollX, false);
+ requestAnimationInvalidation();
+ }
+ }
+
+ private void requestAnimationInvalidation() {
+ post(this::computeScroll);
+ }
+
+ public void setOnMenuButtonClickListener(OnMenuButtonClickListener listener) {
+ this.menuButtonClickListener = listener;
+ }
+
+ public void setOnContentClickListener(OnContentClickListener listener) {
+ this.contentClickListener = listener;
+ }
+
+ public void setOnContentLongClickListener(OnContentLongClickListener listener) {
+ this.contentLongClickListener = listener;
+ }
+
+ public void setItemId(long itemId) {
+ this.itemId = itemId;
+ }
+
+ public long getItemId() {
+ return itemId;
+ }
+
+ public boolean isMenuOpen() {
+ return currentState == STATE_OPEN;
+ }
+
+ public void setSwipeEnabled(boolean enabled) {
+ this.swipeEnabled = enabled;
+ }
+
+ public boolean isSwipeEnabled() {
+ return swipeEnabled;
+ }
+}
diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java
index d6f854b..43a13b4 100644
--- a/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java
+++ b/src/Notesmaster/app/src/main/java/net/micode/notes/viewmodel/NotesListViewModel.java
@@ -703,6 +703,121 @@ public class NotesListViewModel extends ViewModel {
return currentFolderId == Notes.ID_TRASH_FOLER;
}
+ /**
+ * 重命名文件夹
+ *
+ * 重命名指定文件夹,并刷新侧栏
+ *
+ *
+ * @param folderId 文件夹ID
+ * @param newName 新名称
+ */
+ public void renameFolder(long folderId, String newName) {
+ isLoading.postValue(true);
+ errorMessage.postValue(null);
+
+ repository.renameFolder(folderId, newName, new NotesRepository.Callback() {
+ @Override
+ public void onSuccess(Integer rowsAffected) {
+ isLoading.postValue(false);
+ // 触发列表和侧栏刷新
+ refreshNotes();
+ sidebarRefreshNeeded.postValue(true);
+ Log.d(TAG, "Successfully renamed folder: " + folderId);
+ }
+
+ @Override
+ public void onError(Exception error) {
+ isLoading.postValue(false);
+ String message = "重命名文件夹失败: " + error.getMessage();
+ errorMessage.postValue(message);
+ Log.e(TAG, message, error);
+ }
+ });
+ }
+
+ /**
+ * 重命名笔记
+ *
+ * 修改笔记标题,并触发侧栏和列表刷新
+ *
+ *
+ * @param noteId 笔记ID
+ * @param newName 新标题
+ */
+ public void renameNote(long noteId, String newName) {
+ isLoading.postValue(true);
+ errorMessage.postValue(null);
+
+ repository.renameNote(noteId, newName, new NotesRepository.Callback() {
+ @Override
+ public void onSuccess(Integer rowsAffected) {
+ isLoading.postValue(false);
+ // 触发列表和侧栏刷新
+ refreshNotes();
+ sidebarRefreshNeeded.postValue(true);
+ Log.d(TAG, "Successfully renamed note: " + noteId);
+ }
+
+ @Override
+ public void onError(Exception error) {
+ isLoading.postValue(false);
+ String message = "重命名笔记失败: " + error.getMessage();
+ errorMessage.postValue(message);
+ Log.e(TAG, message, error);
+ }
+ });
+ }
+
+ /**
+ * 删除文件夹
+ *
+ * 将文件夹移动到回收站,并刷新侧栏
+ *
+ *
+ * @param folderId 文件夹ID
+ */
+ public void deleteFolder(long folderId) {
+ isLoading.postValue(true);
+ errorMessage.postValue(null);
+
+ repository.deleteFolder(folderId, new NotesRepository.Callback() {
+ @Override
+ public void onSuccess(Integer rowsAffected) {
+ isLoading.postValue(false);
+ // 触发侧栏刷新
+ sidebarRefreshNeeded.postValue(true);
+ Log.d(TAG, "Successfully deleted folder: " + folderId);
+ }
+
+ @Override
+ public void onError(Exception error) {
+ isLoading.postValue(false);
+ String message = "删除文件夹失败: " + error.getMessage();
+ errorMessage.postValue(message);
+ Log.e(TAG, message, error);
+ }
+ });
+ }
+
+ /**
+ * 获取文件夹信息
+ *
+ * 查询单个文件夹的详细信息
+ *
+ *
+ * @param folderId 文件夹ID
+ * @param callback 回调接口
+ */
+ public void getFolderInfo(long folderId, NotesRepository.Callback callback) {
+ try {
+ NotesRepository.NoteInfo folderInfo = repository.getFolderInfo(folderId);
+ callback.onSuccess(folderInfo);
+ } catch (Exception e) {
+ callback.onError(e);
+ }
+ }
+
/**
* ViewModel销毁时的清理
*
diff --git a/src/Notesmaster/app/src/main/res/anim/swipe_menu_close.xml b/src/Notesmaster/app/src/main/res/anim/swipe_menu_close.xml
new file mode 100644
index 0000000..e7bf4ba
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/anim/swipe_menu_close.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/anim/swipe_menu_open.xml b/src/Notesmaster/app/src/main/res/anim/swipe_menu_open.xml
new file mode 100644
index 0000000..1ca1765
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/anim/swipe_menu_open.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/layout/dialog_folder_delete.xml b/src/Notesmaster/app/src/main/res/layout/dialog_folder_delete.xml
new file mode 100644
index 0000000..ac59d7b
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/layout/dialog_folder_delete.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/layout/dialog_folder_name.xml b/src/Notesmaster/app/src/main/res/layout/dialog_folder_name.xml
new file mode 100644
index 0000000..8054c64
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/layout/dialog_folder_name.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/layout/note_item_swipe.xml b/src/Notesmaster/app/src/main/res/layout/note_item_swipe.xml
new file mode 100644
index 0000000..93480c5
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/layout/note_item_swipe.xml
@@ -0,0 +1,133 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/layout/swipe_menu_note.xml b/src/Notesmaster/app/src/main/res/layout/swipe_menu_note.xml
new file mode 100644
index 0000000..b3964c2
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/layout/swipe_menu_note.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/layout/swipe_menu_trash.xml b/src/Notesmaster/app/src/main/res/layout/swipe_menu_trash.xml
new file mode 100644
index 0000000..698f9bc
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/layout/swipe_menu_trash.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
diff --git a/src/Notesmaster/app/src/main/res/menu/folder_context_menu.xml b/src/Notesmaster/app/src/main/res/menu/folder_context_menu.xml
new file mode 100644
index 0000000..6939573
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/menu/folder_context_menu.xml
@@ -0,0 +1,14 @@
+
+
diff --git a/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml b/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml
index 61b9801..89d68d0 100644
--- a/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/Notesmaster/app/src/main/res/values-zh-rCN/strings.xml
@@ -133,4 +133,33 @@
回收站
创建文件夹成功
+
+ 重命名
+ 重命名文件夹
+ 删除文件夹
+ 删除 "%1$s" 及其 %2$d 条笔记?
+ 删除 "%1$s"?
+ 文件夹名称
+ 无效的意图
+ 不支持的意图操作
+ 暂无便签,点击右下角按钮创建
+ 空便签图标
+ 编辑便签
+ 登录
+ 导出
+ 设置
+ 关闭侧边栏
+ 创建文件夹
+ 文件夹已存在
+ 置顶
+ 锁定
+ 确定需要为其上锁?
+ 确定
+ 再想想
+ 确定要删除选中的便签吗?
+ 取消置顶
+ 解锁
+ 恢复
+ 永久删除
+
diff --git a/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml b/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml
index 3c41894..e17bfdd 100644
--- a/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/Notesmaster/app/src/main/res/values-zh-rTW/strings.xml
@@ -121,7 +121,46 @@
設置
取消
- - %1$s 條符合”%2$s“的搜尋結果
+ - %1$s 條符合"%2$s"的搜尋結果
+
+ 我的便籤
+ %d 個便籤
+ 創建文件夾
+ 文件夾名稱
+ 文件夾名稱不能為空
+ 文件夾名稱過長(最多50個字符)
+ 回收站
+ 創建文件夾成功
+
+
+ 重命名
+ 重命名文件夾
+ 刪除文件夾
+ 刪除 "%1$s" 及其 %2$d 條筆記?
+ 刪除 "%1$s"?
+ 文件夾名稱
+ 無效的意圖
+ 不支持的意圖操作
+ 暫無便籤,點擊右下角按鈕創建
+ 空便籤圖標
+ 編輯便籤
+ 登錄
+ 導出
+ 設置
+ 關閉側邊欄
+ 創建文件夾
+ 文件夾已存在
+ 置頂
+ 鎖定
+ 確定需要為其上鎖?
+ 確定
+ 再想想
+ 確定要刪除選中的便籤嗎?
+ 取消置頂
+ 解鎖
+ 恢復
+ 永久刪除
+
diff --git a/src/Notesmaster/app/src/main/res/values/colors_swipe.xml b/src/Notesmaster/app/src/main/res/values/colors_swipe.xml
new file mode 100644
index 0000000..39fa9a5
--- /dev/null
+++ b/src/Notesmaster/app/src/main/res/values/colors_swipe.xml
@@ -0,0 +1,10 @@
+
+
+
+ #2196F3
+ #FF9800
+ #4CAF50
+ #F44336
+ #2196F3
+ #F44336
+
diff --git a/src/Notesmaster/app/src/main/res/values/strings.xml b/src/Notesmaster/app/src/main/res/values/strings.xml
index c6f38f4..7947219 100644
--- a/src/Notesmaster/app/src/main/res/values/strings.xml
+++ b/src/Notesmaster/app/src/main/res/values/strings.xml
@@ -36,19 +36,20 @@
Browse web
Open map
- /MIUI/notes/
- notes_%s.txt
-
- (%d)
- New Folder
- Export text
- Sync
- Cancel syncing
- Settings
- Search
- Delete
- Move to folder
- %d selected
+ /MIUI/notes/
+ notes_%s.txt
+
+ (%d)
+ New Folder
+ Export text
+ Sync
+ Cancel syncing
+ Settings
+ Search
+ Delete
+ Move to folder
+ Rename
+ %d selected
Nothing selected, the operation is invalid
Select all
Deselect all
@@ -59,10 +60,17 @@
Super
Enter check list
Leave check list
- View folder
- Delete folder
- Change folder name
- The folder %1$s exist, please rename
+ View folder
+ Delete folder
+ Change folder name
+ The folder %1$s exist, please rename
+
+
+ Rename folder
+ Delete folder
+ Delete \"%1$s\" and its %2$d notes?
+ Delete \"%1$s\"?
+ Folder name
Share
Send to home
Remind me
@@ -75,15 +83,15 @@
Confirm to delete the selected %d notes?
Confirm to delete this note?
Have moved selected %1$d notes to %2$s folder
-
- SD card busy, not available now
- Export failed, please check SD card
- The note is not exist
- Sorry, can not set clock on empty note
- Sorry, can not send and empty note to home
- Invalid intent
- Unsupported intent action
- Export successful
+
+ SD card busy, not available now
+ Export failed, please check SD card
+ The note is not exist
+ Sorry, can not set clock on empty note
+ Sorry, can not send and empty note to home
+ Invalid intent
+ Unsupported intent action
+ Export successful
Export fail
Export text file (%1$s) to SD (%2$s) directory
@@ -103,7 +111,7 @@
Sync account
Sync notes with google task
Last sync time %1$s
- yyyy-MM-dd hh:mm:ss
+ yyyy-MM-dd hh:mm:ss
Add account
Change sync account
Remove sync account
@@ -161,4 +169,6 @@
Are you sure you want to delete selected notes?
Unpin
Unlock
-
+ Restore
+ Delete Forever
+