实现左滑操作

pull/32/head
包尔俊 4 weeks ago
parent 3a8141722a
commit 2a59a2aa01

1
.gitignore vendored

@ -21,3 +21,4 @@ AGENTS.md
.opencode/
opencode.json
.opencode.backup
log

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>Notes-master-Notesmaster</name>
<comment>Project Notesmaster created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
<filteredResources>
<filter>
<id>1769218588724</id>
<name></name>
<type>30</type>
<matcher>
<id>org.eclipse.core.resources.regexFilterMatcher</id>
<arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
</matcher>
</filter>
</filteredResources>
</projectDescription>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

@ -1,57 +0,0 @@
# ListView迁移至RecyclerView实施方案
## 实施概述
将小米便签的ListView组件迁移至RecyclerView实现性能提升、功能增强和代码可维护性改进。
## 主要工作内容
### 1. 依赖配置
* 修改`build.gradle.kts`添加RecyclerView和CursorAdapter依赖
### 2. 核心类创建
* 创建`NoteViewHolder.java`实现ViewHolder模式
* 创建`NoteItemDecoration.java`:实现分隔线
* 创建`NoteItemTouchCallback.java`:实现手势操作
* 创建`list_divider.xml`:分隔线资源
### 3. 适配器重构
* 重写`NotesListAdapter.java`为`NotesRecyclerViewAdapter`
* 继承`CursorRecyclerViewAdapter`保持Cursor数据源
* 实现ViewHolder模式使用SparseArray优化选中状态
### 4. 布局修改
* 修改`note_list.xml`将ListView替换为RecyclerView
* 修改`NotesListActivity.java`:更新初始化和事件处理逻辑
### 5. 功能增强
* 添加DefaultItemAnimator实现增删改动画
* 实现ItemTouchHelper支持滑动删除和拖拽
* 实现局部刷新机制
## 预期效果
* 内存占用降低30-40%
* 滚动帧率提升至55-60 FPS
* 支持局部刷新和内置动画
* 代码可维护性显著提升
## 实施周期
预计10-15个工作日完成全部迁移和测试。

@ -147,6 +147,22 @@ public class MainActivity extends AppCompatActivity implements SidebarFragment.O
closeSidebar();
}
@Override
public void onRenameFolder(long folderId) {
Log.d(TAG, "Rename folder: " + folderId);
// TODO: 文件夹操作主要由NotesListActivity处理
// 这里可以添加跳转逻辑或通知NotesListActivity
closeSidebar();
}
@Override
public void onDeleteFolder(long folderId) {
Log.d(TAG, "Delete folder: " + folderId);
// TODO: 文件夹操作主要由NotesListActivity处理
// 这里可以添加跳转逻辑或通知NotesListActivity
closeSidebar();
}
// ==================== 私有方法 ====================
/**

@ -70,6 +70,7 @@ public class NotesRepository {
public int bgColorId;
public boolean isPinned; // 新增置顶字段
public boolean isLocked; // 新增锁定字段
public int noteCount; // 新增笔记数量字段
public NoteInfo() {}
@ -131,16 +132,16 @@ public class NotesRepository {
private NoteInfo noteFromCursor(Cursor cursor) {
NoteInfo noteInfo = new NoteInfo();
noteInfo.id = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.ID));
// Read TITLE and SNIPPET
String dbTitle = "";
int titleIndex = cursor.getColumnIndex(NoteColumns.TITLE);
if (titleIndex != -1) {
dbTitle = cursor.getString(titleIndex);
}
noteInfo.snippet = cursor.getString(cursor.getColumnIndexOrThrow(NoteColumns.SNIPPET));
// Prioritize TITLE, fallback to SNIPPET
if (dbTitle != null && !dbTitle.trim().isEmpty()) {
noteInfo.title = dbTitle;
@ -153,7 +154,7 @@ public class NotesRepository {
noteInfo.modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(NoteColumns.MODIFIED_DATE));
noteInfo.type = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.TYPE));
noteInfo.localModified = cursor.getInt(cursor.getColumnIndexOrThrow(NoteColumns.LOCAL_MODIFIED));
int bgColorIdIndex = cursor.getColumnIndex(NoteColumns.BG_COLOR_ID);
if (bgColorIdIndex != -1 && !cursor.isNull(bgColorIdIndex)) {
noteInfo.bgColorId = cursor.getInt(bgColorIdIndex);
@ -172,7 +173,15 @@ public class NotesRepository {
} else {
noteInfo.isLocked = false;
}
// 读取笔记数量
int notesCountIndex = cursor.getColumnIndex(NoteColumns.NOTES_COUNT);
if (notesCountIndex != -1) {
noteInfo.noteCount = cursor.getInt(notesCountIndex);
} else {
noteInfo.noteCount = 0;
}
return noteInfo;
}
@ -1009,6 +1018,173 @@ public class NotesRepository {
: content;
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param newName
* @param callback
*/
public void renameFolder(long folderId, String newName, Callback<Integer> callback) {
executor.execute(() -> {
try {
// 检查是否是系统文件夹(禁止重命名)
if (folderId <= 0) {
callback.onError(new IllegalArgumentException("System folder cannot be renamed"));
return;
}
// 检查名称是否为空
if (newName == null || newName.trim().isEmpty()) {
callback.onError(new IllegalArgumentException("Folder name cannot be empty"));
return;
}
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, newName);
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 renamed folder: " + folderId + " to: " + newName);
} else {
callback.onError(new RuntimeException("No folder found with ID: " + folderId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to rename folder: " + folderId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param noteId ID
* @param newName
* @param callback
*/
public void renameNote(long noteId, String newName, Callback<Integer> callback) {
executor.execute(() -> {
try {
// 检查名称是否为空
if (newName == null || newName.trim().isEmpty()) {
callback.onError(new IllegalArgumentException("Note title cannot be empty"));
return;
}
ContentValues values = new ContentValues();
values.put(NoteColumns.SNIPPET, newName);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId);
int rows = contentResolver.update(uri, values, null, null);
if (rows > 0) {
callback.onSuccess(rows);
Log.d(TAG, "Successfully renamed note: " + noteId + " to: " + newName);
} else {
callback.onError(new RuntimeException("No note found with ID: " + noteId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to rename note: " + noteId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param newParentId ID
* @param callback
*/
public void moveFolder(long folderId, long newParentId, Callback<Integer> callback) {
executor.execute(() -> {
try {
// 检查是否是系统文件夹(禁止移动)
if (folderId <= 0) {
callback.onError(new IllegalArgumentException("System folder cannot be moved"));
return;
}
// 检查目标文件夹是否有效(不能是系统文件夹)
if (newParentId < 0 && newParentId != Notes.ID_TRASH_FOLER) {
callback.onError(new IllegalArgumentException("Invalid target folder"));
return;
}
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, newParentId);
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: " + folderId + " to parent: " + newParentId);
} else {
callback.onError(new RuntimeException("No folder found with ID: " + folderId));
}
} catch (Exception e) {
Log.e(TAG, "Failed to move folder: " + folderId, e);
callback.onError(e);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param callback
*/
public void deleteFolder(long folderId, Callback<Integer> 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
* <p>

@ -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;
/**
*
* <p>
*
* </p>
*/
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() {
}
}
}

@ -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
* <p>
@ -153,7 +225,7 @@ public class NoteInfoAdapter extends BaseAdapter {
public HashSet<Long> 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;
}
}

@ -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<NotesRepository.NoteInfo> 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<NotesRepository.NoteInfo>() {
@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<NotesRepository.NoteInfo>() {
@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();
}
/**
*
* <p>
@ -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<NotesRepository.NoteInfo> 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<NotesRepository.NoteInfo> notes = viewModel.getNotesLiveData().getValue();
if (notes != null) {
for (NotesRepository.NoteInfo note : notes) {
if (note.getId() == itemId) {
showDeleteForeverConfirmDialog(note);
break;
}
}
}
}

@ -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<FolderTreeItem> folderItems;
private FolderListViewModel viewModel;
private OnFolderItemClickListener folderItemClickListener;
private OnFolderItemLongClickListener folderItemLongClickListener;
public FolderTreeAdapter(List<FolderTreeItem> 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
*

@ -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;
/**
*
* <p>
*
* </p>
*/
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;
}
}

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

@ -703,6 +703,121 @@ public class NotesListViewModel extends ViewModel {
return currentFolderId == Notes.ID_TRASH_FOLER;
}
/**
*
* <p>
*
* </p>
*
* @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<Integer>() {
@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);
}
});
}
/**
*
* <p>
*
* </p>
*
* @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<Integer>() {
@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);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
*/
public void deleteFolder(long folderId) {
isLoading.postValue(true);
errorMessage.postValue(null);
repository.deleteFolder(folderId, new NotesRepository.Callback<Integer>() {
@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);
}
});
}
/**
*
* <p>
*
* </p>
*
* @param folderId ID
* @param callback
*/
public void getFolderInfo(long folderId, NotesRepository.Callback<NotesRepository.NoteInfo> callback) {
try {
NotesRepository.NoteInfo folderInfo = repository.getFolderInfo(folderId);
callback.onSuccess(folderInfo);
} catch (Exception e) {
callback.onError(e);
}
}
/**
* ViewModel
* <p>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/accelerate_interpolator">
<translate
android:fromXDelta="0%"
android:toXDelta="100%" />
</set>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="300"
android:interpolator="@android:anim/decelerate_interpolator">
<translate
android:fromXDelta="100%"
android:toXDelta="0%" />
</set>

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tv_delete_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:lineSpacingExtra="4dp" />
</LinearLayout>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<EditText
android:id="@+id/et_folder_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/folder_name_hint"
android:maxLength="50"
android:inputType="text"
android:padding="12dp" />
</LinearLayout>

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<net.micode.notes.ui.SwipeMenuLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- 内容视图原始的note_item内容 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="12dp"
android:background="@null">
<!-- 标题行 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<!-- 类型图标(文件夹/便签) -->
<ImageView
android:id="@+id/iv_type_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginEnd="8dp"
android:visibility="gone" />
<TextView
android:id="@+id/tv_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="16sp"
android:textColor="@android:color/black"
android:textStyle="bold"
android:maxLines="1"
android:ellipsize="end"
android:singleLine="true" />
<ImageView
android:id="@+id/iv_lock_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="4dp"
android:src="@android:drawable/ic_lock_lock"
app:tint="@android:color/darker_gray"
android:visibility="gone" />
<TextView
android:id="@+id/tv_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:textSize="12sp"
android:textColor="@android:color/darker_gray" />
</LinearLayout>
<!-- 文件夹名称 -->
<TextView
android:id="@+id/tv_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textSize="14sp"
android:textColor="@android:color/darker_gray"
android:maxLines="1"
android:ellipsize="end"
android:visibility="gone" />
<!-- 底部控制行:复选框和提醒图标 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<CheckBox
android:id="@android:id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:visibility="gone" />
<ImageView
android:id="@+id/iv_alert_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:visibility="gone" />
<!-- 填充空间 -->
<View
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<ImageView
android:id="@+id/iv_pinned_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_menu_upload"
app:tint="@android:color/darker_gray"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<!-- 操作按钮视图(包含普通和回收站两个菜单) -->
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<!-- 普通模式菜单 -->
<include
android:id="@+id/swipe_menu_normal"
android:layout_width="wrap_content"
android:layout_height="match_parent"
layout="@layout/swipe_menu_note"
android:visibility="gone" />
<!-- 回收站模式菜单 -->
<include
android:id="@+id/swipe_menu_trash"
android:layout_width="wrap_content"
android:layout_height="match_parent"
layout="@layout/swipe_menu_trash"
android:visibility="gone" />
</FrameLayout>
</net.micode.notes.ui.SwipeMenuLayout>

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/btn_edit"
android:tag="edit"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="@color/swipe_edit_bg"
android:gravity="center"
android:text="@string/menu_rename"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_pin"
android:tag="pin"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="@color/swipe_pin_bg"
android:gravity="center"
android:text="@string/menu_pin"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_move"
android:tag="move"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="@color/swipe_move_bg"
android:gravity="center"
android:text="@string/menu_move"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_delete"
android:tag="delete"
android:layout_width="60dp"
android:layout_height="match_parent"
android:background="@color/swipe_delete_bg"
android:gravity="center"
android:text="@string/menu_delete"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/btn_restore"
android:tag="restore"
android:layout_width="120dp"
android:layout_height="match_parent"
android:background="@color/swipe_restore_bg"
android:gravity="center"
android:text="@string/menu_restore"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/btn_permanent_delete"
android:tag="permanent_delete"
android:layout_width="120dp"
android:layout_height="match_parent"
android:background="@color/swipe_permanent_bg"
android:gravity="center"
android:text="@string/menu_permanent_delete"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/action_rename"
android:title="@string/menu_rename" />
<item
android:id="@+id/action_move"
android:title="@string/menu_move" />
<item
android:id="@+id/action_delete"
android:title="@string/menu_delete" />
</menu>

@ -133,4 +133,33 @@
<string name="menu_trash">回收站</string>
<string name="create_folder_success">创建文件夹成功</string>
<!-- New translations for missing strings -->
<string name="menu_rename">重命名</string>
<string name="dialog_rename_folder_title">重命名文件夹</string>
<string name="dialog_delete_folder_title">删除文件夹</string>
<string name="dialog_delete_folder_with_notes">删除 "%1$s" 及其 %2$d 条笔记?</string>
<string name="dialog_delete_folder_empty">删除 "%1$s"?</string>
<string name="folder_name_hint">文件夹名称</string>
<string name="error_intent_invalid">无效的意图</string>
<string name="error_intent_unsupported">不支持的意图操作</string>
<string name="empty_notes_hint">暂无便签,点击右下角按钮创建</string>
<string name="empty_notes_icon">空便签图标</string>
<string name="menu_edit_note">编辑便签</string>
<string name="menu_login">登录</string>
<string name="menu_export">导出</string>
<string name="menu_settings">设置</string>
<string name="sidebar_close">关闭侧边栏</string>
<string name="sidebar_create_folder">创建文件夹</string>
<string name="error_folder_name_exists">文件夹已存在</string>
<string name="menu_pin">置顶</string>
<string name="menu_lock">锁定</string>
<string name="lock_confirmation">确定需要为其上锁?</string>
<string name="lock_confirm_button">确定</string>
<string name="lock_cancel_button">再想想</string>
<string name="delete_confirmation">确定要删除选中的便签吗?</string>
<string name="menu_unpin">取消置顶</string>
<string name="menu_unlock">解锁</string>
<string name="menu_restore">恢复</string>
<string name="menu_permanent_delete">永久删除</string>
</resources>

@ -121,7 +121,46 @@
<string name="datetime_dialog_ok">設置</string>
<string name="datetime_dialog_cancel">取消</string>
<plurals name="search_results_title">
<item quantity="other"><xliff:g id="NUMBER">%1$s</xliff:g> 條符合<xliff:g id="SEARCH">%2$s</xliff:g>的搜尋結果</item>
<item quantity="other"><xliff:g id="NUMBER">%1$s</xliff:g> 條符合"<xliff:g id="SEARCH">%2$s</xliff:g>"的搜尋結果</item>
</plurals>
<!-- Sidebar -->
<string name="root_folder_name">我的便籤</string>
<string name="folder_note_count">%d 個便籤</string>
<string name="dialog_create_folder_title">創建文件夾</string>
<string name="dialog_create_folder_hint">文件夾名稱</string>
<string name="error_folder_name_empty">文件夾名稱不能為空</string>
<string name="error_folder_name_too_long">文件夾名稱過長最多50個字符</string>
<string name="menu_trash">回收站</string>
<string name="create_folder_success">創建文件夾成功</string>
<!-- New translations for missing strings -->
<string name="menu_rename">重命名</string>
<string name="dialog_rename_folder_title">重命名文件夾</string>
<string name="dialog_delete_folder_title">刪除文件夾</string>
<string name="dialog_delete_folder_with_notes">刪除 "%1$s" 及其 %2$d 條筆記?</string>
<string name="dialog_delete_folder_empty">刪除 "%1$s"?</string>
<string name="folder_name_hint">文件夾名稱</string>
<string name="error_intent_invalid">無效的意圖</string>
<string name="error_intent_unsupported">不支持的意圖操作</string>
<string name="empty_notes_hint">暫無便籤,點擊右下角按鈕創建</string>
<string name="empty_notes_icon">空便籤圖標</string>
<string name="menu_edit_note">編輯便籤</string>
<string name="menu_login">登錄</string>
<string name="menu_export">導出</string>
<string name="menu_settings">設置</string>
<string name="sidebar_close">關閉側邊欄</string>
<string name="sidebar_create_folder">創建文件夾</string>
<string name="error_folder_name_exists">文件夾已存在</string>
<string name="menu_pin">置頂</string>
<string name="menu_lock">鎖定</string>
<string name="lock_confirmation">確定需要為其上鎖?</string>
<string name="lock_confirm_button">確定</string>
<string name="lock_cancel_button">再想想</string>
<string name="delete_confirmation">確定要刪除選中的便籤嗎?</string>
<string name="menu_unpin">取消置頂</string>
<string name="menu_unlock">解鎖</string>
<string name="menu_restore">恢復</string>
<string name="menu_permanent_delete">永久刪除</string>
</resources>

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 滑动按钮颜色 -->
<color name="swipe_edit_bg">#2196F3</color> <!-- 蓝色 -->
<color name="swipe_pin_bg">#FF9800</color> <!-- 橙色 -->
<color name="swipe_move_bg">#4CAF50</color> <!-- 绿色 -->
<color name="swipe_delete_bg">#F44336</color> <!-- 红色 -->
<color name="swipe_restore_bg">#2196F3</color> <!-- 蓝色 -->
<color name="swipe_permanent_bg">#F44336</color> <!-- 红色 -->
</resources>

@ -36,19 +36,20 @@
<string name="note_link_web">Browse web</string>
<string name="note_link_other">Open map</string>
<!-- Text export file information -->
<string name="file_path">/MIUI/notes/</string>
<string name="file_name_txt_format">notes_%s.txt</string>
<!-- notes list string -->
<string name="format_folder_files_count">(%d)</string>
<string name="menu_create_folder">New Folder</string>
<string name="menu_export_text">Export text</string>
<string name="menu_sync">Sync</string>
<string name="menu_sync_cancel">Cancel syncing</string>
<string name="menu_setting">Settings</string>
<string name="menu_search">Search</string>
<string name="menu_delete">Delete</string>
<string name="menu_move">Move to folder</string>
<string name="menu_select_title">%d selected</string>
<string name="file_path" translatable="false">/MIUI/notes/</string>
<string name="file_name_txt_format" translatable="false">notes_%s.txt</string>
<!-- notes list string -->
<string name="format_folder_files_count" translatable="false">(%d)</string>
<string name="menu_create_folder">New Folder</string>
<string name="menu_export_text">Export text</string>
<string name="menu_sync">Sync</string>
<string name="menu_sync_cancel">Cancel syncing</string>
<string name="menu_setting">Settings</string>
<string name="menu_search">Search</string>
<string name="menu_delete">Delete</string>
<string name="menu_move">Move to folder</string>
<string name="menu_rename">Rename</string>
<string name="menu_select_title">%d selected</string>
<string name="menu_select_none">Nothing selected, the operation is invalid</string>
<string name="menu_select_all">Select all</string>
<string name="menu_deselect_all">Deselect all</string>
@ -59,10 +60,17 @@
<string name="menu_font_super">Super</string>
<string name="menu_list_mode">Enter check list</string>
<string name="menu_normal_mode">Leave check list</string>
<string name="menu_folder_view">View folder</string>
<string name="menu_folder_delete">Delete folder</string>
<string name="menu_folder_change_name">Change folder name</string>
<string name="folder_exist">The folder %1$s exist, please rename</string>
<string name="menu_folder_view">View folder</string>
<string name="menu_folder_delete">Delete folder</string>
<string name="menu_folder_change_name">Change folder name</string>
<string name="folder_exist">The folder %1$s exist, please rename</string>
<!-- Folder operation dialogs -->
<string name="dialog_rename_folder_title">Rename folder</string>
<string name="dialog_delete_folder_title">Delete folder</string>
<string name="dialog_delete_folder_with_notes">Delete \"%1$s\" and its %2$d notes?</string>
<string name="dialog_delete_folder_empty">Delete \"%1$s\"?</string>
<string name="folder_name_hint">Folder name</string>
<string name="menu_share">Share</string>
<string name="menu_send_to_desktop">Send to home</string>
<string name="menu_alert">Remind me</string>
@ -75,15 +83,15 @@
<string name="alert_message_delete_notes">Confirm to delete the selected %d notes?</string>
<string name="alert_message_delete_note">Confirm to delete this note?</string>
<string name="format_move_notes_to_folder">Have moved selected %1$d notes to %2$s folder</string>
<!-- Error information -->
<string name="error_sdcard_unmounted">SD card busy, not available now</string>
<string name="error_sdcard_export">Export failed, please check SD card</string>
<string name="error_note_not_exist">The note is not exist</string>
<string name="error_note_empty_for_clock">Sorry, can not set clock on empty note</string>
<string name="error_note_empty_for_send_to_desktop">Sorry, can not send and empty note to home</string>
<string name="error_intent_invalid">Invalid intent</string>
<string name="error_intent_unsupported">Unsupported intent action</string>
<string name="success_sdcard_export">Export successful</string>
<!-- Error information -->
<string name="error_sdcard_unmounted">SD card busy, not available now</string>
<string name="error_sdcard_export">Export failed, please check SD card</string>
<string name="error_note_not_exist">The note is not exist</string>
<string name="error_note_empty_for_clock">Sorry, can not set clock on empty note</string>
<string name="error_note_empty_for_send_to_desktop">Sorry, can not send and empty note to home</string>
<string name="error_intent_invalid">Invalid intent</string>
<string name="error_intent_unsupported">Unsupported intent action</string>
<string name="success_sdcard_export">Export successful</string>
<string name="failed_sdcard_export">Export fail</string>
<string name="format_exported_file_location">Export text file (%1$s) to SD (%2$s) directory</string>
<!-- Sync -->
@ -103,7 +111,7 @@
<string name="preferences_account_title">Sync account</string>
<string name="preferences_account_summary">Sync notes with google task</string>
<string name="preferences_last_sync_time">Last sync time %1$s</string>
<string name="preferences_last_sync_time_format">yyyy-MM-dd hh:mm:ss</string>
<string name="preferences_last_sync_time_format" translatable="false">yyyy-MM-dd hh:mm:ss</string>
<string name="preferences_add_account">Add account</string>
<string name="preferences_menu_change_account">Change sync account</string>
<string name="preferences_menu_remove_account">Remove sync account</string>
@ -161,4 +169,6 @@
<string name="delete_confirmation">Are you sure you want to delete selected notes?</string>
<string name="menu_unpin">Unpin</string>
<string name="menu_unlock">Unlock</string>
</resources>
<string name="menu_restore">Restore</string>
<string name="menu_permanent_delete">Delete Forever</string>
</resources>

Loading…
Cancel
Save