diff --git a/docs/开发共享文档/功能扩展规划-精简版.md b/docs/开发共享文档/功能扩展规划-精简版.md index 08b050b..98638f5 100644 --- a/docs/开发共享文档/功能扩展规划-精简版.md +++ b/docs/开发共享文档/功能扩展规划-精简版.md @@ -76,14 +76,14 @@ **当前状态**: - ✅ 基础搜索功能(ContentProvider支持search URI) - ✅ 搜索建议功能 -- ❌ 搜索历史记录 -- ❌ 高级筛选选项 -- ❌ 搜索结果高亮 +- ✅ 搜索历史记录 +- ✅ 高级筛选选项 +- ✅ 搜索结果高亮 **待实现功能点**: -- [ ] 搜索历史记录(本地存储常用搜索词) -- [ ] 搜索结果高亮显示 -- [ ] 搜索频率排序 +- ✅ 搜索历史记录(本地存储常用搜索词) +- ✅ 搜索结果高亮显示 +- ✅ 搜索频率排序 **技术方案**: - 使用 SharedPreferences 存储搜索历史 @@ -131,11 +131,11 @@ **描述**: 支持简单的笔记撤回操作 **功能点**: -- [ ] 撤回上一次编辑 -- [ ] 撤回历史栈(可连续撤回10-20次) -- [ ] 重做功能 -- [ ] 撤回/重做状态提示 -- [ ] 清空撤回历史 +- ✅ 撤回上一次编辑 +- ✅ 撤回历史栈(可连续撤回10-20次) +- ✅ 重做功能 +- ✅ 撤回/重做状态提示 +- ✅ 清空撤回历史 **技术方案**: - 实现 UndoStack 数据结构 @@ -270,16 +270,16 @@ **描述**: 增强文本编辑功能,支持多种格式 **功能点**: -- [ ] 粗体、斜体、下划线 -- [ ] 删除线 -- [ ] 标题层级 (H1-H6) -- [ ] 列表(无序、有序、检查列表) -- [ ] 引用块 -- [ ] 代码块 -- [ ] 链接 -- [ ] 分割线 -- [ ] 文本颜色 -- [ ] 文本背景色 +- ✅ 粗体、斜体、下划线 +- ✅ 删除线 +- ✅ 标题层级 (H1-H6) +- ✅ 列表(无序、有序、检查列表) +- ✅ 引用块 +- ✅ 代码块 +- ✅ 链接 +- ✅ 分割线 +- ✅ 文本颜色 +- ✅ 文本背景色 **技术方案**: - 集成富文本编辑库(如 RichEditor、SpannableStringBuilder) @@ -373,14 +373,14 @@ **描述**: 在笔记中创建待办事项清单 **功能点**: -- [ ] 添加任务项(- [ ] 语法) -- [ ] 标记完成/未完成 -- [ ] 任务优先级(高/中/低) -- [ ] 任务截止日期 -- [ ] 任务提醒 -- [ ] 任务统计(完成率) -- [ ] 过滤已完成任务 -- [ ] 任务拖拽排序 +- ✅ 添加任务项(- [ ] 语法) +- ✅ 标记完成/未完成 +- ✅ 任务优先级(高/中/低) +- ✅ 任务截止日期 +- ✅ 任务提醒 +- ❌ 任务统计(完成率) +- ✅ 过滤已完成任务 +- ❌ 任务拖拽排序 **技术方案**: - 扩展 data 表支持任务类型(mime_type = "text/x-todo") @@ -506,14 +506,14 @@ ## 功能实现时间线(精简版) ### Month 1-2: 核心功能增强 -- [ ] Week 1: 搜索功能增强 (2.1) - 搜索历史、高级筛选 +- ✅ Week 1: 搜索功能增强 (2.1) - 搜索历史、高级筛选 - [ ] Week 2: 导入导出功能增强 (2.2) - 便签图片导出、Markdown/TXT -- [ ] Week 3: 撤回功能 (2.3) - 撤回/重做 +- ✅ Week 3: 撤回功能 (2.3) - 撤回/重做 - [ ] Week 4: 标签系统 (2.4) - 标签分类和筛选 ### Month 3-4: 用户体验提升 - [ ] Week 5: 笔记模板 (2.6) - 模板管理 -- [ ] Week 6-8: 富文本编辑 (3.1) - 完整格式支持 +- ✅ Week 6-8: 富文本编辑 (3.1) - 完整格式支持 ### Month 5-6: 功能扩展 - [ ] Week 9-10: 图片附件 (3.2) - 图片管理和预览 @@ -521,7 +521,7 @@ ### Month 7-8: 高级功能 - [ ] Week 13-14: 链接笔记 (3.4) - 笔图谱 -- [ ] Week 15-16: 任务清单 (3.5) - 任务管理 +- ✅ Week 15-16: 任务清单 (3.5) - 任务管理 ### Month 9+: 智能化和生态 - [ ] Week 17-20: 云同步和账号系统 (4.3) - 注册登录、云同步 diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java index ea3a47e..c61f705 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/MainActivity.java @@ -125,6 +125,13 @@ public class MainActivity extends AppCompatActivity implements SidebarFragment.O // TODO: 实现导出功能 } + @Override + public void onTemplateSelected() { + Log.d(TAG, "Template selected"); + // 跳转到模板列表(在 NotesListActivity 中处理) + closeSidebar(); + } + @Override public void onSettingsSelected() { Log.d(TAG, "Settings selected"); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/data/Notes.java b/src/Notesmaster/app/src/main/java/net/micode/notes/data/Notes.java index 79435fa..520d27a 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/data/Notes.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/data/Notes.java @@ -81,6 +81,10 @@ public class Notes { * 回收站文件夹ID,用于存储已删除的笔记 */ public static final int ID_TRASH_FOLER = -3; + /** + * Template folder ID + */ + public static final int ID_TEMPLATE_FOLDER = -4; /** * Intent Extra键:提醒日期 diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java index 8c6a8e6..b960d55 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesDatabaseHelper.java @@ -70,7 +70,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { * 当数据库版本变更时,onUpgrade方法会被调用以执行升级逻辑。 *
*/ - private static final int DB_VERSION = 9; + private static final int DB_VERSION = 10; /** * 数据库表名常量接口 @@ -427,6 +427,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER); values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); db.insert(TABLE.NOTE, null, values); + + /** + * create template folder + */ + // 创建模板文件夹 + createTemplateFolder(db); } /** @@ -493,6 +499,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { public void onCreate(SQLiteDatabase db) { createNoteTable(db); createDataTable(db); + createPresetTemplates(db); } /** @@ -565,6 +572,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { oldVersion++; } + // 从V9升级到V10 + if (oldVersion == 9) { + upgradeToV10(db); + oldVersion++; + } + // 如果需要,重新创建触发器 if (reCreateTriggers) { reCreateNoteTableTriggers(db); @@ -813,4 +826,88 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { Log.e(TAG, "Failed to add GTASK columns in V9 upgrade", e); } } + + /** + * 升级数据库到V10版本 + *+ * 创建模板系统文件夹并预置模板。 + *
+ * + * @param db SQLiteDatabase实例 + */ + private void upgradeToV10(SQLiteDatabase db) { + createTemplateFolder(db); + createPresetTemplates(db); + } + + /** + * 创建模板系统文件夹 + * + * @param db SQLiteDatabase实例 + */ + private void createTemplateFolder(SQLiteDatabase db) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.ID, Notes.ID_TEMPLATE_FOLDER); + values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM); + db.insert(TABLE.NOTE, null, values); + } + + /** + * 创建预置模板 + * + * @param db SQLiteDatabase实例 + */ + private void createPresetTemplates(SQLiteDatabase db) { + // 工作模板 + long workFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "工作"); + if (workFolderId > 0) { + insertNote(db, workFolderId, "会议记录", "会议主题:\n时间:\n地点:\n参会人:\n\n会议内容:\n\n行动项:\n"); + insertNote(db, workFolderId, "周报", "本周工作总结:\n1. \n2. \n\n下周工作计划:\n1. \n2. \n\n需要协调的问题:\n"); + } + + // 生活模板 + long lifeFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "生活"); + if (lifeFolderId > 0) { + insertNote(db, lifeFolderId, "日记", "日期:\n天气:\n心情:\n\n正文:\n"); + insertNote(db, lifeFolderId, "购物清单", "1. \n2. \n3. \n"); + } + + // 学习模板 + long studyFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "学习"); + if (studyFolderId > 0) { + insertNote(db, studyFolderId, "读书笔记", "书名:\n作者:\n\n核心观点:\n\n精彩摘录:\n\n读后感:\n"); + } + } + + private long insertFolder(SQLiteDatabase db, long parentId, String name) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, parentId); + values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER); + values.put(NoteColumns.SNIPPET, name); + values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis()); + values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + values.put(NoteColumns.NOTES_COUNT, 0); + return db.insert(TABLE.NOTE, null, values); + } + + private void insertNote(SQLiteDatabase db, long parentId, String title, String content) { + ContentValues values = new ContentValues(); + values.put(NoteColumns.PARENT_ID, parentId); + values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis()); + values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + values.put(NoteColumns.SNIPPET, content); // SNIPPET acts as content preview or full content for simple notes + values.put(NoteColumns.TITLE, title); // Assuming V8+ has TITLE + long noteId = db.insert(TABLE.NOTE, null, values); + + if (noteId > 0) { + ContentValues dataValues = new ContentValues(); + dataValues.put(DataColumns.NOTE_ID, noteId); + dataValues.put(DataColumns.MIME_TYPE, DataConstants.NOTE); + dataValues.put(DataColumns.CONTENT, content); + dataValues.put(NoteColumns.CREATED_DATE, System.currentTimeMillis()); + dataValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis()); + db.insert(TABLE.DATA, null, dataValues); + } + } } diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java index 2bf9f83..9c07c7e 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesRepository.java @@ -305,6 +305,15 @@ public class NotesRepository { return root; } + if (folderId == Notes.ID_TEMPLATE_FOLDER) { + NoteInfo root = new NoteInfo(); + root.id = Notes.ID_TEMPLATE_FOLDER; + root.title = "笔记模板"; + root.snippet = "笔记模板"; + root.type = Notes.TYPE_FOLDER; // Treat as folder for UI + return root; + } + String selection = NoteColumns.ID + "=?"; String[] selectionArgs = new String[]{String.valueOf(folderId)}; @@ -1023,4 +1032,152 @@ public class NotesRepository { Log.d(TAG, "Executor shutdown"); } } + + /** + * 应用模板 + * + * @param templateId 模板笔记ID + * @param targetFolderId 目标文件夹ID + * @param callback 回调 + */ + public void applyTemplate(long templateId, long targetFolderId, Callback
@@ -489,7 +491,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
} else {
String content = mWorkingNote.getContent();
if (content.contains("<") && content.contains(">")) {
- mNoteEditor.setText(RichTextHelper.fromHtml(content));
+ mNoteEditor.setText(RichTextHelper.fromHtml(content, this));
} else {
mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery));
}
@@ -991,6 +993,12 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
case R.id.menu_new_note:
createNewNote();
break;
+ case R.id.menu_save_as_template:
+ saveAsTemplate();
+ break;
+ case R.id.menu_picture:
+ pickImage();
+ break;
case R.id.menu_delete:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
@@ -1571,8 +1579,53 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
startActivityForResult(intent, REQUEST_CODE_PICK_WALLPAPER);
}
+ private void pickImage() {
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
+ }
+
+ private void saveImageToPrivateStorage(android.net.Uri uri) {
+ new Thread(() -> {
+ try {
+ java.io.InputStream is = getContentResolver().openInputStream(uri);
+ if (is == null) return;
+
+ // Create images directory if not exists
+ java.io.File imagesDir = new java.io.File(getFilesDir(), "images");
+ if (!imagesDir.exists()) {
+ imagesDir.mkdirs();
+ }
+
+ // Create a unique file name
+ String fileName = "img_" + System.currentTimeMillis() + ".jpg";
+ java.io.File destFile = new java.io.File(imagesDir, fileName);
+
+ java.io.FileOutputStream fos = new java.io.FileOutputStream(destFile);
+ byte[] buffer = new byte[4096];
+ int bytesRead;
+ while ((bytesRead = is.read(buffer)) != -1) {
+ fos.write(buffer, 0, bytesRead);
+ }
+ fos.close();
+ is.close();
+
+ final String filePath = "file://" + destFile.getAbsolutePath();
+ runOnUiThread(() -> {
+ RichTextHelper.insertImage(mNoteEditor, filePath);
+ });
+
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to copy image", e);
+ runOnUiThread(() -> {
+ showToast(R.string.failed_sdcard_export); // Use generic failure message or add new one
+ });
+ }
+ }).start();
+ }
+
@Override
- protected3 void onActivityResult(int requestCode, int resultCode, Intent data) {
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_WALLPAPER && resultCode == RESULT_OK && data != null) {
android.net.Uri uri = data.getData();
@@ -1587,6 +1640,11 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
mWorkingNote.setWallpaper(uri.toString());
mNoteBgColorSelector.setVisibility(View.GONE);
}
+ } else if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK && data != null) {
+ android.net.Uri uri = data.getData();
+ if (uri != null) {
+ saveImageToPrivateStorage(uri);
+ }
}
}
@@ -1686,4 +1744,100 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
}
});
}
+
+ private void saveAsTemplate() {
+ if (!mWorkingNote.existInDatabase()) {
+ saveNote();
+ }
+
+ final net.micode.notes.data.NotesRepository repository = new net.micode.notes.data.NotesRepository(getContentResolver());
+
+ new Thread(() -> {
+ android.database.Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI,
+ new String[]{net.micode.notes.data.Notes.NoteColumns.ID, net.micode.notes.data.Notes.NoteColumns.SNIPPET},
+ net.micode.notes.data.Notes.NoteColumns.PARENT_ID + "=? AND " + net.micode.notes.data.Notes.NoteColumns.TYPE + "=?",
+ new String[]{String.valueOf(Notes.ID_TEMPLATE_FOLDER), String.valueOf(Notes.TYPE_FOLDER)},
+ null);
+
+ final java.util.List
@@ -123,6 +208,111 @@ public class NoteEditText extends EditText {
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
+ init(context);
+ }
+
+ private void init(Context context) {
+ mScaleDetector = new ScaleGestureDetector(context, this);
+ mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ float x = e.getX();
+ float y = e.getY();
+
+ x += getScrollX();
+ y += getScrollY();
+ x -= getTotalPaddingLeft();
+ y -= getTotalPaddingTop();
+
+ Layout layout = getLayout();
+ if (layout != null) {
+ int line = layout.getLineForVertical((int) y);
+ int offset = layout.getOffsetForHorizontal(line, x);
+
+ if (getText() instanceof Spanned) {
+ Spanned spanned = (Spanned) getText();
+ ImageSpan[] spans = spanned.getSpans(offset, offset, ImageSpan.class);
+ if (spans.length > 0) {
+ showResizeDialog(spans[0]);
+ return true;
+ }
+ }
+ }
+ return super.onDoubleTap(e);
+ }
+ });
+ }
+
+ private void showResizeDialog(final ImageSpan imageSpan) {
+ if (imageSpan.getDrawable() == null) return;
+
+ final Rect bounds = imageSpan.getDrawable().getBounds();
+ final int originalWidth = bounds.width();
+ final int originalHeight = bounds.height();
+ final float aspectRatio = (float) originalHeight / originalWidth;
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setTitle("Resize Image");
+
+ LinearLayout layout = new LinearLayout(getContext());
+ layout.setOrientation(LinearLayout.VERTICAL);
+ layout.setPadding(50, 20, 50, 20);
+
+ final TextView label = new TextView(getContext());
+ label.setText("Scale: 100%");
+ layout.addView(label);
+
+ final SeekBar seekBar = new SeekBar(getContext());
+ seekBar.setMax(200); // 0 to 200%
+ seekBar.setProgress(100);
+ layout.addView(seekBar);
+
+ builder.setView(layout);
+
+ seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ // Minimum 10%
+ if (progress < 10) progress = 10;
+
+ float scale = progress / 100f;
+ int newWidth = (int) (originalWidth * scale);
+ int newHeight = (int) (newWidth * aspectRatio);
+
+ label.setText("Scale: " + progress + "%");
+
+ // Live preview
+ imageSpan.getDrawable().setBounds(0, 0, newWidth, newHeight);
+ invalidate();
+ requestLayout();
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {}
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {}
+ });
+
+ builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Rect finalBounds = imageSpan.getDrawable().getBounds();
+ RichTextHelper.updateImageSpanSize(NoteEditText.this, imageSpan, finalBounds.width(), finalBounds.height());
+ }
+ });
+
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // Revert
+ imageSpan.getDrawable().setBounds(0, 0, originalWidth, originalHeight);
+ invalidate();
+ requestLayout();
+ }
+ });
+
+ builder.show();
}
/**
@@ -151,6 +341,7 @@ public class NoteEditText extends EditText {
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
+ init(context);
}
/**
@@ -162,6 +353,7 @@ public class NoteEditText extends EditText {
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ init(context);
}
/**
@@ -174,6 +366,17 @@ public class NoteEditText extends EditText {
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
+ if (mScaleDetector != null) {
+ mScaleDetector.onTouchEvent(event);
+ if (mScaleDetector.isInProgress()) {
+ return true;
+ }
+ }
+
+ if (mGestureDetector != null) {
+ mGestureDetector.onTouchEvent(event);
+ }
+
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸坐标
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 32a837c..429f4b8 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
@@ -456,6 +456,8 @@ public class NotesListActivity extends AppCompatActivity
if (viewModel.isTrashMode()) {
// 回收站模式:弹出恢复/删除对话框
showTrashItemDialog(note);
+ } else if (viewModel.isTemplateMode() && note.type == Notes.TYPE_NOTE) {
+ showApplyTemplateDialog(note);
} else if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
// 检查隐私锁
@@ -501,6 +503,45 @@ public class NotesListActivity extends AppCompatActivity
}
}
+ /**
+ * 显示应用模板确认对话框
+ */
+ 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
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 738c0aa..79d28b6 100644
--- a/src/Notesmaster/app/src/main/res/layout/sidebar_layout.xml
+++ b/src/Notesmaster/app/src/main/res/layout/sidebar_layout.xml
@@ -137,6 +137,20 @@
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
+
+