diff --git a/src/Notesmaster/app/src/main/AndroidManifest.xml b/src/Notesmaster/app/src/main/AndroidManifest.xml index a08a1f3..a29be50 100644 --- a/src/Notesmaster/app/src/main/AndroidManifest.xml +++ b/src/Notesmaster/app/src/main/AndroidManifest.xml @@ -197,6 +197,13 @@ android:theme="@style/Theme.Notesmaster" > + + --> + + + + + + + + + + + + + + + + + + + + > callback) { executor.execute(() -> { try { - List notes = queryNotes(folderId); + // Modified to only return notes (no folders) as per new UI requirement + List notes = queryNotesOnly(folderId); callback.onSuccess(notes); Log.d(TAG, "Successfully loaded notes for folder: " + folderId); } catch (Exception e) { @@ -214,10 +233,108 @@ public class NotesRepository { } /** - * 查询笔记列表(内部方法) - *

- * 同时返回文件夹和便签,文件夹显示在便签之前 - *

+ * 获取指定文件夹下的子文件夹 + * + * @param folderId 父文件夹ID + * @param callback 回调接口 + */ + public void getSubFolders(long folderId, Callback> callback) { + executor.execute(() -> { + try { + List folders = querySubFolders(folderId); + callback.onSuccess(folders); + Log.d(TAG, "Successfully loaded sub-folders for folder: " + folderId); + } catch (Exception e) { + Log.e(TAG, "Failed to load sub-folders for folder: " + folderId, e); + callback.onError(e); + } + }); + } + + private List querySubFolders(long folderId) { + List folders = new ArrayList<>(); + String selection; + String[] selectionArgs; + + if (folderId == Notes.ID_ROOT_FOLDER) { + selection = "(" + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER + " AND " + NoteColumns.PARENT_ID + "=?) OR (" + + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)"; + selectionArgs = new String[]{String.valueOf(Notes.ID_ROOT_FOLDER)}; + } else { + selection = NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_FOLDER; + selectionArgs = new String[]{String.valueOf(folderId)}; + } + + Cursor cursor = contentResolver.query( + Notes.CONTENT_NOTE_URI, + null, + selection, + selectionArgs, + NoteColumns.MODIFIED_DATE + " DESC" + ); + + if (cursor != null) { + try { + while (cursor.moveToNext()) { + folders.add(noteFromCursor(cursor)); + } + } finally { + cursor.close(); + } + } + return folders; + } + + private List queryNotesOnly(long folderId) { + List normalNotes = new ArrayList<>(); + String selection; + String[] selectionArgs; + + if (folderId == Notes.ID_ALL_NOTES_FOLDER) { + // Query ALL notes (except trash and system folders) + // We want all notes where TYPE=NOTE and PARENT_ID != TRASH + selection = NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER; + selectionArgs = null; + } else if (folderId == Notes.ID_ROOT_FOLDER) { + selection = "(" + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "=?)"; + selectionArgs = new String[]{String.valueOf(Notes.ID_ROOT_FOLDER)}; + } else { + selection = NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + selectionArgs = new String[]{String.valueOf(folderId)}; + } + + Cursor cursor = contentResolver.query( + Notes.CONTENT_NOTE_URI, + null, + selection, + selectionArgs, + NoteColumns.MODIFIED_DATE + " DESC" + ); + + if (cursor != null) { + try { + while (cursor.moveToNext()) { + normalNotes.add(noteFromCursor(cursor)); + } + } finally { + cursor.close(); + } + } + + // Sort: Pinned first, then Modified Date DESC + normalNotes.sort((a, b) -> { + if (a.isPinned != b.isPinned) { + return a.isPinned ? -1 : 1; + } + return Long.compare(b.modifiedDate, a.modifiedDate); + }); + + return normalNotes; + } + + /** + * 查询笔记列表(内部方法) - Deprecated logic kept for reference but unused by main flow now * * @param folderId 文件夹ID * @return 笔记列表(包含文件夹和便签) @@ -589,7 +706,7 @@ public class NotesRepository { /** * 删除笔记 *

- * 将笔记移动到回收站文件夹 + * 将笔记移动到回收站文件夹,并保存原始位置 *

* * @param noteId 笔记ID @@ -598,8 +715,12 @@ public class NotesRepository { public void deleteNote(long noteId, Callback callback) { executor.execute(() -> { try { + // 1. 获取当前父文件夹ID + long currentParentId = getParentFolderId(noteId); + ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.ORIGIN_PARENT_ID, currentParentId); values.put(NoteColumns.LOCAL_MODIFIED, 1); Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); @@ -607,7 +728,7 @@ public class NotesRepository { if (rows > 0) { callback.onSuccess(rows); - Log.d(TAG, "Successfully moved note to trash: " + noteId); + Log.d(TAG, "Successfully moved note to trash: " + noteId + ", origin: " + currentParentId); } else { callback.onError(new RuntimeException("No note found with ID: " + noteId)); } @@ -634,8 +755,12 @@ public class NotesRepository { int totalRows = 0; for (Long noteId : noteIds) { + // 1. 获取当前父文件夹ID + long currentParentId = getParentFolderId(noteId); + ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, Notes.ID_TRASH_FOLER); + values.put(NoteColumns.ORIGIN_PARENT_ID, currentParentId); values.put(NoteColumns.LOCAL_MODIFIED, 1); Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); @@ -657,7 +782,7 @@ public class NotesRepository { } /** - * 恢复笔记(从回收站移回根目录) + * 恢复笔记(从回收站移回原始位置或根目录) * * @param noteIds 笔记ID列表 * @param callback 回调接口 @@ -671,12 +796,40 @@ public class NotesRepository { } int totalRows = 0; - // 恢复到根目录 - long targetFolderId = Notes.ID_ROOT_FOLDER; for (Long noteId : noteIds) { + // 1. 查询原始父文件夹ID + long originParentId = Notes.ID_ROOT_FOLDER; + Cursor cursor = contentResolver.query( + ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), + new String[]{NoteColumns.ORIGIN_PARENT_ID}, + null, + null, + null + ); + + if (cursor != null) { + try { + if (cursor.moveToFirst()) { + int idx = cursor.getColumnIndex(NoteColumns.ORIGIN_PARENT_ID); + if (idx != -1) { + originParentId = cursor.getLong(idx); + } + } + } finally { + cursor.close(); + } + } + + // 如果原始位置无效(例如0或仍是回收站),则恢复到根目录 + if (originParentId == Notes.ID_TRASH_FOLER || originParentId == 0) { + originParentId = Notes.ID_ROOT_FOLDER; + } + + // 2. 恢复 ContentValues values = new ContentValues(); - values.put(NoteColumns.PARENT_ID, targetFolderId); + values.put(NoteColumns.PARENT_ID, originParentId); + values.put(NoteColumns.ORIGIN_PARENT_ID, 0); // Reset origin values.put(NoteColumns.LOCAL_MODIFIED, 1); Uri uri = ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId); @@ -1004,6 +1157,36 @@ public class NotesRepository { }); } + /** + * 解锁所有笔记 + *

+ * 将所有笔记的锁定状态重置为未锁定 + *

+ * + * @param callback 回调接口 + */ + public void unlockAllNotes(Callback callback) { + executor.execute(() -> { + try { + ContentValues values = new ContentValues(); + values.put(NoteColumns.LOCKED, 0); + values.put(NoteColumns.LOCAL_MODIFIED, 1); + + // Update all notes where locked is 1 + String selection = NoteColumns.LOCKED + " = ?"; + String[] selectionArgs = new String[]{"1"}; + + int rows = contentResolver.update(Notes.CONTENT_NOTE_URI, values, selection, selectionArgs); + + callback.onSuccess(rows); + Log.d(TAG, "Successfully unlocked " + rows + " notes"); + } catch (Exception e) { + Log.e(TAG, "Failed to unlock all notes", e); + callback.onError(e); + } + }); + } + /** * 从内容中提取摘要 * @@ -1014,10 +1197,22 @@ public class NotesRepository { if (content == null || content.isEmpty()) { return ""; } + + // Convert HTML to plain text to remove tags and resolve entities + String plainText; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + plainText = android.text.Html.fromHtml(content, android.text.Html.FROM_HTML_MODE_LEGACY).toString(); + } else { + plainText = android.text.Html.fromHtml(content).toString(); + } + + // Remove extra whitespace + plainText = plainText.trim(); + int maxLength = 100; - return content.length() > maxLength - ? content.substring(0, maxLength) - : content; + return plainText.length() > maxLength + ? plainText.substring(0, maxLength) + : plainText; } /** 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..ad837c4 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 @@ -1,37 +1,22 @@ -/* - * 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.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.BaseAdapter; import android.widget.CheckBox; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + import net.micode.notes.R; 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 android.util.Log; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -40,143 +25,68 @@ import java.util.HashSet; import java.util.List; import java.util.Locale; -/** - * 便签列表适配器 - *

- * 将 List 数据绑定到 ListView - * 支持便签显示、选中状态、图标显示 - *

- *

- * 现代化实现:使用 ViewHolder 模式优化性能 - *

- */ -public class NoteInfoAdapter extends BaseAdapter { - private LayoutInflater inflater; +public class NoteInfoAdapter extends RecyclerView.Adapter { + + private Context context; private List notes; private HashSet selectedIds; + private boolean isSelectionMode; private OnNoteButtonClickListener buttonClickListener; private OnNoteItemClickListener itemClickListener; private OnNoteItemLongClickListener itemLongClickListener; - - /** - * 便签按钮点击事件回调接口 - */ + public interface OnNoteButtonClickListener { - /** - * 编辑按钮点击事件 - * - * @param position 位置 - * @param noteId 便签 ID - */ void onEditButtonClick(int position, long noteId); } - /** - * 便签项点击事件回调接口 - */ public interface OnNoteItemClickListener { void onNoteItemClick(int position, long noteId); } - /** - * 便签项长按事件回调接口 - */ public interface OnNoteItemLongClickListener { void onNoteItemLongClick(int position, long noteId); } - - /** - * 构造函数 - * - * @param context 上下文 - */ + public NoteInfoAdapter(Context context) { - this.inflater = LayoutInflater.from(context); + this.context = context; this.notes = new ArrayList<>(); this.selectedIds = new HashSet<>(); + this.isSelectionMode = false; } - - /** - * 设置便签列表 - * - * @param notes 便签列表 - */ + + public void setSelectionMode(boolean isSelectionMode) { + this.isSelectionMode = isSelectionMode; + notifyDataSetChanged(); + } + public void setNotes(List notes) { this.notes = notes != null ? notes : new ArrayList<>(); notifyDataSetChanged(); } - /** - * 设置选中的便签 ID 集合 - *

- * 用于多选模式同步,让 ViewModel 更新 selectedNoteIds 后, - * Adapter 的 selectedIds 也能同步更新 - *

- * - * @param selectedIds 选中的便签 ID 集合 - */ public void setSelectedIds(HashSet selectedIds) { - if (selectedIds != null && selectedIds != this.selectedIds) { - this.selectedIds.clear(); - this.selectedIds.addAll(selectedIds); - notifyDataSetChanged(); - } else if (selectedIds == null) { - this.selectedIds.clear(); - notifyDataSetChanged(); - } + this.selectedIds = selectedIds != null ? new HashSet<>(selectedIds) : new HashSet<>(); + notifyDataSetChanged(); } - /** - * 设置选中的便签 ID 列表 - *

- * 重载方法,接受 List 参数,在内部转换为 HashSet - *

- * - * @param selectedIds 选中的便签 ID 列表 - */ - public void setSelectedIds(List selectedIds) { - if (selectedIds != null && !selectedIds.isEmpty()) { - this.selectedIds.clear(); - this.selectedIds.addAll(selectedIds); - notifyDataSetChanged(); - } else { - this.selectedIds.clear(); - notifyDataSetChanged(); - } + @NonNull + @Override + public NoteViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(context).inflate(R.layout.note_item, parent, false); + return new NoteViewHolder(view); } - /** - * 获取选中的便签 ID - * - * @return 选中的便签 ID 集合 - */ - public HashSet getSelectedIds() { - return selectedIds; + @Override + public void onBindViewHolder(@NonNull NoteViewHolder holder, int position) { + NotesRepository.NoteInfo note = notes.get(position); + holder.bind(note, position); } - - /** - * 切换选中状态 - * - * @param noteId 便签 ID - */ - public void toggleSelection(long noteId) { - if (selectedIds.contains(noteId)) { - selectedIds.remove(noteId); - } else { - selectedIds.add(noteId); - } - notifyDataSetChanged(); + + @Override + public int getItemCount() { + return notes.size(); } - /** - * 设置按钮点击监听器 - * - * @param listener 监听器 - */ - public void setOnNoteButtonClickListener(OnNoteButtonClickListener listener) { - this.buttonClickListener = listener; - } - public void setOnNoteItemClickListener(OnNoteItemClickListener listener) { this.itemClickListener = listener; } @@ -184,168 +94,113 @@ 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; - } - - @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); - holder = new ViewHolder(); - 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); - 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()); + + class NoteViewHolder extends RecyclerView.ViewHolder { + CardView cardView; + TextView title, time, name; + ImageView typeIcon, lockIcon, alertIcon, pinnedIcon; + CheckBox checkBox; + + public NoteViewHolder(View itemView) { + super(itemView); + cardView = (CardView) itemView; + title = itemView.findViewById(R.id.tv_title); + time = itemView.findViewById(R.id.tv_time); + name = itemView.findViewById(R.id.tv_name); + checkBox = itemView.findViewById(android.R.id.checkbox); + typeIcon = itemView.findViewById(R.id.iv_type_icon); + lockIcon = itemView.findViewById(R.id.iv_lock_icon); + alertIcon = itemView.findViewById(R.id.iv_alert_icon); + pinnedIcon = itemView.findViewById(R.id.iv_pinned_icon); + + itemView.setOnClickListener(v -> { + if (itemClickListener != null) { + int pos = getAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) { + // Pass click event to Fragment, which will handle selection toggle if in selection mode + itemClickListener.onNoteItemClick(pos, notes.get(pos).getId()); } } - 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!"); + + itemView.setOnLongClickListener(v -> { + if (itemLongClickListener != null) { + int pos = getAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) { + itemLongClickListener.onNoteItemLongClick(pos, notes.get(pos).getId()); + return true; + } } - Log.d("NoteInfoAdapter", "===== setOnLongClickListener END ====="); - return true; + return false; }); - } else { - holder = (ViewHolder) convertView.getTag(); } - - holder.position = position; - - NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) getItem(position); - if (note != null) { - String title = note.title; - if (title == null || title.trim().isEmpty()) { - title = "无标题"; - } - holder.title.setText(title); + + public void bind(NotesRepository.NoteInfo note, int position) { + String titleStr = note.title; + String snippet = note.snippet; - // 设置类型图标和时间显示 - if (note.type == Notes.TYPE_FOLDER) { - // 文件夹 - holder.typeIcon.setVisibility(View.VISIBLE); - holder.typeIcon.setImageResource(R.drawable.ic_folder); - // 文件夹不显示时间 - holder.time.setVisibility(View.GONE); - } else { - // 便签 - holder.typeIcon.setVisibility(View.GONE); - holder.time.setVisibility(View.VISIBLE); - holder.time.setText(formatDate(note.modifiedDate)); + if (titleStr == null || titleStr.trim().isEmpty()) { + titleStr = snippet; + if (titleStr != null && titleStr.contains("\n")) { + titleStr = titleStr.substring(0, titleStr.indexOf("\n")); + } + if (titleStr == null || titleStr.trim().isEmpty()) { + titleStr = "无标题"; + } } - int bgResId; - int totalCount = getCount(); - int bgColorId = note.bgColorId; - - if (totalCount == 1) { - bgResId = NoteItemBgResources.getNoteBgSingleRes(bgColorId); - } else if (position == 0) { - bgResId = NoteItemBgResources.getNoteBgFirstRes(bgColorId); - } else if (position == totalCount - 1) { - bgResId = NoteItemBgResources.getNoteBgLastRes(bgColorId); - } else { - bgResId = NoteItemBgResources.getNoteBgNormalRes(bgColorId); + // Bind Summary + TextView summaryView = itemView.findViewById(R.id.tv_summary); + if (summaryView != null) { + // Clean up snippet for display (in case it contains HTML or entities) + String cleanSnippet = snippet; + if (cleanSnippet != null && !cleanSnippet.isEmpty()) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + cleanSnippet = android.text.Html.fromHtml(cleanSnippet, android.text.Html.FROM_HTML_MODE_LEGACY).toString(); + } else { + cleanSnippet = android.text.Html.fromHtml(cleanSnippet).toString(); + } + cleanSnippet = cleanSnippet.trim(); + } + summaryView.setText(cleanSnippet); } - - convertView.setBackgroundResource(bgResId); - if (selectedIds.contains(note.getId())) { - convertView.setActivated(true); + if (note.type == Notes.TYPE_FOLDER) { + title.setText(note.getSnippet() + " (" + note.getNotesCount() + ")"); + time.setVisibility(View.GONE); + typeIcon.setVisibility(View.VISIBLE); + typeIcon.setImageResource(R.drawable.ic_folder); + cardView.setCardBackgroundColor(context.getColor(R.color.bg_white)); } else { - convertView.setActivated(false); + typeIcon.setVisibility(View.GONE); + time.setVisibility(View.VISIBLE); + title.setText(titleStr); + time.setText(formatDate(note.modifiedDate)); + + // Color Logic + int bgColorId = note.bgColorId; + int color = ResourceParser.getNoteBgColor(context, bgColorId); + cardView.setCardBackgroundColor(color); } - Log.d("NoteInfoAdapter", "===== Setting checkbox visibility ====="); - Log.d("NoteInfoAdapter", "selectedIds.isEmpty(): " + selectedIds.isEmpty()); - Log.d("NoteInfoAdapter", "selectedIds.size(): " + selectedIds.size()); - Log.d("NoteInfoAdapter", "selectedIds contains note " + note.getId() + ": " + selectedIds.contains(note.getId())); - - if (!selectedIds.isEmpty()) { - Log.d("NoteInfoAdapter", "Setting checkbox VISIBLE"); - holder.checkBox.setVisibility(View.VISIBLE); - holder.checkBox.setChecked(selectedIds.contains(note.getId())); - holder.checkBox.setClickable(false); + // Selection Logic + if (isSelectionMode) { + checkBox.setVisibility(View.VISIBLE); + checkBox.setChecked(selectedIds.contains(note.getId())); } else { - Log.d("NoteInfoAdapter", "Setting checkbox GONE"); - holder.checkBox.setVisibility(View.GONE); + checkBox.setVisibility(View.GONE); } - Log.d("NoteInfoAdapter", "===== Checkbox visibility set ====="); - if (note.isPinned) { - holder.pinnedIcon.setVisibility(View.VISIBLE); - } else { - holder.pinnedIcon.setVisibility(View.GONE); - } + if (note.isPinned) pinnedIcon.setVisibility(View.VISIBLE); + else pinnedIcon.setVisibility(View.GONE); - if (note.isLocked) { - holder.lockIcon.setVisibility(View.VISIBLE); - } else { - holder.lockIcon.setVisibility(View.GONE); - } + if (note.isLocked) lockIcon.setVisibility(View.VISIBLE); + else lockIcon.setVisibility(View.GONE); } - - return convertView; } - - /** - * 格式化日期 - * - * @param timestamp 时间戳 - * @return 格式化后的日期字符串 - */ + private String formatDate(long timestamp) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); return sdf.format(new Date(timestamp)); } - - /** - * ViewHolder 模式:优化 ListView 性能 - */ - private static class ViewHolder { - TextView title; - TextView time; - ImageView typeIcon; - ImageView lockIcon; - CheckBox checkBox; - ImageView pinnedIcon; - int position; - } -} +} \ No newline at end of file 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 429f4b8..386f83b 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 @@ -1,1058 +1,418 @@ -/* - * Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net) - * - * 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.appwidget.AppWidgetManager; -import android.content.Context; -import android.content.DialogInterface; -import android.content.Intent; import android.os.Bundle; -import android.text.InputFilter; -import android.text.TextUtils; -import android.util.Log; -import androidx.appcompat.view.ActionMode; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; -import android.view.WindowManager; -import android.widget.AdapterView; -import android.widget.Button; -import android.widget.EditText; -import android.widget.LinearLayout; -import android.widget.ListView; -import android.widget.PopupMenu; +import android.widget.ImageView; import android.widget.TextView; -import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.view.ViewCompat; -import androidx.core.view.WindowCompat; +import androidx.core.view.GravityCompat; import androidx.drawerlayout.widget.DrawerLayout; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModel; +import androidx.fragment.app.Fragment; import androidx.lifecycle.ViewModelProvider; - +import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.bottomnavigation.BottomNavigationView; +import android.app.AlertDialog; +import androidx.recyclerview.widget.RecyclerView; import net.micode.notes.R; import net.micode.notes.data.Notes; import net.micode.notes.data.NotesRepository; -import net.micode.notes.databinding.NoteListBinding; -import net.micode.notes.tool.SecurityManager; -import net.micode.notes.ui.NoteInfoAdapter; import net.micode.notes.viewmodel.NotesListViewModel; +import net.micode.notes.tool.SecurityManager; +import android.content.Intent; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import java.util.List; +public class NotesListActivity extends AppCompatActivity implements SidebarFragment.OnSidebarItemSelectedListener { -/** - * 笔记列表Activity(重构版) - *

- * 仅负责UI展示和用户交互,业务逻辑委托给ViewModel - * 符合MVVM架构模式 - *

- *

- * 相比原版(1305行),重构后代码量减少约70% - *

- * - * @see NotesListViewModel - * @see NotesRepository - */ -public class NotesListActivity extends AppCompatActivity - implements NoteInfoAdapter.OnNoteButtonClickListener, - NoteInfoAdapter.OnNoteItemClickListener, - NoteInfoAdapter.OnNoteItemLongClickListener, - SidebarFragment.OnSidebarItemSelectedListener { private static final String TAG = "NotesListActivity"; - private static final int REQUEST_CODE_OPEN_NODE = 102; - private static final int REQUEST_CODE_NEW_NODE = 103; - private static final int REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN = 104; - private static final int REQUEST_CODE_CHECK_PASSWORD_FOR_LOCK = 105; - + private ViewPager2 viewPager; + private DrawerLayout drawerLayout; + private TextView tvTitle; + private ImageView btnChangeLayout; private NotesListViewModel viewModel; - private NoteListBinding binding; - private NoteInfoAdapter adapter; - private View sidebarFragment; - - // 多选模式状态 - private boolean isMultiSelectMode = false; - // 待打开的受保护笔记 - private long mPendingNodeIdToOpen = -1; - private int mPendingNodeTypeToOpen = -1; - private static final String KEY_PENDING_NODE_ID = "pending_node_id"; - private static final String KEY_PENDING_NODE_TYPE = "pending_node_type"; - private static final String KEY_CURRENT_FOLDER_ID = "current_folder_id"; + private RecyclerView rvFolderTabs; + private View tvSearchBar; // Add member variable + private FolderAdapter folderAdapter; + private View headerContainer; + private View selectionHeader; + private View selectionBottomBar; + private BottomNavigationView bottomNav; + private TextView tvSelectionCount; + private TextView btnSelectionRestore; + private TextView btnSelectionDeleteForever; + private TextView btnActionPin; + private TextView btnActionLock; + private static final int REQUEST_CODE_VERIFY_PASSWORD_FOR_LOCK = 106; - /** - * 活动创建时的初始化方法 - *

- * 设置布局,初始化ViewModel,设置UI监听器 - *

- * - * @param savedInstanceState 保存的实例状态 - */ @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + setContentView(R.layout.activity_home); - // 启用边缘到边缘显示 - WindowCompat.setDecorFitsSystemWindows(getWindow(), false); - - binding = NoteListBinding.inflate(getLayoutInflater()); - setContentView(binding.getRoot()); - - // 注意:CoordinatorLayout和AppBarLayout会自动处理WindowInsets - // FAB也会自动避开导航栏 - // 不需要手动设置padding - - initViewModel(); - - // 恢复 pending 状态和当前文件夹 - if (savedInstanceState != null) { - mPendingNodeIdToOpen = savedInstanceState.getLong(KEY_PENDING_NODE_ID, -1); - mPendingNodeTypeToOpen = savedInstanceState.getInt(KEY_PENDING_NODE_TYPE, -1); - - long savedFolderId = savedInstanceState.getLong(KEY_CURRENT_FOLDER_ID, Notes.ID_ROOT_FOLDER); - if (savedFolderId != Notes.ID_ROOT_FOLDER) { - viewModel.setCurrentFolderId(savedFolderId); - } - - Log.d(TAG, "Restored pending node: " + mPendingNodeIdToOpen + ", type: " + mPendingNodeTypeToOpen + ", folder: " + savedFolderId); - } - - initViews(); - observeViewModel(); - } - - /** - * 保存 Activity 状态 - */ - @Override - protected void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putLong(KEY_PENDING_NODE_ID, mPendingNodeIdToOpen); - outState.putInt(KEY_PENDING_NODE_TYPE, mPendingNodeTypeToOpen); - outState.putLong(KEY_CURRENT_FOLDER_ID, viewModel.getCurrentFolderId()); - Log.d(TAG, "Saved pending node: " + mPendingNodeIdToOpen + ", current folder: " + viewModel.getCurrentFolderId()); - } + drawerLayout = findViewById(R.id.drawer_layout); + viewPager = findViewById(R.id.view_pager); + tvTitle = findViewById(R.id.tv_title); + btnChangeLayout = findViewById(R.id.btn_change_layout); + bottomNav = findViewById(R.id.bottom_navigation); + + rvFolderTabs = findViewById(R.id.rv_folder_tabs); + headerContainer = findViewById(R.id.header_container); + selectionHeader = findViewById(R.id.selection_header); + selectionBottomBar = findViewById(R.id.selection_bottom_bar); + tvSelectionCount = findViewById(R.id.tv_selection_count); + tvSearchBar = findViewById(R.id.tv_search_bar); // Init tvSearchBar + + btnSelectionRestore = findViewById(R.id.btn_selection_restore); + btnSelectionDeleteForever = findViewById(R.id.btn_selection_delete_forever); + btnActionPin = findViewById(R.id.btn_action_pin); + btnActionLock = findViewById(R.id.btn_action_lock); - /** - * 活动启动时的回调方法 - *

- * 加载笔记列表 - *

- */ - @Override - protected void onStart() { - super.onStart(); - // 刷新当前文件夹的笔记,而不是强制加载根目录 - // 这样可以保证从 PasswordActivity 返回时,如果 onStart 先执行,不会重置为根目录 - viewModel.refreshNotes(); - } - private void initViewModel() { + // Init Shared ViewModel NotesRepository repository = new NotesRepository(getContentResolver()); viewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() { @Override - public T create(Class modelClass) { - if (modelClass.isAssignableFrom(NotesListViewModel.class)) { - return (T) new NotesListViewModel(repository); - } - throw new IllegalArgumentException("Unknown ViewModel class"); + public T create(Class modelClass) { + return (T) new NotesListViewModel(repository); } }).get(NotesListViewModel.class); - Log.d(TAG, "ViewModel initialized"); - } - - /** - * 初始化视图 - */ - private void initViews() { - // 特殊处理:Fragment标签不会在ViewBinding中生成字段 - // 必须使用findViewById获取标签声明的Fragment实例 - // 这是Android ViewBinding的已知限制,不是遗漏 - sidebarFragment = findViewById(R.id.sidebar_fragment); + + // Load initial notes (All Notes by default now) + if (savedInstanceState == null) { + viewModel.loadNotes(Notes.ID_ALL_NOTES_FOLDER); + } - // 设置适配器 - adapter = new NoteInfoAdapter(this); - binding.notesList.setAdapter(adapter); - adapter.setOnNoteButtonClickListener(this); - adapter.setOnNoteItemClickListener(this); - adapter.setOnNoteItemLongClickListener(this); + // Setup Folder Tabs + folderAdapter = new FolderAdapter(this); + rvFolderTabs.setAdapter(folderAdapter); + folderAdapter.setOnFolderClickListener(folderId -> viewModel.enterFolder(folderId)); + + // Observe Folder Changes for UI updates + viewModel.getCurrentFolderIdLiveData().observe(this, folderId -> { + updateTrashModeUI(folderId == Notes.ID_TRASH_FOLER); + folderAdapter.setSelectedFolderId(folderId); + }); - // 设置点击监听 - binding.notesList.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - Object item = parent.getItemAtPosition(position); - if (item instanceof NotesRepository.NoteInfo) { - NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) item; - handleItemClick(note, position); + viewModel.getFoldersLiveData().observe(this, folders -> { + folderAdapter.setFolders(folders); + }); + + // Selection Mode Observer + viewModel.getIsSelectionMode().observe(this, isSelection -> { + if (isSelection) { + headerContainer.setVisibility(View.GONE); + selectionHeader.setVisibility(View.VISIBLE); + + if (viewModel.isTrashMode()) { + selectionBottomBar.setVisibility(View.GONE); + btnSelectionRestore.setVisibility(View.VISIBLE); + btnSelectionDeleteForever.setVisibility(View.VISIBLE); + // Hide Select All if needed, or keep it. Let's keep it for now but maybe change layout if crowded. + } else { + selectionBottomBar.setVisibility(View.VISIBLE); + btnSelectionRestore.setVisibility(View.GONE); + btnSelectionDeleteForever.setVisibility(View.GONE); + } + + // bottomNav.setVisibility(View.GONE); // Overlay covers it + updateSelectionCount(); // Initial update when entering mode + } else { + headerContainer.setVisibility(View.VISIBLE); + selectionHeader.setVisibility(View.GONE); + selectionBottomBar.setVisibility(View.GONE); + + // Only show bottom nav if NOT in trash mode (handled by updateTrashModeUI) + if (!viewModel.isTrashMode()) { + bottomNav.setVisibility(View.VISIBLE); } } }); - - // 初始化 Toolbar - setSupportActionBar(binding.toolbar); - if (getSupportActionBar() != null) { - getSupportActionBar().setTitle(R.string.app_name); - } - - // 初始化为普通模式 - updateToolbarForNormalMode(); - - // 设置 Toolbar 的汉堡菜单按钮点击监听器(打开侧栏) - binding.toolbar.setNavigationOnClickListener(v -> { - binding.drawerLayout.openDrawer(sidebarFragment); + + // Update selection count + viewModel.getSelectedIdsLiveData().observe(this, selectedIds -> { + updateSelectionCount(); + updateSelectionActionUI(); }); + + // Selection Actions + findViewById(R.id.btn_close_selection).setOnClickListener(v -> viewModel.setIsSelectionMode(false)); - // Set FAB click event - if (binding.btnNewNote != null) { - binding.btnNewNote.setOnClickListener(v -> { - Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_INSERT_OR_EDIT); - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, viewModel.getCurrentFolderId()); - startActivityForResult(intent, REQUEST_CODE_NEW_NODE); - }); - } - } + btnActionPin.setOnClickListener(v -> { + viewModel.toggleSelectedNotesPin(); + viewModel.setIsSelectionMode(false); + }); - /** - * 处理列表项点击 - *

- * 如果是便签,打开编辑器;如果是文件夹,进入该文件夹 - *

- * - * @param note 项 - * @param position 位置 - */ - private void handleItemClick(NotesRepository.NoteInfo note, int position) { - if (isMultiSelectMode) { - // 多选模式:切换选中状态 - boolean isSelected = viewModel.getSelectedNoteIds().contains(note.getId()); - viewModel.toggleNoteSelection(note.getId(), !isSelected); - if (adapter != null) { - adapter.setSelectedIds(viewModel.getSelectedNoteIds()); - } - updateToolbarForMultiSelectMode(); - } else { - // 普通模式 - if (note.type == Notes.TYPE_FOLDER) { - // 文件夹:进入该文件夹 - // 检查隐私锁 - if (note.isLocked && SecurityManager.getInstance(this).isPasswordSet()) { - mPendingNodeIdToOpen = note.getId(); - mPendingNodeTypeToOpen = note.type; - Intent intent = new Intent(this, PasswordActivity.class); - intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD); - startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN); - return; - } - viewModel.enterFolder(note.getId()); + btnActionLock.setOnClickListener(v -> { + SecurityManager securityManager = SecurityManager.getInstance(this); + if (!securityManager.isPasswordSet()) { + new AlertDialog.Builder(this) + .setTitle("设置密码") + .setMessage("使用加锁功能前请先设置密码") + .setPositiveButton("去设置", (d, w) -> { + showSetPasswordDialog(); + }) + .setNegativeButton("取消", null) + .show(); } else { - // 便签:打开编辑器 - openNoteEditor(note); + Intent intent = new Intent(this, PasswordActivity.class); + intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD); + startActivityForResult(intent, REQUEST_CODE_VERIFY_PASSWORD_FOR_LOCK); } - } - } + }); + + findViewById(R.id.btn_select_all).setOnClickListener(v -> { + viewModel.selectAllNotes(); + }); + + findViewById(R.id.btn_action_delete).setOnClickListener(v -> { + new AlertDialog.Builder(this) + .setTitle("删除") + .setMessage("确定要删除选中的笔记吗?") + .setPositiveButton("删除", (d,w) -> { + viewModel.deleteSelectedNotes(); + viewModel.setIsSelectionMode(false); + }) + .setNegativeButton("取消", null) + .show(); + }); + + // Restore Action + btnSelectionRestore.setOnClickListener(v -> { + new AlertDialog.Builder(this) + .setTitle("恢复笔记") + .setMessage("确定要恢复选中的笔记吗?") + .setPositiveButton("确定", (d, w) -> { + viewModel.restoreSelectedNotes(); + viewModel.setIsSelectionMode(false); + }) + .setNegativeButton("再想想", null) + .show(); + }); - /** - * 观察ViewModel的LiveData - */ - private void observeViewModel() { - // 观察笔记列表 - viewModel.getNotesLiveData().observe(this, new Observer>() { - @Override - public void onChanged(List notes) { - updateAdapter(notes); - updateToolbarForNormalMode(); - } + // Delete Forever Action + btnSelectionDeleteForever.setOnClickListener(v -> { + AlertDialog dialog = new AlertDialog.Builder(this) + .setTitle("永久删除") + .setMessage("确定要永久删除选中的笔记吗?此操作不可撤销。") + .setPositiveButton("确定", (d, w) -> { + viewModel.deleteSelectedNotesForever(); + viewModel.setIsSelectionMode(false); + }) + .setNegativeButton("再想想", null) + .show(); + // Set positive button color to red/dark if possible, but default is fine or need custom view + dialog.setOnShowListener(d -> { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(getResources().getColor(android.R.color.holo_red_dark)); + }); }); - // 观察加载状态 - viewModel.getIsLoading().observe(this, new Observer() { - @Override - public void onChanged(Boolean isLoading) { - updateLoadingState(isLoading); - } + // Search Bar + tvSearchBar.setOnClickListener(v -> { + startActivity(new android.content.Intent(this, NoteSearchActivity.class)); }); - // 观察错误消息 - viewModel.getErrorMessage().observe(this, new Observer() { - @Override - public void onChanged(String message) { - if (message != null && !message.isEmpty()) { - showError(message); - } + MainPagerAdapter adapter = new MainPagerAdapter(this); + viewPager.setAdapter(adapter); + viewPager.setUserInputEnabled(false); // Disable swipe to avoid conflict + + // Setup Bottom Navigation + bottomNav.setOnItemSelectedListener(item -> { + int itemId = item.getItemId(); + if (itemId == R.id.nav_notes) { + viewPager.setCurrentItem(0, false); + tvTitle.setText(R.string.app_name); + btnChangeLayout.setVisibility(View.VISIBLE); + + // Show Search & Tabs + tvSearchBar.setVisibility(View.VISIBLE); + rvFolderTabs.setVisibility(View.VISIBLE); + + updateLayoutButtonIcon(); + return true; + } else if (itemId == R.id.nav_tasks) { + viewPager.setCurrentItem(1, false); + tvTitle.setText("待办"); + btnChangeLayout.setVisibility(View.GONE); + + // Hide Search & Tabs + tvSearchBar.setVisibility(View.GONE); + rvFolderTabs.setVisibility(View.GONE); + + return true; } + return false; }); - // 观察文件夹路径(用于面包屑导航) - viewModel.getFolderPathLiveData().observe(this, new Observer>() { - @Override - public void onChanged(List path) { - updateBreadcrumb(path); + // Sidebar Button + findViewById(R.id.btn_sidebar).setOnClickListener(v -> { + if (drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } else { + drawerLayout.openDrawer(GravityCompat.START); } }); - // 观察侧栏刷新通知 - viewModel.getSidebarRefreshNeeded().observe(this, new Observer() { - @Override - public void onChanged(Boolean refreshNeeded) { - if (refreshNeeded != null && refreshNeeded) { - // 通知侧栏刷新 - SidebarFragment sidebarFrag = (SidebarFragment) getSupportFragmentManager() - .findFragmentById(R.id.sidebar_fragment); - if (sidebarFrag != null) { - sidebarFrag.refreshFolderTree(); - } - // 重置刷新状态 - viewModel.getSidebarRefreshNeeded().setValue(false); - } + // Layout Toggle Button + btnChangeLayout.setOnClickListener(v -> { + NotesListFragment fragment = getNotesListFragment(); + if (fragment != null) { + boolean isStaggered = fragment.toggleLayout(); + btnChangeLayout.setImageResource(isStaggered ? R.drawable.ic_view_list : R.drawable.ic_view_grid); } }); + + // Initial icon update (post to ensure fragment is attached) + viewPager.post(this::updateLayoutButtonIcon); } - - /** - * 更新面包屑导航 - * - * @param path 文件夹路径 - */ - private void updateBreadcrumb(List path) { - if (binding.breadcrumbInclude == null || binding.breadcrumbInclude.breadcrumbItems == null || path == null) { - return; - } - - binding.breadcrumbInclude.breadcrumbItems.removeAllViews(); - - for (int i = 0; i < path.size(); i++) { - NotesRepository.NoteInfo folder = path.get(i); - - // 如果不是第一个,添加分隔符 " > " - if (i > 0) { - TextView separator = new TextView(this); - separator.setText(" > "); - separator.setTextSize(14); - separator.setTextColor(android.R.color.darker_gray); - binding.breadcrumbInclude.breadcrumbItems.addView(separator); - } - - // 创建面包屑项 - TextView breadcrumbItem = (TextView) getLayoutInflater() - .inflate(R.layout.breadcrumb_item, binding.breadcrumbInclude.breadcrumbItems, false); - breadcrumbItem.setText(folder.title); - - // 如果是当前文件夹(最后一个),高亮显示且不可点击 - if (i == path.size() - 1) { - breadcrumbItem.setTextColor(getColor(R.color.primary_color)); - breadcrumbItem.setEnabled(false); + + private void updateTrashModeUI(boolean isTrash) { + if (isTrash) { + tvTitle.setText("回收站"); + tvSearchBar.setVisibility(View.GONE); + rvFolderTabs.setVisibility(View.GONE); + bottomNav.setVisibility(View.GONE); + } else { + // Restore normal UI state + if (bottomNav.getSelectedItemId() == R.id.nav_notes) { + tvTitle.setText(R.string.app_name); + tvSearchBar.setVisibility(View.VISIBLE); + rvFolderTabs.setVisibility(View.VISIBLE); } else { - // 其他层级可以点击跳转 - final long targetFolderId = folder.id; - breadcrumbItem.setOnClickListener(v -> viewModel.enterFolder(targetFolderId)); + tvTitle.setText("待办"); + tvSearchBar.setVisibility(View.GONE); + rvFolderTabs.setVisibility(View.GONE); } - - binding.breadcrumbInclude.breadcrumbItems.addView(breadcrumbItem); + bottomNav.setVisibility(View.VISIBLE); } } - - /** - * 更新适配器数据 - */ - private void updateAdapter(List notes) { - adapter.setNotes(notes); - Log.d(TAG, "Adapter updated with " + notes.size() + " notes"); - } - - /** - * 更新加载状态 - */ - private void updateLoadingState(boolean isLoading) { - // TODO: 显示/隐藏进度条 - } - - /** - * 显示错误消息 - */ - private void showError(String message) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - - /** - * 打开笔记编辑器 - */ - private void openNoteEditor(NotesRepository.NoteInfo note) { - // 检查隐私锁 - if (note.isLocked && SecurityManager.getInstance(this).isPasswordSet()) { - mPendingNodeIdToOpen = note.getId(); - mPendingNodeTypeToOpen = note.type; - Intent intent = new Intent(this, PasswordActivity.class); - intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD); - startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN); - return; + + public void updateSelectionCount() { + if (viewModel != null) { + int count = viewModel.getSelectedCount(); + tvSelectionCount.setText("已选择 " + count + " 项"); } - - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, note.getParentId()); - intent.putExtra(Intent.EXTRA_UID, note.getId()); - startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); } - /** - * 编辑按钮点击事件处理 - * - * @param position 列表位置 - * @param noteId 便签 ID - */ - @Override - public void onEditButtonClick(int position, long noteId) { - NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position); - if (note != null) { - openNoteEditor(note); - } else { - Log.e(TAG, "Edit button clicked but note is null at position: " + position); - } + private void showSetPasswordDialog() { + new AlertDialog.Builder(this) + .setTitle("设置密码") + .setItems(new String[]{"数字锁", "手势锁"}, (dialog, which) -> { + int type = (which == 0) ? SecurityManager.TYPE_PIN : SecurityManager.TYPE_PATTERN; + Intent intent = new Intent(this, PasswordActivity.class); + intent.setAction(PasswordActivity.ACTION_SETUP_PASSWORD); + intent.putExtra(PasswordActivity.EXTRA_PASSWORD_TYPE, type); + startActivity(intent); + }) + .show(); } - @Override - public void onNoteItemClick(int position, long noteId) { - Log.d(TAG, "===== onNoteItemClick CALLED ====="); - Log.d(TAG, "position: " + position + ", noteId: " + noteId); - - if (isMultiSelectMode) { - Log.d(TAG, "Multi-select mode active, toggling selection"); - NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position); - if (note != null) { - boolean isSelected = viewModel.getSelectedNoteIds().contains(note.getId()); - viewModel.toggleNoteSelection(note.getId(), !isSelected); - - if (adapter != null) { - adapter.setSelectedIds(viewModel.getSelectedNoteIds()); + private void updateSelectionActionUI() { + if (viewModel != null) { + if (btnActionPin != null) { + boolean isAllPinned = viewModel.isAllSelectedPinned(); + if (isAllPinned) { + btnActionPin.setText("取消置顶"); + } else { + btnActionPin.setText("置顶"); } - // 更新toolbar标题 - updateToolbarForMultiSelectMode(); } - Log.d(TAG, "===== onNoteItemClick END (multi-select mode) ====="); - } else { - Log.d(TAG, "Normal mode, checking item type"); - NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position); - if (note != null) { - if (viewModel.isTrashMode()) { - // 回收站模式:弹出恢复/删除对话框 - showTrashItemDialog(note); - } else if (viewModel.isTemplateMode() && note.type == Notes.TYPE_NOTE) { - showApplyTemplateDialog(note); - } else if (note.type == Notes.TYPE_FOLDER) { - // 文件夹:进入该文件夹 - // 检查隐私锁 - if (note.isLocked && SecurityManager.getInstance(this).isPasswordSet()) { - mPendingNodeIdToOpen = note.getId(); - mPendingNodeTypeToOpen = note.type; - Intent intent = new Intent(this, PasswordActivity.class); - intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD); - startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN); - return; - } - Log.d(TAG, "Folder clicked, entering folder: " + note.getId()); - viewModel.enterFolder(note.getId()); + + if (btnActionLock != null) { + boolean isAllLocked = viewModel.isAllSelectedLocked(); + if (isAllLocked) { + btnActionLock.setText("解锁"); } else { - // 便签:打开编辑器 - Log.d(TAG, "Note clicked, opening editor"); - openNoteEditor(note); + btnActionLock.setText("加锁"); } } - Log.d(TAG, "===== onNoteItemClick END ====="); } } - @Override - public void onNoteItemLongClick(int position, long noteId) { - Log.d(TAG, "===== onNoteItemLongClick CALLED ====="); - Log.d(TAG, "position: " + position + ", noteId: " + noteId); - - if (!isMultiSelectMode) { - Log.d(TAG, "Entering multi-select mode"); - enterMultiSelectMode(); - viewModel.toggleNoteSelection(noteId, true); - - if (adapter != null) { - adapter.setSelectedIds(viewModel.getSelectedNoteIds()); + private NotesListFragment getNotesListFragment() { + for (Fragment fragment : getSupportFragmentManager().getFragments()) { + if (fragment instanceof NotesListFragment) { + return (NotesListFragment) fragment; } - - updateSelectionState(position, true); - - Log.d(TAG, "===== onNoteItemLongClick END ====="); - } else { - Log.d(TAG, "Multi-select mode already active, ignoring long click"); - } - } - - /** - * 显示应用模板确认对话框 - */ - private void showApplyTemplateDialog(NotesRepository.NoteInfo note) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("应用模板"); - builder.setMessage("使用模板 \"" + (TextUtils.isEmpty(note.title) ? "未命名" : note.title) + "\" 创建新笔记?"); - builder.setPositiveButton("创建", (dialog, which) -> { - viewModel.applyTemplate(note.getId(), new NotesRepository.Callback() { - @Override - public void onSuccess(Long newNoteId) { - runOnUiThread(() -> { - Toast.makeText(NotesListActivity.this, "创建成功", Toast.LENGTH_SHORT).show(); - // 跳转到新笔记编辑页 - Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, newNoteId); - startActivity(intent); - // 退出模板模式(返回根目录) - // viewModel.loadNotes(Notes.ID_ROOT_FOLDER); - }); - } - - @Override - public void onError(Exception error) { - runOnUiThread(() -> { - Toast.makeText(NotesListActivity.this, "创建失败: " + error.getMessage(), Toast.LENGTH_SHORT).show(); - }); - } - }); - }); - builder.setNegativeButton("取消", null); - builder.setNeutralButton("编辑模板", (dialog, which) -> { - // 打开编辑器编辑模板本身 - openNoteEditor(note); - }); - builder.show(); - } - - /** - * 显示回收站条目操作对话框 - */ - private void showTrashItemDialog(NotesRepository.NoteInfo note) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("操作确认"); - builder.setMessage("要恢复还是永久删除此笔记?"); - builder.setPositiveButton("恢复", (dialog, which) -> { - // 临时将选中的ID设为当前ID,以便复用ViewModel的restoreSelectedNotes - // 这里为了简单,我们直接调用ViewModel的新方法restoreNote(note.getId()) - // 但ViewModel还没这个方法,所以我们先手动构造一个List - viewModel.clearSelection(); - viewModel.toggleNoteSelection(note.getId(), true); - viewModel.restoreSelectedNotes(); - }); - builder.setNegativeButton("永久删除", (dialog, which) -> { - showDeleteForeverConfirmDialog(note); - }); - builder.setNeutralButton("再想想", null); - builder.show(); - } - - /** - * 显示永久删除确认对话框 - */ - private void showDeleteForeverConfirmDialog(NotesRepository.NoteInfo note) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle("永久删除"); - builder.setMessage("确定要永久删除此笔记吗?删除后无法恢复!"); - builder.setIcon(android.R.drawable.ic_dialog_alert); - - builder.setPositiveButton("确定", (dialog, which) -> { - viewModel.clearSelection(); - viewModel.toggleNoteSelection(note.getId(), true); - viewModel.deleteSelectedNotesForever(); - }); - - // 设置“确定”按钮颜色为深色(通常系统默认就是强调色,如果需要特定颜色需自定义View或Theme) - // 这里使用默认样式,通常Positive是强调色 - - builder.setNegativeButton("取消", null); - builder.show(); - } - - /** - * 进入多选模式 - */ - private void enterMultiSelectMode() { - isMultiSelectMode = true; - // 隐藏FAB按钮 - if (binding.btnNewNote != null) { - binding.btnNewNote.setVisibility(View.GONE); } - // 更新toolbar为多选模式 - updateToolbarForMultiSelectMode(); + return null; } - /** - * 退出多选模式 - */ - private void exitMultiSelectMode() { - isMultiSelectMode = false; - // 显示FAB按钮 - if (binding.btnNewNote != null) { - binding.btnNewNote.setVisibility(View.VISIBLE); - } - // 清除选中状态 - viewModel.clearSelection(); - if (adapter != null) { - adapter.setSelectedIds(new java.util.HashSet<>()); - adapter.notifyDataSetChanged(); - } - // 更新toolbar为普通模式 - updateToolbarForNormalMode(); - } - - /** - * 更新Toolbar为多选模式 - */ - private void updateToolbarForMultiSelectMode() { - if (binding.toolbar == null) return; - - // 设置标题为选中数量 - int selectedCount = viewModel.getSelectedCount(); - String title = getString(R.string.menu_select_title, selectedCount); - binding.toolbar.setTitle(title); - - // 设置导航图标为返回(取消多选) - binding.toolbar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material); - binding.toolbar.setNavigationOnClickListener(v -> exitMultiSelectMode()); - - // 移除普通模式的菜单(如果有) - binding.toolbar.getMenu().clear(); - - // 直接在toolbar上添加操作按钮(不在三点菜单中) - Menu menu = binding.toolbar.getMenu(); - - // 删除按钮 - MenuItem deleteItem = menu.add(Menu.NONE, R.id.multi_select_delete, 1, getString(R.string.menu_delete)); - deleteItem.setIcon(android.R.drawable.ic_menu_delete); - deleteItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - - // 移动按钮 - MenuItem moveItem = menu.add(Menu.NONE, R.id.multi_select_move, 2, getString(R.string.menu_move)); - moveItem.setIcon(android.R.drawable.ic_menu_sort_by_size); - moveItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - - // 置顶按钮 - boolean allPinned = viewModel.isAllSelectedPinned(); - MenuItem pinItem = menu.add(Menu.NONE, R.id.multi_select_pin, 3, allPinned ? getString(R.string.menu_unpin) : getString(R.string.menu_pin)); - // 使用上传图标代替置顶图标,或者如果有合适的资源可以使用 - pinItem.setIcon(android.R.drawable.ic_menu_upload); - pinItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - - // 锁定按钮 - boolean allLocked = viewModel.isAllSelectedLocked(); - MenuItem lockItem = menu.add(Menu.NONE, R.id.multi_select_lock, 4, getString(R.string.menu_lock)); - lockItem.setIcon(android.R.drawable.ic_lock_lock); - lockItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - } - - /** - * 更新Toolbar为普通模式 - */ - private void updateToolbarForNormalMode() { - if (binding.toolbar == null) return; - - // 清除多选模式菜单 - binding.toolbar.getMenu().clear(); - - // 设置标题 - if (viewModel.isTrashMode()) { - binding.toolbar.setTitle(R.string.menu_trash); - } else if (viewModel.isTemplateMode()) { - binding.toolbar.setTitle(R.string.menu_templates); - } else { - binding.toolbar.setTitle(R.string.app_name); - // 添加普通模式菜单 - binding.toolbar.inflateMenu(R.menu.note_list); - } - - // 设置导航图标为汉堡菜单 - binding.toolbar.setNavigationIcon(android.R.drawable.ic_menu_sort_by_size); - binding.toolbar.setNavigationOnClickListener(v -> { - binding.drawerLayout.openDrawer(sidebarFragment); - }); - - // 如果是回收站模式,不显示新建按钮 - if (viewModel.isTrashMode()) { - if (binding.btnNewNote != null) { - binding.btnNewNote.setVisibility(View.GONE); - } - } else { - if (binding.btnNewNote != null) { - binding.btnNewNote.setVisibility(View.VISIBLE); - } + private void updateLayoutButtonIcon() { + NotesListFragment fragment = getNotesListFragment(); + if (fragment != null) { + boolean isStaggered = fragment.isStaggeredLayout(); + btnChangeLayout.setImageResource(isStaggered ? R.drawable.ic_view_list : R.drawable.ic_view_grid); } } - - - /** - * 显示删除确认对话框 - */ - private void showDeleteDialog() { - int selectedCount = viewModel.getSelectedCount(); - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.alert_title_delete)); - builder.setIcon(android.R.drawable.ic_dialog_alert); - builder.setMessage(getString(R.string.alert_message_delete_notes, selectedCount)); - builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - viewModel.deleteSelectedNotes(); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - - /** - * 显示移动菜单 - */ - private void showMoveMenu() { - // TODO: 实现文件夹选择逻辑 - Toast.makeText(this, "移动功能开发中", Toast.LENGTH_SHORT).show(); - } - - /** - * 活动结果回调方法 - */ @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); - - if (resultCode == RESULT_OK) { - if (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE) { - viewModel.refreshNotes(); - } else if (requestCode == REQUEST_CODE_CHECK_PASSWORD_FOR_OPEN) { - if (mPendingNodeIdToOpen != -1) { - if (mPendingNodeTypeToOpen == Notes.TYPE_FOLDER) { - // 文件夹密码验证通过,直接进入文件夹 - Log.d(TAG, "Password verified for folder: " + mPendingNodeIdToOpen); - viewModel.enterFolder(mPendingNodeIdToOpen); - } else { - // 密码验证通过,直接打开编辑器 - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, mPendingNodeIdToOpen); - startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); - } - mPendingNodeIdToOpen = -1; - mPendingNodeTypeToOpen = -1; - } - } else if (requestCode == REQUEST_CODE_CHECK_PASSWORD_FOR_LOCK) { - // 加锁/解锁密码验证通过 - boolean wasLocked = viewModel.isAllSelectedLocked(); - viewModel.toggleSelectedNotesLock(); - String lockMsg = wasLocked ? getString(R.string.menu_unlock) + "成功" : getString(R.string.menu_lock) + "成功"; - Toast.makeText(this, lockMsg, Toast.LENGTH_SHORT).show(); - } - } else if (requestCode == REQUEST_CODE_CHECK_PASSWORD_FOR_LOCK) { - // 加锁/解锁密码验证失败或取消 - boolean wasLocked = viewModel.isAllSelectedLocked(); - String lockMsg = wasLocked ? getString(R.string.menu_unlock) + "失败" : getString(R.string.menu_lock) + "失败"; - Toast.makeText(this, lockMsg, Toast.LENGTH_SHORT).show(); + if (requestCode == REQUEST_CODE_VERIFY_PASSWORD_FOR_LOCK && resultCode == RESULT_OK) { + viewModel.toggleSelectedNotesLock(); + viewModel.setIsSelectionMode(false); } } - /** - * 创建选项菜单 - */ - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.note_list, menu); - return true; - } - - /** - * 选项菜单项点击事件 - */ - @Override - public boolean onOptionsItemSelected(MenuItem item) { - int itemId = item.getItemId(); + private class MainPagerAdapter extends FragmentStateAdapter { + public MainPagerAdapter(@NonNull AppCompatActivity activity) { + super(activity); + } - switch (itemId) { - case R.id.menu_tasks: - startActivity(new Intent(this, TaskListActivity.class)); - overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left); - return true; - case R.id.menu_search: - Intent searchIntent = new Intent(this, NoteSearchActivity.class); - startActivity(searchIntent); - return true; - case R.id.menu_new_folder: - // 创建新文件夹 - showCreateFolderDialog(); - return true; - case R.id.menu_export_text: - // TODO: 导出笔记 - Toast.makeText(this, "导出功能开发中", Toast.LENGTH_SHORT).show(); - return true; - case R.id.menu_sync: - // TODO: 同步功能 - Toast.makeText(this, "同步功能暂不可用", Toast.LENGTH_SHORT).show(); - return true; - case R.id.menu_setting: - // TODO: 设置功能 - Toast.makeText(this, "设置功能开发中", Toast.LENGTH_SHORT).show(); - return true; - // 多选模式菜单项 - case R.id.multi_select_delete: - showDeleteDialog(); - return true; - case R.id.multi_select_move: - showMoveMenu(); - return true; - case R.id.multi_select_pin: - boolean wasPinned = viewModel.isAllSelectedPinned(); - viewModel.toggleSelectedNotesPin(); - String toastMsg = wasPinned ? getString(R.string.menu_unpin) + "成功" : getString(R.string.menu_pin) + "成功"; - Toast.makeText(this, toastMsg, Toast.LENGTH_SHORT).show(); - return true; - case R.id.multi_select_lock: - // 检查是否设置了隐私密码 - if (SecurityManager.getInstance(this).isPasswordSet()) { - // 需要先验证密码 - Intent intent = new Intent(this, PasswordActivity.class); - intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD); - startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD_FOR_LOCK); - } else { - Toast.makeText(this, "请先在设置中设置隐私密码", Toast.LENGTH_SHORT).show(); - // 跳转到设置密码界面 - Intent intent = new Intent(this, NotesPreferenceActivity.class); - startActivity(intent); - } - return true; - default: - return super.onOptionsItemSelected(item); + @NonNull + @Override + public Fragment createFragment(int position) { + if (position == 0) { + return new NotesListFragment(); + } else { + return new TaskListFragment(); + } } - } - /** - * 上下文菜单创建 - */ - @Override - public void onCreateContextMenu(android.view.ContextMenu menu, View v, android.view.ContextMenu.ContextMenuInfo menuInfo) { - getMenuInflater().inflate(R.menu.sub_folder, menu); + @Override + public int getItemCount() { + return 2; + } } - /** - * 上下文菜单项点击 - */ @Override - public boolean onContextItemSelected(MenuItem item) { - // TODO: 处理文件夹上下文菜单 - return super.onContextItemSelected(item); - } - - private void updateSelectionState(int position, boolean selected) { - Log.d("NotesListActivity", "===== updateSelectionState called ====="); - Log.d("NotesListActivity", "position: " + position + ", selected: " + selected); - NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) adapter.getItem(position); - if (note != null) { - Log.d("NotesListActivity", "note ID: " + note.getId()); - Log.d("NotesListActivity", "Current selectedIds size before update: " + adapter.getSelectedIds().size()); - Log.d("NotesListActivity", "Note already in selectedIds: " + adapter.getSelectedIds().contains(note.getId())); - if (adapter.getSelectedIds().contains(note.getId()) != selected) { - if (selected) { - Log.d("NotesListActivity", "Adding note ID to selectedIds"); - adapter.getSelectedIds().add(note.getId()); - } else { - Log.d("NotesListActivity", "Removing note ID from selectedIds"); - adapter.getSelectedIds().remove(note.getId()); - } - Log.d("NotesListActivity", "SelectedIds size after update: " + adapter.getSelectedIds().size()); - adapter.notifyDataSetChanged(); - Log.d("NotesListActivity", "notifyDataSetChanged() called"); - } else { - Log.d("NotesListActivity", "Note selection state unchanged, skipping update"); - } + public void onBackPressed() { + if (drawerLayout != null && drawerLayout.isDrawerOpen(GravityCompat.START)) { + drawerLayout.closeDrawer(GravityCompat.START); + } else if (viewModel != null && viewModel.navigateUp()) { + // Handled by ViewModel navigation } else { - Log.e("NotesListActivity", "note is NULL at position: " + position); + super.onBackPressed(); } - Log.d("NotesListActivity", "===== updateSelectionState END ====="); } - // ==================== SidebarFragment.OnSidebarItemSelectedListener 实现 ==================== - + // ==================== Sidebar Callbacks ==================== + @Override public void onFolderSelected(long folderId) { - // 跳转到指定文件夹 + drawerLayout.closeDrawer(GravityCompat.START); viewModel.enterFolder(folderId); - // 关闭侧栏 - if (binding.drawerLayout != null) { - binding.drawerLayout.closeDrawer(sidebarFragment); - } } @Override public void onTrashSelected() { - // 跳转到回收站 + drawerLayout.closeDrawer(GravityCompat.START); viewModel.enterFolder(Notes.ID_TRASH_FOLER); - // 关闭侧栏 - if (binding.drawerLayout != null) { - binding.drawerLayout.closeDrawer(sidebarFragment); - } } - - @Override - public void onSyncSelected() { - // TODO: 实现同步功能 - Log.d(TAG, "Sync selected"); - Toast.makeText(this, "同步功能待实现", Toast.LENGTH_SHORT).show(); - } - - @Override - public void onLoginSelected() { - // TODO: 实现登录功能 - Log.d(TAG, "Login selected"); - Toast.makeText(this, "登录功能待实现", Toast.LENGTH_SHORT).show(); - } - - @Override - public void onExportSelected() { - // TODO: 实现导出功能 - Log.d(TAG, "Export selected"); - Toast.makeText(this, "导出功能待实现", Toast.LENGTH_SHORT).show(); - } - - @Override - public void onTemplateSelected() { - // 跳转到模板文件夹 + @Override public void onSyncSelected() { drawerLayout.closeDrawer(GravityCompat.START); } + @Override public void onLoginSelected() { drawerLayout.closeDrawer(GravityCompat.START); } + @Override public void onExportSelected() { drawerLayout.closeDrawer(GravityCompat.START); } + @Override public void onTemplateSelected() { + drawerLayout.closeDrawer(GravityCompat.START); viewModel.enterFolder(Notes.ID_TEMPLATE_FOLDER); - // 关闭侧栏 - if (binding.drawerLayout != null) { - binding.drawerLayout.closeDrawer(sidebarFragment); - } } - - @Override - public void onSettingsSelected() { - // 打开设置页面 - Intent intent = new Intent(this, net.micode.notes.ui.NotesPreferenceActivity.class); - startActivity(intent); - // 关闭侧栏 - if (binding.drawerLayout != null) { - binding.drawerLayout.closeDrawer(sidebarFragment); - } - } - - @Override - public void onCreateFolder() { - // 显示创建文件夹对话框 - showCreateFolderDialog(); - } - - /** - * 显示创建文件夹对话框 - */ - private void showCreateFolderDialog() { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.dialog_create_folder_title); - - final EditText input = new EditText(this); - input.setHint(R.string.dialog_create_folder_hint); - input.setFilters(new InputFilter[]{new InputFilter.LengthFilter(50)}); - - builder.setView(input); - - builder.setPositiveButton(R.string.menu_create_folder, (dialog, which) -> { - String folderName = input.getText().toString().trim(); - if (TextUtils.isEmpty(folderName)) { - Toast.makeText(this, R.string.error_folder_name_empty, Toast.LENGTH_SHORT).show(); - return; - } - if (folderName.length() > 50) { - Toast.makeText(this, R.string.error_folder_name_too_long, Toast.LENGTH_SHORT).show(); - return; - } - - // 创建文件夹 - NotesRepository repository = new NotesRepository(getContentResolver()); - long parentId = viewModel.getCurrentFolderId(); - if (parentId == 0) { - parentId = Notes.ID_ROOT_FOLDER; - } - repository.createFolder(parentId, folderName, - new NotesRepository.Callback() { - @Override - public void onSuccess(Long folderId) { - runOnUiThread(() -> { - Toast.makeText(NotesListActivity.this, R.string.create_folder_success, Toast.LENGTH_SHORT).show(); - // 刷新笔记列表 - viewModel.loadNotes(viewModel.getCurrentFolderId()); - }); - } - - @Override - public void onError(Exception error) { - runOnUiThread(() -> { - Toast.makeText(NotesListActivity.this, "创建文件夹失败: " + error.getMessage(), Toast.LENGTH_SHORT).show(); - }); - } - }); - }); - - builder.setNegativeButton(android.R.string.cancel, null); - builder.show(); - } - - @Override - public void onCloseSidebar() { - // 关闭侧栏 - if (binding.drawerLayout != null) { - binding.drawerLayout.closeDrawer(sidebarFragment); - } - } - - /** - * 返回键按下事件处理 - *

- * 多选模式:退出多选模式 - * 子文件夹:返回上一级文件夹 - * 根文件夹:最小化应用 - *

- */ - @Override - public void onBackPressed() { - if (isMultiSelectMode) { - // 多选模式:退出多选模式 - exitMultiSelectMode(); - } else if (binding.drawerLayout != null && binding.drawerLayout.isDrawerOpen(sidebarFragment)) { - // 侧栏打开:关闭侧栏 - binding.drawerLayout.closeDrawer(sidebarFragment); - } else if (viewModel.getCurrentFolderId() != Notes.ID_ROOT_FOLDER && - viewModel.getCurrentFolderId() != Notes.ID_CALL_RECORD_FOLDER) { - // 子文件夹:返回上一级 - if (!viewModel.navigateUp()) { - // 如果没有导航历史,返回根文件夹 - viewModel.loadNotes(Notes.ID_ROOT_FOLDER); - } - } else { - // 根文件夹:最小化应用 - moveTaskToBack(true); - } - } - - @Override - protected void onDestroy() { - super.onDestroy(); - binding = null; + + @Override public void onSettingsSelected() { + drawerLayout.closeDrawer(GravityCompat.START); + startActivity(new android.content.Intent(this, SettingsActivity.class)); } -} + @Override public void onCreateFolder() { drawerLayout.closeDrawer(GravityCompat.START); } + @Override public void onCloseSidebar() { drawerLayout.closeDrawer(GravityCompat.START); } +} \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java index de22828..b8a9eda 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsFragment.java @@ -18,7 +18,13 @@ import net.micode.notes.R; import net.micode.notes.tool.SecurityManager; import net.micode.notes.data.ThemeRepository; +import net.micode.notes.data.NotesRepository; +import android.provider.Settings; +import android.net.Uri; +import androidx.preference.SwitchPreferenceCompat; import androidx.preference.ListPreference; +import net.micode.notes.capsule.CapsuleService; +import net.micode.notes.capsule.ClipboardMonitorService; import static android.app.Activity.RESULT_OK; @@ -26,7 +32,9 @@ public class SettingsFragment extends PreferenceFragmentCompat { public static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key"; public static final String PREFERENCE_SECURITY_KEY = "pref_key_security"; public static final String PREFERENCE_THEME_MODE = "pref_theme_mode"; + public static final String PREFERENCE_CAPSULE_ENABLE = "pref_key_capsule_enable"; public static final int REQUEST_CODE_CHECK_PASSWORD = 104; + public static final int REQUEST_CODE_OVERLAY_PERMISSION = 105; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { @@ -34,6 +42,116 @@ public class SettingsFragment extends PreferenceFragmentCompat { loadThemePreference(); loadSecurityPreference(); loadAccountPreference(); + loadCapsulePreference(); + } + + private void loadCapsulePreference() { + SwitchPreferenceCompat capsulePref = findPreference(PREFERENCE_CAPSULE_ENABLE); + if (capsulePref != null) { + capsulePref.setOnPreferenceChangeListener((preference, newValue) -> { + boolean enabled = (boolean) newValue; + if (enabled) { + checkCapsulePermissions(); + } else { + stopCapsuleService(); + } + return true; + }); + } + } + + private void checkCapsulePermissions() { + Context context = getContext(); + if (context == null) return; + + // Check Overlay Permission + if (!Settings.canDrawOverlays(context)) { + new AlertDialog.Builder(context) + .setTitle(R.string.capsule_permission_alert_window_title) + .setMessage(R.string.capsule_permission_alert_window_message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, + Uri.parse("package:" + context.getPackageName())); + startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMISSION); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> { + // Reset switch if cancelled + SwitchPreferenceCompat pref = findPreference(PREFERENCE_CAPSULE_ENABLE); + if (pref != null) pref.setChecked(false); + }) + .show(); + return; + } + + // Check Accessibility Permission + if (!isAccessibilitySettingsOn(context)) { + new AlertDialog.Builder(context) + .setTitle(R.string.capsule_permission_accessibility_title) + .setMessage(R.string.capsule_permission_accessibility_message) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + startActivity(intent); + }) + .setNegativeButton(android.R.string.cancel, (dialog, which) -> { + // Reset switch + SwitchPreferenceCompat pref = findPreference(PREFERENCE_CAPSULE_ENABLE); + if (pref != null) pref.setChecked(false); + }) + .show(); + return; + } + + startCapsuleService(); + } + + private boolean isAccessibilitySettingsOn(Context mContext) { + int accessibilityEnabled = 0; + final String service = mContext.getPackageName() + "/" + ClipboardMonitorService.class.getCanonicalName(); + try { + accessibilityEnabled = Settings.Secure.getInt( + mContext.getApplicationContext().getContentResolver(), + android.provider.Settings.Secure.ACCESSIBILITY_ENABLED); + } catch (Settings.SettingNotFoundException e) { + } + TextUtils.SimpleStringSplitter mStringColonSplitter = new TextUtils.SimpleStringSplitter(':'); + + if (accessibilityEnabled == 1) { + String settingValue = Settings.Secure.getString( + mContext.getApplicationContext().getContentResolver(), + Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES); + if (settingValue != null) { + mStringColonSplitter.setString(settingValue); + while (mStringColonSplitter.hasNext()) { + String accessibilityService = mStringColonSplitter.next(); + if (accessibilityService.equalsIgnoreCase(service)) { + return true; + } + } + } + } + return false; + } + + private void startCapsuleService() { + Context context = getContext(); + if (context != null) { + Intent intent = new Intent(context, CapsuleService.class); + // In Android 8.0+, we might need startForegroundService, but for now stick to startService + // The service itself should call startForeground + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + } + } + + private void stopCapsuleService() { + Context context = getContext(); + if (context != null) { + Intent intent = new Intent(context, CapsuleService.class); + context.stopService(intent); + } } private void loadThemePreference() { 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 f47be61..b6c3830 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 @@ -131,10 +131,13 @@ public class SidebarFragment extends Fragment { @Override public void onAttach(@NonNull Context context) { super.onAttach(context); - if (context instanceof OnSidebarItemSelectedListener) { + if (getParentFragment() instanceof OnSidebarItemSelectedListener) { + listener = (OnSidebarItemSelectedListener) getParentFragment(); + } else if (context instanceof OnSidebarItemSelectedListener) { listener = (OnSidebarItemSelectedListener) context; } else { - throw new RuntimeException(context.toString() + " must implement OnSidebarItemSelectedListener"); + // throw new RuntimeException(context.toString() + " must implement OnSidebarItemSelectedListener"); + // Allow null listener for now to avoid crashes during refactoring } } diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java index b97cf59..9b965d4 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListActivity.java @@ -36,21 +36,13 @@ public class TaskListActivity extends AppCompatActivity implements TaskListAdapt // Initialize Toolbar Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - - // Remove default title - if (getSupportActionBar() != null) { - getSupportActionBar().setDisplayShowTitleEnabled(false); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - } - - // Setup custom "Notes" navigation - View notesTitle = findViewById(R.id.tv_toolbar_title_notes); - if (notesTitle != null) { - notesTitle.setOnClickListener(v -> { - finish(); - overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right); - }); + if (toolbar != null) { + setSupportActionBar(toolbar); + // Remove default title + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); + getSupportActionBar().setDisplayHomeAsUpEnabled(false); + } } // Setup Navigation Icon (Back Button) - REMOVED as per new requirement 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 574d462..1217078 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 @@ -36,7 +36,7 @@ import java.util.List; *

* * @see NotesRepository - * @see Note + * @see net.micode.notes.model.Note */ public class NotesListViewModel extends ViewModel { private static final String TAG = "NotesListViewModel"; @@ -46,6 +46,9 @@ public class NotesListViewModel extends ViewModel { // 笔记列表LiveData private final MutableLiveData> notesLiveData = new MutableLiveData<>(); + // 文件夹列表LiveData (用于顶部Tab) + private final MutableLiveData> foldersLiveData = new MutableLiveData<>(); + // 加载状态LiveData private final MutableLiveData isLoading = new MutableLiveData<>(false); @@ -54,9 +57,16 @@ public class NotesListViewModel extends ViewModel { // 选中的笔记ID集合 private final HashSet selectedNoteIds = new HashSet<>(); + private final MutableLiveData> selectedIdsLiveData = new MutableLiveData<>(new HashSet<>()); + + // 是否处于多选模式 + private final MutableLiveData isSelectionMode = new MutableLiveData<>(false); // 当前文件夹ID - private long currentFolderId = Notes.ID_ROOT_FOLDER; + private long currentFolderId = Notes.ID_ALL_NOTES_FOLDER; // Default to "All" + + // 当前文件夹ID LiveData (用于UI监听) + private final MutableLiveData currentFolderIdLiveData = new MutableLiveData<>((long) Notes.ID_ALL_NOTES_FOLDER); // 文件夹路径LiveData(用于面包屑导航) private final MutableLiveData> folderPathLiveData = new MutableLiveData<>(); @@ -104,6 +114,30 @@ public class NotesListViewModel extends ViewModel { return errorMessage; } + /** + * 获取文件夹列表LiveData (顶部Tab) + */ + public MutableLiveData> getFoldersLiveData() { + return foldersLiveData; + } + + /** + * 获取是否处于多选模式 + */ + public MutableLiveData getIsSelectionMode() { + return isSelectionMode; + } + + /** + * 设置多选模式 + */ + public void setIsSelectionMode(boolean isSelection) { + isSelectionMode.postValue(isSelection); + if (!isSelection) { + clearSelection(); + } + } + /** * 加载笔记列表 *

@@ -114,8 +148,12 @@ public class NotesListViewModel extends ViewModel { */ public void loadNotes(long folderId) { this.currentFolderId = folderId; + this.currentFolderIdLiveData.postValue(folderId); isLoading.postValue(true); errorMessage.postValue(null); + + // 退出选择模式 + setIsSelectionMode(false); // 加载文件夹路径 repository.getFolderPath(folderId, new NotesRepository.Callback>() { @@ -129,8 +167,41 @@ public class NotesListViewModel extends ViewModel { Log.e(TAG, "Failed to load folder path", error); } }); + + // 加载子文件夹 (Category Tabs) - Always load root folders to keep tabs visible + repository.getSubFolders(Notes.ID_ROOT_FOLDER, new NotesRepository.Callback>() { + @Override + public void onSuccess(List folders) { + // Construct the display list with "All" and "Uncategorized" + List displayFolders = new ArrayList<>(); + + // 1. "All" Folder (Virtual) + NotesRepository.NoteInfo allFolder = new NotesRepository.NoteInfo(); + allFolder.setId(Notes.ID_ALL_NOTES_FOLDER); + allFolder.snippet = "所有"; // Name + displayFolders.add(allFolder); + + // 2. Real Folders (from DB) + if (folders != null) { + displayFolders.addAll(folders); + } + + // 3. "Uncategorized" Folder (Actually Root Folder) + NotesRepository.NoteInfo uncategorizedFolder = new NotesRepository.NoteInfo(); + uncategorizedFolder.setId(Notes.ID_ROOT_FOLDER); + uncategorizedFolder.snippet = "未分类"; // Custom Name for Root + displayFolders.add(uncategorizedFolder); + + foldersLiveData.postValue(displayFolders); + } + + @Override + public void onError(Exception error) { + Log.e(TAG, "Failed to load sub-folders", error); + } + }); - // 加载笔记 + // 加载笔记 (No folders) repository.getNotes(folderId, new NotesRepository.Callback>() { @Override public void onSuccess(List notes) { @@ -283,6 +354,14 @@ public class NotesListViewModel extends ViewModel { }); } + public MutableLiveData> getSelectedIdsLiveData() { + return selectedIdsLiveData; + } + + public void notifySelectionChanged() { + selectedIdsLiveData.postValue(new HashSet<>(selectedNoteIds)); + } + /** * 切换笔记选中状态 * @@ -295,6 +374,7 @@ public class NotesListViewModel extends ViewModel { } else { selectedNoteIds.remove(noteId); } + notifySelectionChanged(); } /** @@ -310,6 +390,7 @@ public class NotesListViewModel extends ViewModel { selectedNoteIds.add(note.getId()); } } + notifySelectionChanged(); } /** @@ -320,6 +401,7 @@ public class NotesListViewModel extends ViewModel { */ public void deselectAllNotes() { selectedNoteIds.clear(); + notifySelectionChanged(); } /** @@ -363,6 +445,13 @@ public class NotesListViewModel extends ViewModel { return currentFolderId; } + /** + * 获取当前文件夹ID LiveData + */ + public MutableLiveData getCurrentFolderIdLiveData() { + return currentFolderIdLiveData; + } + /** * 设置当前文件夹 * @@ -432,6 +521,7 @@ public class NotesListViewModel extends ViewModel { */ public void clearSelection() { selectedNoteIds.clear(); + notifySelectionChanged(); } /** @@ -745,4 +835,33 @@ public class NotesListViewModel extends ViewModel { selectedNoteIds.clear(); Log.d(TAG, "ViewModel cleared"); } -} + + /** + * 切换单个笔记的锁定状态 + * + * @param noteId 笔记ID + * @param isLocked 是否锁定 + */ + public void toggleNoteLock(long noteId, boolean isLocked) { + List ids = new ArrayList<>(); + ids.add(noteId); + + isLoading.postValue(true); + repository.batchLock(ids, isLocked, new NotesRepository.Callback() { + @Override + public void onSuccess(Integer rowsAffected) { + isLoading.postValue(false); + refreshNotes(); + Log.d(TAG, "Successfully toggled lock state for note: " + noteId); + } + + @Override + public void onError(Exception error) { + isLoading.postValue(false); + String message = "锁定操作失败: " + error.getMessage(); + errorMessage.postValue(message); + Log.e(TAG, message, error); + } + }); + } +} \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/activity_settings.xml b/src/Notesmaster/app/src/main/res/layout/activity_settings.xml index 791a380..b331ecc 100644 --- a/src/Notesmaster/app/src/main/res/layout/activity_settings.xml +++ b/src/Notesmaster/app/src/main/res/layout/activity_settings.xml @@ -1,15 +1,5 @@ - - - - - - - + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/activity_task_list.xml b/src/Notesmaster/app/src/main/res/layout/activity_task_list.xml index 51fbca5..7c108ae 100644 --- a/src/Notesmaster/app/src/main/res/layout/activity_task_list.xml +++ b/src/Notesmaster/app/src/main/res/layout/activity_task_list.xml @@ -5,47 +5,12 @@ android:layout_height="match_parent" android:background="@color/background_color"> - - - - - - - - - - + android:paddingBottom="80dp"/> \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/note_item.xml b/src/Notesmaster/app/src/main/res/layout/note_item.xml index a479806..1718f1a 100644 --- a/src/Notesmaster/app/src/main/res/layout/note_item.xml +++ b/src/Notesmaster/app/src/main/res/layout/note_item.xml @@ -1,122 +1,122 @@ - - - - + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + app:cardCornerRadius="16dp" + app:cardElevation="2dp" + app:cardBackgroundColor="@color/bg_white"> - + android:orientation="vertical" + android:padding="16dp"> - - + + + + + + + + + - - + android:visibility="visible" /> - - - - - - - - - - + - - - - - + android:layout_marginTop="12dp" + android:orientation="horizontal" + android:gravity="center_vertical"> + + + + + + + + + - - + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/note_list.xml b/src/Notesmaster/app/src/main/res/layout/note_list.xml index 3d88391..4571f8f 100644 --- a/src/Notesmaster/app/src/main/res/layout/note_list.xml +++ b/src/Notesmaster/app/src/main/res/layout/note_list.xml @@ -1,106 +1,29 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/sidebar_layout.xml b/src/Notesmaster/app/src/main/res/layout/sidebar_layout.xml index 79d28b6..b762932 100644 --- a/src/Notesmaster/app/src/main/res/layout/sidebar_layout.xml +++ b/src/Notesmaster/app/src/main/res/layout/sidebar_layout.xml @@ -10,7 +10,7 @@ android:layout_height="match_parent" android:layout_gravity="start" android:orientation="vertical" - android:background="@android:color/white" + android:background="@color/bg_white" android:elevation="8dp"> @@ -19,7 +19,7 @@ android:layout_height="wrap_content" android:orientation="horizontal" android:padding="16dp" - android:background="@android:color/white" + android:background="@color/bg_white" android:elevation="4dp"> @@ -30,7 +30,8 @@ android:background="?attr/selectableItemBackgroundBorderless" android:contentDescription="关闭侧栏" android:padding="8dp" - android:src="@android:drawable/ic_menu_close_clear_cancel" /> + android:src="@android:drawable/ic_menu_close_clear_cancel" + android:tint="@color/text_color_primary" /> + android:src="@android:drawable/ic_input_add" + android:tint="@color/text_color_primary" /> @@ -64,7 +66,7 @@ android:drawablePadding="12dp" android:text="@string/root_folder_name" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> @@ -105,7 +107,7 @@ android:drawablePadding="12dp" android:text="@string/menu_sync" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> @@ -119,7 +121,7 @@ android:drawablePadding="12dp" android:text="@string/menu_login" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> @@ -133,7 +135,7 @@ android:drawablePadding="12dp" android:text="@string/menu_export" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> @@ -147,7 +149,7 @@ android:drawablePadding="12dp" android:text="@string/menu_templates" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> @@ -161,7 +163,7 @@ android:drawablePadding="12dp" android:text="@string/menu_settings" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> @@ -182,7 +184,7 @@ android:drawablePadding="12dp" android:text="@string/menu_trash" android:textSize="16sp" - android:textColor="@android:color/black" + android:textColor="@color/text_color_primary" android:background="?attr/selectableItemBackground" android:gravity="center_vertical" /> - + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/task_list_item.xml b/src/Notesmaster/app/src/main/res/layout/task_list_item.xml index 65706b2..2d9817c 100644 --- a/src/Notesmaster/app/src/main/res/layout/task_list_item.xml +++ b/src/Notesmaster/app/src/main/res/layout/task_list_item.xml @@ -1,70 +1,84 @@ - - - + android:layout_marginStart="12dp" + android:layout_marginEnd="12dp" + android:layout_marginTop="6dp" + android:layout_marginBottom="6dp" + app:cardCornerRadius="16dp" + app:cardElevation="2dp" + app:cardBackgroundColor="@color/bg_white"> + android:orientation="horizontal" + android:padding="16dp" + android:gravity="center_vertical"> - + - + android:layout_weight="1" + android:orientation="vertical" + android:paddingStart="12dp" + android:paddingEnd="8dp"> + + android:textSize="16sp" + android:textColor="@color/text_color_primary" + android:ellipsize="end" + android:maxLines="2" /> - + android:orientation="horizontal" + android:layout_marginTop="4dp" + android:gravity="center_vertical"> + + + + + - - - + + - + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/values/colors.xml b/src/Notesmaster/app/src/main/res/values/colors.xml index c07b5f2..bffeabd 100644 --- a/src/Notesmaster/app/src/main/res/values/colors.xml +++ b/src/Notesmaster/app/src/main/res/values/colors.xml @@ -19,23 +19,23 @@ #335b5b5b #FFFFFF #000000 - #263238 - #FFFFFF - #E8E8E8 - #000000 - #808080 + #F7F7F7 + #212121 + #F7F7F7 + #212121 + #757575 #FFC107 - #000000 - #808080 + #212121 + #757575 - #FFF9C4 - #B3E5FC + #FFF8D6 + #E3F2FD #FFFFFF - #C8E6C9 - #FFCDD2 + #E0F2F1 + #FCE4EC #212121 diff --git a/src/Notesmaster/app/src/main/res/values/strings.xml b/src/Notesmaster/app/src/main/res/values/strings.xml index 993846d..1050839 100644 --- a/src/Notesmaster/app/src/main/res/values/strings.xml +++ b/src/Notesmaster/app/src/main/res/values/strings.xml @@ -179,4 +179,13 @@ Save as template Picture Rich Text + + + 小米便签-全局速记服务,用于监听剪贴板和应用来源。 + 需要悬浮窗权限 + 全局速记胶囊需要悬浮窗权限才能显示在其他应用上层。 + 需要无障碍服务权限 + 全局速记胶囊需要无障碍服务权限来监听剪贴板和获取来源应用。 + 全局速记胶囊 + 开启侧边悬浮胶囊,支持跨应用拖拽和剪贴板速记 diff --git a/src/Notesmaster/app/src/main/res/values/styles.xml b/src/Notesmaster/app/src/main/res/values/styles.xml index 67bc75f..4d1e7fd 100644 --- a/src/Notesmaster/app/src/main/res/values/styles.xml +++ b/src/Notesmaster/app/src/main/res/values/styles.xml @@ -36,6 +36,15 @@ + +