From 48344838f156024370d7a2303a38e5b64eeb491e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=B0=94=E4=BF=8A?= Date: Sun, 1 Feb 2026 10:10:35 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E8=83=B6=E5=9B=8A=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/src/main/AndroidManifest.xml | 6 ++ .../java/net/micode/notes/MainActivity.java | 8 ++ .../micode/notes/data/NotesRepository.java | 5 +- .../net/micode/notes/model/WorkingNote.java | 7 +- .../micode/notes/ui/CapsuleListActivity.java | 33 ++++++++ .../micode/notes/ui/CapsuleListFragment.java | 61 ++++++++++---- .../micode/notes/ui/NotesListActivity.java | 4 + .../net/micode/notes/ui/SidebarFragment.java | 12 +++ .../main/res/layout/activity_capsule_list.xml | 5 ++ .../src/main/res/layout/fragment_sidebar.xml | 28 +++++++ .../app/src/main/res/layout/item_capsule.xml | 79 ++++++++++++------- 11 files changed, 198 insertions(+), 50 deletions(-) create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListActivity.java create mode 100644 src/Notesmaster/app/src/main/res/layout/activity_capsule_list.xml diff --git a/src/Notesmaster/app/src/main/AndroidManifest.xml b/src/Notesmaster/app/src/main/AndroidManifest.xml index b4152a3..ff73964 100644 --- a/src/Notesmaster/app/src/main/AndroidManifest.xml +++ b/src/Notesmaster/app/src/main/AndroidManifest.xml @@ -110,6 +110,12 @@ android:exported="false" /> + + { if (getContext() == null) return; - // Query notes in CAPSULE folder - String selection = Notes.NoteColumns.PARENT_ID + "=?"; - String[] selectionArgs = new String[]{String.valueOf(Notes.ID_CAPSULE_FOLDER)}; - + // Query notes in CAPSULE folder. + // Join with Data table to get DATA3 (source package) Cursor cursor = getContext().getContentResolver().query( Notes.CONTENT_NOTE_URI, null, - selection, - selectionArgs, + Notes.NoteColumns.PARENT_ID + "=?", + new String[]{String.valueOf(Notes.ID_CAPSULE_FOLDER)}, Notes.NoteColumns.MODIFIED_DATE + " DESC" ); @@ -73,14 +90,18 @@ public class CapsuleListFragment extends Fragment { String snippet = cursor.getString(cursor.getColumnIndexOrThrow(Notes.NoteColumns.SNIPPET)); long modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(Notes.NoteColumns.MODIFIED_DATE)); - // We need to fetch DATA3 (source) which is in DATA table. - // For performance, we might do a join or lazy load. - // For now, let's just use snippet and date. - // To get Source, we really should query DATA table or use a projection if CONTENT_NOTE_URI supports joining. - // NotesProvider usually joins. Let's check NoteColumns. - // Notes.DataColumns.DATA3 is NOT in NoteColumns. + // Try to get source from projection (if joined) or query separately + String source = ""; + try { + int sourceIdx = cursor.getColumnIndex(Notes.DataColumns.DATA3); + if (sourceIdx != -1) { + source = cursor.getString(sourceIdx); + } + } catch (Exception e) { + // Not joined, ignore for now or lazy load + } - items.add(new CapsuleItem(id, snippet, modifiedDate, "Loading source...")); + items.add(new CapsuleItem(id, snippet, modifiedDate, source)); } cursor.close(); } @@ -95,6 +116,13 @@ public class CapsuleListFragment extends Fragment { }).start(); } + private void openNoteEditor(long noteId) { + Intent intent = new Intent(getActivity(), NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, noteId); + startActivity(intent); + } + private static class CapsuleItem { long id; String summary; @@ -133,15 +161,14 @@ public class CapsuleListFragment extends Fragment { holder.tvTime.setText(sdf.format(new Date(item.time))); if (item.source != null && !item.source.isEmpty()) { - holder.tvSource.setText("Source: " + item.source); + holder.tvSource.setText(item.source); holder.tvSource.setVisibility(View.VISIBLE); } else { holder.tvSource.setVisibility(View.GONE); } holder.itemView.setOnClickListener(v -> { - // Open Note Edit - // We need to implement this + openNoteEditor(item.id); }); } 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 a58beb3..fa8b2de 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 @@ -368,6 +368,10 @@ public class NotesListActivity extends AppCompatActivity implements SidebarFragm @Override public void onExportSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); Toast.makeText(this, "导出功能待实现", Toast.LENGTH_SHORT).show(); } @Override public void onTemplateSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); viewModel.enterFolder(Notes.ID_TEMPLATE_FOLDER); } @Override public void onSettingsSelected() { binding.drawerLayout.closeDrawer(GravityCompat.START); startActivity(new Intent(this, SettingsActivity.class)); } + @Override public void onCapsuleSelected() { + binding.drawerLayout.closeDrawer(GravityCompat.START); + startActivity(new Intent(this, CapsuleListActivity.class)); + } @Override public void onCreateFolder() { binding.drawerLayout.closeDrawer(GravityCompat.START); /* Show dialog */ } @Override public void onCloseSidebar() { binding.drawerLayout.closeDrawer(GravityCompat.START); } @Override public void onRenameFolder(long folderId) { /* Handle rename */ } 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 f589a92..f61cd7d 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 @@ -71,6 +71,7 @@ public class SidebarFragment extends Fragment { private LinearLayout menuTemplates; private LinearLayout menuExport; private LinearLayout menuSettings; + private LinearLayout menuCapsule; private LinearLayout menuLogin; private LinearLayout menuLogout; @@ -106,6 +107,7 @@ public class SidebarFragment extends Fragment { void onExportSelected(); void onTemplateSelected(); void onSettingsSelected(); + void onCapsuleSelected(); void onCreateFolder(); void onCloseSidebar(); void onRenameFolder(long folderId); @@ -170,6 +172,7 @@ public class SidebarFragment extends Fragment { menuTemplates = view.findViewById(R.id.menu_templates); menuExport = view.findViewById(R.id.menu_export); menuSettings = view.findViewById(R.id.menu_settings); + menuCapsule = view.findViewById(R.id.menu_capsule); menuLogin = view.findViewById(R.id.menu_login); menuLogout = view.findViewById(R.id.menu_logout); @@ -237,6 +240,15 @@ public class SidebarFragment extends Fragment { } }); + if (menuCapsule != null) { + menuCapsule.setOnClickListener(v -> { + if (listener != null) { + listener.onCapsuleSelected(); + listener.onCloseSidebar(); + } + }); + } + menuLogin.setOnClickListener(v -> { if (listener != null) { listener.onLoginSelected(); diff --git a/src/Notesmaster/app/src/main/res/layout/activity_capsule_list.xml b/src/Notesmaster/app/src/main/res/layout/activity_capsule_list.xml new file mode 100644 index 0000000..244af55 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/activity_capsule_list.xml @@ -0,0 +1,5 @@ + + diff --git a/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml b/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml index 5d4d1b4..34683d4 100644 --- a/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml +++ b/src/Notesmaster/app/src/main/res/layout/fragment_sidebar.xml @@ -163,6 +163,34 @@ + + + + + + + + + - + android:layout_marginHorizontal="12dp" + android:layout_marginVertical="6dp" + app:cardCornerRadius="16dp" + app:cardElevation="2dp" + app:cardBackgroundColor="@android:color/white"> - - - + android:orientation="vertical" + android:padding="16dp" + android:background="?android:attr/selectableItemBackground"> - + + + + + + + + + + + - + -- 2.34.1 From 574573cad8432ad831e070000bd5f20c7d46dd41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=B0=94=E4=BF=8A?= Date: Sun, 1 Feb 2026 11:20:34 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E7=AC=94=E8=AE=B0=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/net/micode/notes/data/Notes.java | 5 + .../notes/data/NotesDatabaseHelper.java | 14 +- .../net/micode/notes/data/NotesProvider.java | 2 +- .../micode/notes/data/NotesRepository.java | 27 +- .../net/micode/notes/model/WorkingNote.java | 60 +- .../java/net/micode/notes/tool/DataUtils.java | 16 +- .../net/micode/notes/tool/ResourceParser.java | 16 +- .../net/micode/notes/ui/NoteEditActivity.java | 215 +++--- .../net/micode/notes/ui/NoteItemData.java | 4 +- .../micode/notes/ui/NoteSearchActivity.java | 38 +- .../micode/notes/ui/NotesListFragment.java | 24 + .../net/micode/notes/ui/NotesListItem.java | 6 +- .../micode/notes/ui/NotesRecyclerAdapter.java | 2 +- .../notes/viewmodel/NotesListViewModel.java | 80 ++- .../src/main/res/drawable/bg_bottom_sheet.xml | 7 + .../src/main/res/drawable/preset_forest.xml | 5 + .../src/main/res/drawable/preset_lavender.xml | 5 + .../src/main/res/drawable/preset_ocean.xml | 5 + .../src/main/res/drawable/preset_sunset.xml | 5 + .../res/layout/dialog_background_selector.xml | 69 ++ .../app/src/main/res/layout/note_edit.xml | 658 ++++++------------ .../app/src/main/res/values/colors.xml | 10 + 22 files changed, 667 insertions(+), 606 deletions(-) create mode 100644 src/Notesmaster/app/src/main/res/drawable/bg_bottom_sheet.xml create mode 100644 src/Notesmaster/app/src/main/res/drawable/preset_forest.xml create mode 100644 src/Notesmaster/app/src/main/res/drawable/preset_lavender.xml create mode 100644 src/Notesmaster/app/src/main/res/drawable/preset_ocean.xml create mode 100644 src/Notesmaster/app/src/main/res/drawable/preset_sunset.xml create mode 100644 src/Notesmaster/app/src/main/res/layout/dialog_background_selector.xml 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 ea474ab..479e78e 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 @@ -62,6 +62,11 @@ public class Notes { */ public static final int TYPE_TASK = 3; + /** + * 模板笔记类型 + */ + public static final int TYPE_TEMPLATE = 4; + /** * 以下ID是系统文件夹的标识符 * {@link Notes#ID_ROOT_FOLDER } 是默认文件夹 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 92bfc6c..b130c5b 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 @@ -982,21 +982,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { // 工作模板 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"); + insertNote(db, workFolderId, "会议记录", "会议主题:\n时间:\n地点:\n参会人:\n\n会议内容:\n\n行动项:\n", Notes.TYPE_TEMPLATE); + insertNote(db, workFolderId, "周报", "本周工作总结:\n1. \n2. \n\n下周工作计划:\n1. \n2. \n\n需要协调的问题:\n", Notes.TYPE_TEMPLATE); } // 生活模板 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"); + insertNote(db, lifeFolderId, "日记", "日期:\n天气:\n心情:\n\n正文:\n", Notes.TYPE_TEMPLATE); + insertNote(db, lifeFolderId, "购物清单", "1. \n2. \n3. \n", Notes.TYPE_TEMPLATE); } // 学习模板 long studyFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "学习"); if (studyFolderId > 0) { - insertNote(db, studyFolderId, "读书笔记", "书名:\n作者:\n\n核心观点:\n\n精彩摘录:\n\n读后感:\n"); + insertNote(db, studyFolderId, "读书笔记", "书名:\n作者:\n\n核心观点:\n\n精彩摘录:\n\n读后感:\n", Notes.TYPE_TEMPLATE); } } @@ -1012,10 +1012,10 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper { return db.insert(TABLE.NOTE, null, values); } - private void insertNote(SQLiteDatabase db, long parentId, String title, String content) { + private void insertNote(SQLiteDatabase db, long parentId, String title, String content, int type) { ContentValues values = new ContentValues(); values.put(NoteColumns.PARENT_ID, parentId); - values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + values.put(NoteColumns.TYPE, type); 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 diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesProvider.java b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesProvider.java index aa2cf34..084d09a 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesProvider.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/data/NotesProvider.java @@ -168,7 +168,7 @@ public class NotesProvider extends ContentProvider { + " FROM " + TABLE.NOTE + " WHERE " + NoteColumns.SNIPPET + " LIKE ?" + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER - + " AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE; + + " AND (" + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " OR " + NoteColumns.TYPE + "=" + Notes.TYPE_TEMPLATE + ")"; /** * 创建Content Provider 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 c4282fc..169d437 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 @@ -88,6 +88,10 @@ public class NotesRepository { return parentId; } + public void setParentId(long parentId) { + this.parentId = parentId; + } + public String getNoteDataValue() { return snippet; } @@ -308,11 +312,17 @@ public class NotesRepository { selection = NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + " AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER; selectionArgs = null; + } else if (folderId == Notes.ID_TEMPLATE_FOLDER) { + // Special case for template folder: show all templates regardless of category + selection = NoteColumns.TYPE + "=" + Notes.TYPE_TEMPLATE; + 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; + // In a sub-folder, show both normal notes and templates if they exist there + selection = NoteColumns.PARENT_ID + "=? AND (" + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE + + " OR " + NoteColumns.TYPE + "=" + Notes.TYPE_TEMPLATE + ")"; selectionArgs = new String[]{String.valueOf(folderId)}; } @@ -605,8 +615,19 @@ public class NotesRepository { ContentValues values = new ContentValues(); long currentTime = System.currentTimeMillis(); + int type = Notes.TYPE_NOTE; + if (folderId == Notes.ID_TEMPLATE_FOLDER) { + type = Notes.TYPE_TEMPLATE; + } else if (folderId > 0) { + // Check if folder is under templates + NoteInfo folder = getFolderInfo(folderId); + if (folder != null && folder.parentId == Notes.ID_TEMPLATE_FOLDER) { + type = Notes.TYPE_TEMPLATE; + } + } + values.put(NoteColumns.PARENT_ID, folderId); - values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + values.put(NoteColumns.TYPE, type); values.put(NoteColumns.CREATED_DATE, currentTime); values.put(NoteColumns.MODIFIED_DATE, currentTime); values.put(NoteColumns.LOCAL_MODIFIED, 1); @@ -1482,7 +1503,7 @@ public class NotesRepository { long currentTime = System.currentTimeMillis(); values.put(NoteColumns.PARENT_ID, categoryId); - values.put(NoteColumns.TYPE, Notes.TYPE_NOTE); + values.put(NoteColumns.TYPE, Notes.TYPE_TEMPLATE); values.put(NoteColumns.CREATED_DATE, currentTime); values.put(NoteColumns.MODIFIED_DATE, currentTime); values.put(NoteColumns.LOCAL_MODIFIED, 1); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java b/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java index 63f7173..9a8976f 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java @@ -115,6 +115,7 @@ public class WorkingNote { DataColumns.DATA2, DataColumns.DATA3, DataColumns.DATA4, + DataColumns.DATA5, }; /** 数据查询投影 - 笔记元数据 */ @@ -190,7 +191,36 @@ public class WorkingNote { mMode = 0; mWidgetType = Notes.TYPE_WIDGET_INVALIDE; mLocalModified = 1; // 新建笔记需要同步 - mType = Notes.TYPE_NOTE; // 默认为普通笔记类型 + + // Determine type based on folder + if (folderId == Notes.ID_TEMPLATE_FOLDER) { + mType = Notes.TYPE_TEMPLATE; + } else if (folderId > 0) { + // Check if parent is template folder + int parentType = net.micode.notes.tool.DataUtils.getNoteTypeById(context.getContentResolver(), folderId); + if (parentType == Notes.TYPE_FOLDER) { + // We need to check the folder's parent + long parentId = 0; + android.database.Cursor c = context.getContentResolver().query( + android.content.ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, folderId), + new String[] { NoteColumns.PARENT_ID }, null, null, null); + if (c != null) { + if (c.moveToFirst()) { + parentId = c.getLong(0); + } + c.close(); + } + if (parentId == Notes.ID_TEMPLATE_FOLDER) { + mType = Notes.TYPE_TEMPLATE; + } else { + mType = Notes.TYPE_NOTE; + } + } else { + mType = Notes.TYPE_NOTE; + } + } else { + mType = Notes.TYPE_NOTE; + } } /** @@ -287,6 +317,12 @@ public class WorkingNote { mContent = cursor.getString(DATA_CONTENT_COLUMN); mMode = cursor.getInt(DATA_MODE_COLUMN); mNote.setTextDataId(cursor.getLong(DATA_ID_COLUMN)); + + // 加载壁纸路径 + int wallpaperIndex = cursor.getColumnIndex(DataColumns.DATA5); + if (wallpaperIndex != -1) { + mWallpaperPath = cursor.getString(wallpaperIndex); + } } else if (DataConstants.CALL_NOTE.equals(type)) { // 加载通话记录数据 mNote.setCallDataId(cursor.getLong(DATA_ID_COLUMN)); @@ -366,9 +402,9 @@ public class WorkingNote { public synchronized boolean saveNote() { if (isWorthSaving()) { if (!existInDatabase()) { - // 创建新笔记 - if ((mNoteId = Note.getNewNoteId(mContext, mFolderId)) == 0) { - Log.e(TAG, "Create new note fail with id:" + mNoteId); + // 创建新笔记 + if ((mNoteId = Note.getNewNoteId(mContext, mFolderId, mType)) == 0) { + Log.e(TAG, "Create new note fail with id:" + mNoteId); return false; } } @@ -501,16 +537,12 @@ public class WorkingNote { private String mWallpaperPath; public void setWallpaper(String path) { - mWallpaperPath = path; - // Ideally we should save this to DB, but for now we might use shared prefs or a separate table - // Or reuse bg_color_id with a special flag if we want to stick to existing schema strictly? - // Better: store in a new column or reuse a data column if possible. - // Given existing schema, let's use DataColumns.DATA5 if available? No DATA5. - // Let's use a SharedPreference for mapping noteId -> wallpaperPath for now to avoid schema migration complexity in this step. - // Or just use a special negative color ID range for wallpapers? - // Actually, let's use a separate storage for wallpapers map: note_id -> uri string - if (mNoteSettingStatusListener != null) { - mNoteSettingStatusListener.onBackgroundColorChanged(); // Reuse this to trigger refresh + if (!TextUtils.equals(mWallpaperPath, path)) { + mWallpaperPath = path; + mNote.setTextData(DataColumns.DATA5, mWallpaperPath); + if (mNoteSettingStatusListener != null) { + mNoteSettingStatusListener.onBackgroundColorChanged(); // Reuse this to trigger refresh + } } } diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java index d237122..46fd4c6 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/DataUtils.java @@ -182,10 +182,22 @@ public class DataUtils { * @return 如果笔记可见返回 true,否则返回 false */ public static boolean visibleInNoteDatabase(ContentResolver resolver, long noteId, int type) { + String selection; + String[] selectionArgs; + + if (type == Notes.TYPE_NOTE) { + // If checking for a regular note, also allow templates as they are essentially notes + selection = "(" + NoteColumns.TYPE + "=? OR " + NoteColumns.TYPE + "=?) AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER; + selectionArgs = new String[] {String.valueOf(Notes.TYPE_NOTE), String.valueOf(Notes.TYPE_TEMPLATE)}; + } else { + selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER; + selectionArgs = new String [] {String.valueOf(type)}; + } + Cursor cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, noteId), null, - NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>" + Notes.ID_TRASH_FOLER, - new String [] {String.valueOf(type)}, + selection, + selectionArgs, null); boolean exist = false; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ResourceParser.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ResourceParser.java index 595d7a5..3743336 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ResourceParser.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/ResourceParser.java @@ -51,6 +51,12 @@ public class ResourceParser { public static final int EYE_CARE_GREEN = 6; public static final int WARM = 7; public static final int COOL = 8; + + // Gradient Presets + public static final int SUNSET = 9; + public static final int OCEAN = 10; + public static final int FOREST = 11; + public static final int LAVENDER = 12; /** 自定义颜色按钮 ID (用于 UI 显示) */ public static final int CUSTOM_COLOR_BUTTON_ID = -100; @@ -108,7 +114,15 @@ public class ResourceParser { R.drawable.edit_blue, R.drawable.edit_white, R.drawable.edit_green, - R.drawable.edit_red + R.drawable.edit_red, + R.color.bg_midnight_black, + R.color.bg_eye_care_green, + R.color.bg_warm, + R.color.bg_cool, + R.drawable.preset_sunset, + R.drawable.preset_ocean, + R.drawable.preset_forest, + R.drawable.preset_lavender }; /** 标题栏背景资源数组 */ diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java index efdb3e1..b19c795 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -77,7 +77,10 @@ import java.util.regex.Pattern; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import net.micode.notes.databinding.DialogBackgroundSelectorBinding; +import net.micode.notes.databinding.DialogColorPickerBinding; import net.micode.notes.databinding.NoteEditBinding; import net.micode.notes.tool.RichTextHelper; @@ -162,7 +165,6 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen private UndoRedoManager mUndoRedoManager; private boolean mInUndoRedo = false; - private androidx.recyclerview.widget.RecyclerView mColorSelectorRv; private NoteColorAdapter mColorAdapter; /** @@ -331,7 +333,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen *

*/ private void initResources() { - mHeadViewPanel = binding.noteTitle; + mHeadViewPanel = binding.cvEditorSurface; mNoteHeaderHolder = new HeadViewHolder(); mNoteHeaderHolder.tvModified = binding.tvModifiedDate; mNoteHeaderHolder.ivAlertIcon = binding.ivAlertIcon; @@ -342,9 +344,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen mNoteHeaderHolder.etTitle = binding.etTitle; mNoteEditor = binding.noteEditView; - mNoteEditorPanel = binding.svNoteEdit; + mNoteEditorPanel = binding.cvEditorSurface; mNoteBgColorSelector = binding.noteBgColorSelector; - mColorSelectorRv = binding.rvBgColorSelector; mNoteEditor.addTextChangedListener(new TextWatcher() { private CharSequence mBeforeText; @@ -406,6 +407,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen ResourceParser.EYE_CARE_GREEN, ResourceParser.WARM, ResourceParser.COOL, + ResourceParser.SUNSET, + ResourceParser.OCEAN, + ResourceParser.FOREST, + ResourceParser.LAVENDER, ResourceParser.CUSTOM_COLOR_BUTTON_ID, ResourceParser.WALLPAPER_BUTTON_ID ); @@ -418,11 +423,11 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen pickWallpaper(); } else { mWorkingNote.setBgColorId(colorId); + mWorkingNote.setWallpaper(null); mNoteBgColorSelector.setVisibility(View.GONE); } } }); - mColorSelectorRv.setAdapter(mColorAdapter); mFontSizeSelector = binding.fontSizeSelector; for (int id : sFontSizeBtnsMap.keySet()) { @@ -680,8 +685,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen public void onClick(View v) { int id = v.getId(); if (id == R.id.btn_set_bg_color) { - mNoteBgColorSelector.setVisibility(View.VISIBLE); - // Note: Adapter selection is already set in onBackgroundColorChanged or init + showBackgroundSelector(); } else if (sFontSizeBtnsMap.containsKey(id)) { View fontView = getFontSelectorView(sFontSelectorSelectionMap.get(mFontSizeId)); if (fontView != null) { @@ -764,27 +768,14 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen String wallpaperPath = mWorkingNote.getWallpaperPath(); if (wallpaperPath != null) { - // Load wallpaper + binding.ivNoteWallpaper.setVisibility(View.VISIBLE); + binding.viewBgMask.setVisibility(View.VISIBLE); + android.net.Uri uri = android.net.Uri.parse(wallpaperPath); try { java.io.InputStream inputStream = getContentResolver().openInputStream(uri); android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(inputStream); - android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap); - - // Tiling mode (can be configurable later) - drawable.setTileModeXY(android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT); - - // Add Blur Effect for Android 12+ - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - mNoteEditorPanel.setBackground(drawable); - mNoteEditorPanel.setRenderEffect(android.graphics.RenderEffect.createBlurEffect( - 20f, 20f, android.graphics.Shader.TileMode.CLAMP)); - } else { - mNoteEditorPanel.setBackground(drawable); - } - - // Header always uses original wallpaper (or maybe slightly darker?) - mHeadViewPanel.setBackground(drawable.getConstantState().newDrawable()); + binding.ivNoteWallpaper.setImageBitmap(bitmap); // Dynamic Coloring with Palette androidx.palette.graphics.Palette.from(bitmap).generate(palette -> { @@ -795,12 +786,14 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen } catch (Exception e) { Log.e(TAG, "Failed to load wallpaper", e); - // Fallback to color + binding.ivNoteWallpaper.setVisibility(View.GONE); + binding.viewBgMask.setVisibility(View.GONE); applyColorBackground(colorId); } } else { + binding.ivNoteWallpaper.setVisibility(View.GONE); + binding.viewBgMask.setVisibility(View.GONE); applyColorBackground(colorId); - // Reset toolbar colors to default/theme resetToolbarColors(); } updateTextColor(colorId); @@ -809,30 +802,47 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen private void applyPaletteColors(androidx.palette.graphics.Palette palette) { int primaryColor = palette.getDominantColor(getResources().getColor(R.color.primary_color)); int onPrimaryColor = getResources().getColor(R.color.on_primary_color); + int mutedColor = palette.getMutedColor(android.graphics.Color.WHITE); // Ensure contrast for onPrimaryColor if (androidx.core.graphics.ColorUtils.calculateContrast(onPrimaryColor, primaryColor) < 3.0) { - onPrimaryColor = android.graphics.Color.WHITE; + onPrimaryColor = isColorDark(primaryColor) ? android.graphics.Color.WHITE : android.graphics.Color.BLACK; } - binding.toolbar.setBackgroundColor(primaryColor); binding.toolbar.setTitleTextColor(onPrimaryColor); if (binding.toolbar.getNavigationIcon() != null) { binding.toolbar.getNavigationIcon().setTint(onPrimaryColor); } getWindow().setStatusBarColor(primaryColor); + + // Update Card Surface - semi-transparent glass effect + int surfaceColor = androidx.core.graphics.ColorUtils.setAlphaComponent(mutedColor, 230); // 90% opacity + binding.cvEditorSurface.setCardBackgroundColor(surfaceColor); + + // Update input text color based on surface color + int textColor = isColorDark(surfaceColor) ? android.graphics.Color.WHITE : android.graphics.Color.BLACK; + mNoteEditor.setTextColor(textColor); + if (mNoteHeaderHolder != null && mNoteHeaderHolder.etTitle != null) { + mNoteHeaderHolder.etTitle.setTextColor(textColor); + mNoteHeaderHolder.etTitle.setHintTextColor(androidx.core.graphics.ColorUtils.setAlphaComponent(textColor, 128)); + } + binding.tvCharCount.setTextColor(textColor); + binding.tvModifiedDate.setTextColor(textColor); } private void resetToolbarColors() { int primaryColor = getResources().getColor(R.color.primary_color); int onPrimaryColor = getResources().getColor(R.color.on_primary_color); - binding.toolbar.setBackgroundColor(primaryColor); + binding.toolbar.setBackgroundColor(android.graphics.Color.TRANSPARENT); binding.toolbar.setTitleTextColor(onPrimaryColor); if (binding.toolbar.getNavigationIcon() != null) { binding.toolbar.getNavigationIcon().setTint(onPrimaryColor); } getWindow().setStatusBarColor(primaryColor); + + // Reset Card Surface + binding.cvEditorSurface.setCardBackgroundColor(android.graphics.Color.parseColor("#CCFFFFFF")); } private void updateTextColor(int colorId) { @@ -843,20 +853,27 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen textColor = android.graphics.Color.WHITE; } else if (colorId < 0) { // Custom color: Calculate luminance - // colorId is the ARGB value for custom colors if (isColorDark(colorId)) { textColor = android.graphics.Color.WHITE; } } - // For wallpaper, we might want to check palette, but for now default to black or keep current - // If wallpaper is set, this method is called with the underlying colorId. - // We should probably rely on the underlying color or default to white/black. - - mNoteEditor.setTextColor(textColor); - // Also update title color if needed - if (mNoteHeaderHolder != null && mNoteHeaderHolder.etTitle != null) { - mNoteHeaderHolder.etTitle.setTextColor(textColor); + // If wallpaper is set, applyPaletteColors already handled text color. + if (mWorkingNote.getWallpaperPath() == null) { + mNoteEditor.setTextColor(textColor); + if (mNoteHeaderHolder != null && mNoteHeaderHolder.etTitle != null) { + mNoteHeaderHolder.etTitle.setTextColor(textColor); + mNoteHeaderHolder.etTitle.setHintTextColor(androidx.core.graphics.ColorUtils.setAlphaComponent(textColor, 128)); + } + binding.tvCharCount.setTextColor(textColor); + binding.tvModifiedDate.setTextColor(textColor); + + // Adjust card surface opacity for pure colors + if (colorId == ResourceParser.WHITE) { + binding.cvEditorSurface.setCardBackgroundColor(android.graphics.Color.WHITE); + } else { + binding.cvEditorSurface.setCardBackgroundColor(android.graphics.Color.parseColor("#CCFFFFFF")); + } } } @@ -868,27 +885,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen } private void applyColorBackground(int colorId) { - mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId()); - mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId()); - - if (colorId >= ResourceParser.MIDNIGHT_BLACK || colorId < 0) { - int color = ResourceParser.getNoteBgColor(this, colorId); - if (mNoteEditorPanel.getBackground() != null) { - mNoteEditorPanel.getBackground().setTint(color); - mNoteEditorPanel.getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY); - } - if (mHeadViewPanel.getBackground() != null) { - mHeadViewPanel.getBackground().setTint(color); - mHeadViewPanel.getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY); - } + if (colorId < 0) { + binding.noteEditRoot.setBackgroundColor(colorId); } else { - // Clear tint for legacy resources - if (mNoteEditorPanel.getBackground() != null) { - mNoteEditorPanel.getBackground().clearColorFilter(); - } - if (mHeadViewPanel.getBackground() != null) { - mHeadViewPanel.getBackground().clearColorFilter(); - } + binding.noteEditRoot.setBackgroundResource(ResourceParser.NoteBgResources.getNoteBgResource(colorId)); } } @@ -1535,13 +1535,50 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen SHORTCUT_ICON_TITLE_MAX_LEN) : content; } - private void showColorPickerDialog() { - final View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_color_picker, null); - final View colorPreview = dialogView.findViewById(R.id.view_color_preview); - android.widget.SeekBar sbRed = dialogView.findViewById(R.id.sb_red); - android.widget.SeekBar sbGreen = dialogView.findViewById(R.id.sb_green); - android.widget.SeekBar sbBlue = dialogView.findViewById(R.id.sb_blue); + private void showBackgroundSelector() { + BottomSheetDialog dialog = new BottomSheetDialog(this); + DialogBackgroundSelectorBinding dialogBinding = DialogBackgroundSelectorBinding.inflate(getLayoutInflater()); + + java.util.List colors = java.util.Arrays.asList( + ResourceParser.YELLOW, + ResourceParser.BLUE, + ResourceParser.WHITE, + ResourceParser.GREEN, + ResourceParser.RED, + ResourceParser.MIDNIGHT_BLACK, + ResourceParser.EYE_CARE_GREEN, + ResourceParser.WARM, + ResourceParser.COOL, + ResourceParser.SUNSET, + ResourceParser.OCEAN, + ResourceParser.FOREST, + ResourceParser.LAVENDER + ); + + NoteColorAdapter adapter = new NoteColorAdapter(colors, mWorkingNote.getBgColorId(), colorId -> { + mWorkingNote.setBgColorId(colorId); + mWorkingNote.setWallpaper(null); // Clear wallpaper when color selected + dialog.dismiss(); + }); + dialogBinding.rvBackgroundOptions.setAdapter(adapter); + + dialogBinding.btnPickWallpaper.setOnClickListener(v -> { + pickWallpaper(); + dialog.dismiss(); + }); + + dialogBinding.btnCustomColor.setOnClickListener(v -> { + showColorPickerDialog(); + dialog.dismiss(); + }); + + dialog.setContentView(dialogBinding.getRoot()); + dialog.show(); + } + private void showColorPickerDialog() { + DialogColorPickerBinding dialogBinding = DialogColorPickerBinding.inflate(getLayoutInflater()); + int currentColor = android.graphics.Color.WHITE; if (mWorkingNote.getBgColorId() < 0) { currentColor = mWorkingNote.getBgColorId(); @@ -1553,10 +1590,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen android.graphics.Color.blue(currentColor) }; - colorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2])); - sbRed.setProgress(rgb[0]); - sbGreen.setProgress(rgb[1]); - sbBlue.setProgress(rgb[2]); + dialogBinding.viewColorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2])); + dialogBinding.sbRed.setProgress(rgb[0]); + dialogBinding.sbGreen.setProgress(rgb[1]); + dialogBinding.sbBlue.setProgress(rgb[2]); android.widget.SeekBar.OnSeekBarChangeListener listener = new android.widget.SeekBar.OnSeekBarChangeListener() { @Override @@ -1564,7 +1601,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen if (seekBar.getId() == R.id.sb_red) rgb[0] = progress; else if (seekBar.getId() == R.id.sb_green) rgb[1] = progress; else if (seekBar.getId() == R.id.sb_blue) rgb[2] = progress; - colorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2])); + dialogBinding.viewColorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2])); } @Override @@ -1574,21 +1611,18 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen public void onStopTrackingTouch(android.widget.SeekBar seekBar) {} }; - sbRed.setOnSeekBarChangeListener(listener); - sbGreen.setOnSeekBarChangeListener(listener); - sbBlue.setOnSeekBarChangeListener(listener); + dialogBinding.sbRed.setOnSeekBarChangeListener(listener); + dialogBinding.sbGreen.setOnSeekBarChangeListener(listener); + dialogBinding.sbBlue.setOnSeekBarChangeListener(listener); new AlertDialog.Builder(this) .setTitle("Custom Color") - .setView(dialogView) + .setView(dialogBinding.getRoot()) .setPositiveButton(android.R.string.ok, (dialog, which) -> { int newColor = android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]); - // Use negative integer for custom color. Ensure it's negative. - // ARGB color with alpha 255 is negative in Java int. - // If alpha is 0, it might be positive. We assume full opacity. newColor |= 0xFF000000; mWorkingNote.setBgColorId(newColor); - mNoteBgColorSelector.setVisibility(View.GONE); + mWorkingNote.setWallpaper(null); }) .setNegativeButton(android.R.string.cancel, null) .show(); @@ -1695,27 +1729,26 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen } private void initRichTextToolbar() { - mRichTextSelector = findViewById(R.id.rich_text_selector); - findViewById(R.id.btn_bold).setOnClickListener(new OnClickListener() { + mRichTextSelector = binding.richTextSelector; + binding.btnBold.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyBold(mNoteEditor); } }); - findViewById(R.id.btn_italic).setOnClickListener(new OnClickListener() { + binding.btnItalic.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyItalic(mNoteEditor); } }); - findViewById(R.id.btn_underline).setOnClickListener(new OnClickListener() { + binding.btnUnderline.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyUnderline(mNoteEditor); } }); - findViewById(R.id.btn_strikethrough).setOnClickListener(new OnClickListener() { + binding.btnStrikethrough.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyStrikethrough(mNoteEditor); } }); - findViewById(R.id.btn_header).setOnClickListener(new OnClickListener() { + binding.btnHeader.setOnClickListener(new OnClickListener() { public void onClick(View v) { final CharSequence[] items = {"H1 (Largest)", "H2", "H3", "H4", "H5", "H6 (Smallest)", "Normal"}; AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this); builder.setTitle("Header Level"); builder.setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int item) { - // item index maps to level: 0->1, 1->2, ..., 5->6, 6->0 (Normal) int level = (item == 6) ? 0 : (item + 1); RichTextHelper.applyHeading(mNoteEditor, level); } @@ -1723,22 +1756,22 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen builder.show(); } }); - findViewById(R.id.btn_list).setOnClickListener(new OnClickListener() { + binding.btnList.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyBullet(mNoteEditor); } }); - findViewById(R.id.btn_quote).setOnClickListener(new OnClickListener() { + binding.btnQuote.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyQuote(mNoteEditor); } }); - findViewById(R.id.btn_code).setOnClickListener(new OnClickListener() { + binding.btnCode.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.applyCode(mNoteEditor); } }); - findViewById(R.id.btn_link).setOnClickListener(new OnClickListener() { + binding.btnLink.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.insertLink(NoteEditActivity.this, mNoteEditor); } }); - findViewById(R.id.btn_divider).setOnClickListener(new OnClickListener() { + binding.btnDivider.setOnClickListener(new OnClickListener() { public void onClick(View v) { RichTextHelper.insertDivider(mNoteEditor); } }); - findViewById(R.id.btn_color_text).setOnClickListener(new OnClickListener() { + binding.btnColorText.setOnClickListener(new OnClickListener() { public void onClick(View v) { final CharSequence[] items = {"Black", "Red", "Blue"}; final int[] colors = {android.graphics.Color.BLACK, android.graphics.Color.RED, android.graphics.Color.BLUE}; @@ -1752,7 +1785,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen builder.show(); } }); - findViewById(R.id.btn_color_fill).setOnClickListener(new OnClickListener() { + binding.btnColorFill.setOnClickListener(new OnClickListener() { public void onClick(View v) { final CharSequence[] items = {"None", "Yellow", "Green", "Cyan"}; final int[] colors = {android.graphics.Color.TRANSPARENT, android.graphics.Color.YELLOW, android.graphics.Color.GREEN, android.graphics.Color.CYAN}; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemData.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemData.java index 5e60636..5ca30f1 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemData.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteItemData.java @@ -196,8 +196,8 @@ public class NoteItemData { mIsMultiNotesFollowingFolder = false; mIsOneNoteFollowingFolder = false; - // 如果是普通笔记且不是第一项,检查前一项是否为文件夹 - if (mType == Notes.TYPE_NOTE && !mIsFirstItem) { + // 如果是普通笔记或模板且不是第一项,检查前一项是否为文件夹 + if ((mType == Notes.TYPE_NOTE || mType == Notes.TYPE_TEMPLATE) && !mIsFirstItem) { int position = cursor.getPosition(); if (cursor.moveToPrevious()) { // 前一项是文件夹或系统文件夹 diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java index 523120b..9fc2d88 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteSearchActivity.java @@ -154,14 +154,36 @@ public class NoteSearchActivity extends AppCompatActivity implements SearchView. mHistoryManager.addHistory(query); } - Intent intent = new Intent(this, NoteEditActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(Intent.EXTRA_UID, note.getId()); - // Pass search keyword for highlighting in editor - // NoteEditActivity uses SearchManager.EXTRA_DATA_KEY for ID and USER_QUERY for keyword - intent.putExtra(android.app.SearchManager.EXTRA_DATA_KEY, String.valueOf(note.getId())); - intent.putExtra(android.app.SearchManager.USER_QUERY, mSearchView.getQuery().toString()); - startActivity(intent); + if (note.type == Notes.TYPE_TEMPLATE) { + // Apply template: create a new note based on this template + mRepository.applyTemplate(note.getId(), Notes.ID_ROOT_FOLDER, new NotesRepository.Callback() { + @Override + public void onSuccess(Long newNoteId) { + runOnUiThread(() -> { + Intent intent = new Intent(NoteSearchActivity.this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, newNoteId); + startActivity(intent); + Toast.makeText(NoteSearchActivity.this, "已根据模板创建新笔记", Toast.LENGTH_SHORT).show(); + }); + } + + @Override + public void onError(Exception e) { + runOnUiThread(() -> { + Toast.makeText(NoteSearchActivity.this, "应用模板失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + }); + } + }); + } else { + Intent intent = new Intent(this, NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Intent.EXTRA_UID, note.getId()); + // Pass search keyword for highlighting in editor + intent.putExtra(android.app.SearchManager.EXTRA_DATA_KEY, String.valueOf(note.getId())); + intent.putExtra(android.app.SearchManager.USER_QUERY, query); + startActivity(intent); + } } @Override diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java index 260f015..2fca2c1 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java @@ -148,6 +148,30 @@ public class NotesListFragment extends Fragment implements NotesRepository.NoteInfo note = viewModel.getNotesLiveData().getValue().get(position); if (note.type == Notes.TYPE_FOLDER) { viewModel.enterFolder(note.getId()); + } else if (note.type == Notes.TYPE_TEMPLATE) { + // Apply template: create a new note based on this template + viewModel.applyTemplate(note.getId(), new net.micode.notes.data.NotesRepository.Callback() { + @Override + public void onSuccess(Long newNoteId) { + // Create a temporary NoteInfo to open the editor + net.micode.notes.data.NotesRepository.NoteInfo newNote = new net.micode.notes.data.NotesRepository.NoteInfo(); + newNote.setId(newNoteId); + newNote.setParentId(Notes.ID_ROOT_FOLDER); + newNote.type = Notes.TYPE_NOTE; + + requireActivity().runOnUiThread(() -> { + openNoteEditor(newNote); + Toast.makeText(requireContext(), "已根据模板创建新笔记", Toast.LENGTH_SHORT).show(); + }); + } + + @Override + public void onError(Exception e) { + requireActivity().runOnUiThread(() -> { + Toast.makeText(requireContext(), "应用模板失败: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + }); + } + }); } else { if (note.isLocked) { pendingNote = note; diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListItem.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListItem.java index 9a6d238..38ee421 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListItem.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListItem.java @@ -69,7 +69,7 @@ public class NotesListItem extends LinearLayout { * @param checked 该项是否被选中(仅在多选模式下有意义) */ public void bind(Context context, NoteItemData data, boolean choiceMode, boolean checked) { - if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + if (choiceMode && (data.getType() == Notes.TYPE_NOTE || data.getType() == Notes.TYPE_TEMPLATE)) { mCheckBox.setVisibility(View.VISIBLE); mCheckBox.setChecked(checked); } else { @@ -136,7 +136,7 @@ public class NotesListItem extends LinearLayout { private void setBackground(NoteItemData data) { int id = data.getBgColorId(); int resId; - if (data.getType() == Notes.TYPE_NOTE) { + if (data.getType() == Notes.TYPE_NOTE || data.getType() == Notes.TYPE_TEMPLATE) { if (data.isSingle() || data.isOneFollowingFolder()) { resId = NoteItemBgResources.getNoteBgSingleRes(id); } else if (data.isLast()) { @@ -153,7 +153,7 @@ public class NotesListItem extends LinearLayout { setBackgroundResource(resId); // Apply tint for new colors - if (data.getType() == Notes.TYPE_NOTE && (id >= net.micode.notes.tool.ResourceParser.MIDNIGHT_BLACK || id < 0)) { + if ((data.getType() == Notes.TYPE_NOTE || data.getType() == Notes.TYPE_TEMPLATE) && (id >= net.micode.notes.tool.ResourceParser.MIDNIGHT_BLACK || id < 0)) { int color = net.micode.notes.tool.ResourceParser.getNoteBgColor(getContext(), id); if (getBackground() != null) { getBackground().setTint(color); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java index 5dadec7..3a5c484 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java @@ -105,7 +105,7 @@ public class NotesRecyclerAdapter extends RecyclerView.Adapter path) { folderPathLiveData.postValue(path); - } - - @Override - public void onError(Exception error) { - 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); + // Determine if we are in template mode + boolean isTemplate = (folderId == Notes.ID_TEMPLATE_FOLDER); + if (!isTemplate && path != null) { + for (NotesRepository.NoteInfo info : path) { + if (info.getId() == Notes.ID_TEMPLATE_FOLDER) { + isTemplate = true; + break; + } + } } - // 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); + final boolean templateMode = isTemplate; + long tabParentId = templateMode ? Notes.ID_TEMPLATE_FOLDER : Notes.ID_ROOT_FOLDER; + + // 加载子文件夹 (Category Tabs) + repository.getSubFolders(tabParentId, new NotesRepository.Callback>() { + @Override + public void onSuccess(List folders) { + // Construct the display list with "All" and "Uncategorized" + List displayFolders = new ArrayList<>(); + + // 1. "All" / "All Templates" Folder (Virtual) + NotesRepository.NoteInfo allFolder = new NotesRepository.NoteInfo(); + allFolder.setId(templateMode ? Notes.ID_TEMPLATE_FOLDER : Notes.ID_ALL_NOTES_FOLDER); + allFolder.snippet = templateMode ? "所有模板" : "所有"; // Name + displayFolders.add(allFolder); + + // 2. Real Folders (from DB) + if (folders != null) { + displayFolders.addAll(folders); + } + + // 3. "Uncategorized" Folder (only for normal notes) + if (!templateMode) { + 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); + } + }); } - + @Override public void onError(Exception error) { - Log.e(TAG, "Failed to load sub-folders", error); + Log.e(TAG, "Failed to load folder path", error); } }); diff --git a/src/Notesmaster/app/src/main/res/drawable/bg_bottom_sheet.xml b/src/Notesmaster/app/src/main/res/drawable/bg_bottom_sheet.xml new file mode 100644 index 0000000..a91aca0 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/bg_bottom_sheet.xml @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/preset_forest.xml b/src/Notesmaster/app/src/main/res/drawable/preset_forest.xml new file mode 100644 index 0000000..aced12d --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/preset_forest.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/preset_lavender.xml b/src/Notesmaster/app/src/main/res/drawable/preset_lavender.xml new file mode 100644 index 0000000..b9803bb --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/preset_lavender.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/preset_ocean.xml b/src/Notesmaster/app/src/main/res/drawable/preset_ocean.xml new file mode 100644 index 0000000..dd2e1ee --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/preset_ocean.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/preset_sunset.xml b/src/Notesmaster/app/src/main/res/drawable/preset_sunset.xml new file mode 100644 index 0000000..802f1ca --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/preset_sunset.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/dialog_background_selector.xml b/src/Notesmaster/app/src/main/res/layout/dialog_background_selector.xml new file mode 100644 index 0000000..3f50cf6 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/dialog_background_selector.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/note_edit.xml b/src/Notesmaster/app/src/main/res/layout/note_edit.xml index aba7781..5bacd39 100644 --- a/src/Notesmaster/app/src/main/res/layout/note_edit.xml +++ b/src/Notesmaster/app/src/main/res/layout/note_edit.xml @@ -15,482 +15,258 @@ limitations under the License. --> - - + android:background="@color/background_color"> - - + + android:layout_height="match_parent" + android:scaleType="centerCrop" + android:visibility="gone" /> - - - - + android:layout_height="match_parent" + android:background="#1A000000" + android:visibility="gone" /> - - - - - - - - - - + + - - + android:layout_height="?attr/actionBarSize" + android:background="@android:color/transparent" + app:navigationIcon="@android:drawable/ic_menu_close_clear_cancel" + app:title="@string/menu_edit_note" + app:titleTextAppearance="@style/TextAppearance.Material3.TitleMedium" /> + + + + - - - - + + + + android:layout_height="wrap_content" + app:cardCornerRadius="24dp" + app:cardElevation="0dp" + app:cardBackgroundColor="#CCFFFFFF" + app:strokeWidth="0dp"> + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="24dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + android:textSize="18sp" + android:textColor="@color/text_color_primary" + android:lineSpacingMultiplier="1.5" + android:fontFamily="sans-serif" /> - - - + + - - + + + + + + android:layout_height="56dp" + android:scrollbars="none"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:gravity="center_vertical" + android:paddingHorizontal="12dp"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Notesmaster/app/src/main/res/values/colors.xml b/src/Notesmaster/app/src/main/res/values/colors.xml index 8c4a5a3..6405010 100644 --- a/src/Notesmaster/app/src/main/res/values/colors.xml +++ b/src/Notesmaster/app/src/main/res/values/colors.xml @@ -43,6 +43,16 @@ #FFE0B2 #E1BEE7 + + #FF512F + #DD2476 + #2193B0 + #6DD5ED + #11998E + #38EF7D + #834D9B + #D04ED6 + #1976D2 #2196F3 -- 2.34.1 From f8c785f3485cf8a67f11be2a24644d46236aaaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=B0=94=E4=BF=8A?= Date: Sun, 1 Feb 2026 16:30:36 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BE=BF=E7=AD=BE=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2UI=E4=BC=98=E5=8C=96=E5=92=8C=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E3=80=81=E5=9C=B0=E7=82=B9=E6=99=BA=E8=83=BD=E8=AF=86?= =?UTF-8?q?=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../.idea/deploymentTargetSelector.xml | 15 -- .../app/src/main/AndroidManifest.xml | 18 ++ .../net/micode/notes/model/WorkingNote.java | 9 +- .../net/micode/notes/tool/SmartParser.java | 167 ++++++++++++++++++ .../net/micode/notes/tool/SmartURLSpan.java | 122 +++++++++++++ .../net/micode/notes/ui/NoteEditActivity.java | 55 +++++- .../net/micode/notes/ui/NoteEditText.java | 32 ++++ .../app/src/main/res/values/strings.xml | 2 + 8 files changed, 394 insertions(+), 26 deletions(-) create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartParser.java create mode 100644 src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartURLSpan.java diff --git a/src/Notesmaster/.idea/deploymentTargetSelector.xml b/src/Notesmaster/.idea/deploymentTargetSelector.xml index f802cd9..b268ef3 100644 --- a/src/Notesmaster/.idea/deploymentTargetSelector.xml +++ b/src/Notesmaster/.idea/deploymentTargetSelector.xml @@ -4,21 +4,6 @@ diff --git a/src/Notesmaster/app/src/main/AndroidManifest.xml b/src/Notesmaster/app/src/main/AndroidManifest.xml index ff73964..96ce6f4 100644 --- a/src/Notesmaster/app/src/main/AndroidManifest.xml +++ b/src/Notesmaster/app/src/main/AndroidManifest.xml @@ -22,6 +22,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java b/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java index 9a8976f..a8e50c8 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/model/WorkingNote.java @@ -321,7 +321,12 @@ public class WorkingNote { // 加载壁纸路径 int wallpaperIndex = cursor.getColumnIndex(DataColumns.DATA5); if (wallpaperIndex != -1) { - mWallpaperPath = cursor.getString(wallpaperIndex); + String path = cursor.getString(wallpaperIndex); + if (!TextUtils.isEmpty(path)) { + mWallpaperPath = path; + } else { + mWallpaperPath = null; + } } } else if (DataConstants.CALL_NOTE.equals(type)) { // 加载通话记录数据 @@ -449,7 +454,7 @@ public class WorkingNote { * @return 如果值得保存返回 true,否则返回 false */ private boolean isWorthSaving() { - if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent)) + if (mIsDeleted || (!existInDatabase() && TextUtils.isEmpty(mContent) && TextUtils.isEmpty(mWallpaperPath)) || (existInDatabase() && !mNote.isLocalModified())) { return false; } else { diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartParser.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartParser.java new file mode 100644 index 0000000..0576ed1 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartParser.java @@ -0,0 +1,167 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.os.Build; +import android.text.Spannable; +import android.text.style.URLSpan; +import android.view.textclassifier.TextClassificationManager; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextLinks; + +import java.util.Collection; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 智能解析工具类 + *

+ * 整合了 Android 系统级 AI (TextClassifier) 和自定义正则表达式。 + * 能够识别文本中的时间、地点、电话、URL 等信息。 + *

+ */ +public class SmartParser { + // 时间 Scheme 前缀 + public static final String SCHEME_TIME = "smarttime:"; + // 地点 Scheme 前缀 + public static final String SCHEME_GEO = "smartgeo:"; + + // 优化的时间正则表达式(作为补充) + private static final String TIME_REGEX = + "((今天|明天|后天|下周[一二三四五六日])?\\s*([上下]午)?\\s*(\\d{1,2})[:点](\\d{0,2})分?)" + + "|(\\b\\d{1,2}:\\d{2}\\b)"; + + // 优化的地点正则表达式:匹配 1-10 个中文字符/数字 + 常见的地点后缀 + private static final String GEO_REGEX = + "([一-龥0-9]{1,10}(?:省|市|区|县|街道|路|弄|巷|楼|院|场|店|里|广场|大厦|中心|医院|学校|大学|公园|车站|机场|酒店|宾馆|超市|商场))"; + + // 扩展噪音词列表:包含动词、代词、时间单位和方位词 + private static final String NOISE_PREFIXES = "我在去到从的地了你他们这那点分时上下午"; + + /** + * 解析文本并应用智能链接 + * + * @param context 上下文,用于获取系统服务 + * @param text 要解析的文本内容 + */ + public static void parse(Context context, Spannable text) { + if (text == null || text.length() == 0) return; + + // 1. 清除旧的智能链接 + URLSpan[] allSpans = text.getSpans(0, text.length(), URLSpan.class); + for (URLSpan span : allSpans) { + String url = span.getURL(); + if (url != null && (url.startsWith(SCHEME_TIME) || url.startsWith(SCHEME_GEO))) { + text.removeSpan(span); + } + } + + // 2. 识别逻辑 + // 步骤 A: 优先识别时间(因为时间格式相对固定,误报率低) + applyRegexLinks(text, Pattern.compile(TIME_REGEX, Pattern.CASE_INSENSITIVE), SCHEME_TIME); + + // 步骤 B: 识别地点(地点正则较宽松,需要避开已识别的时间) + applyRegexLinks(text, Pattern.compile(GEO_REGEX), SCHEME_GEO); + + // 3. 使用系统级 AI (TextClassifier) 作为增强 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + try { + TextClassificationManager tcm = context.getSystemService(TextClassificationManager.class); + if (tcm != null) { + TextClassifier classifier = tcm.getTextClassifier(); + TextLinks.Request request = new TextLinks.Request.Builder(text).build(); + TextLinks links = classifier.generateLinks(request); + + for (TextLinks.TextLink link : links.getLinks()) { + String entityType = getTopEntity(link); + if ("address".equals(entityType) || "location".equals(entityType) || "place".equals(entityType)) { + applySmartSpan(text, link.getStart(), link.getEnd(), SCHEME_GEO); + } else if ("date".equals(entityType) || "datetime".equals(entityType)) { + applySmartSpan(text, link.getStart(), link.getEnd(), SCHEME_TIME); + } + } + } + } catch (Exception e) { + // 静默回退 + } + } + } + + /** + * 获取置信度最高的实体类型 + */ + private static String getTopEntity(TextLinks.TextLink link) { + float maxConfidence = -1; + String topType = null; + for (int i = 0; i < link.getEntityCount(); i++) { + String type = link.getEntity(i); + float confidence = link.getConfidenceScore(type); + if (confidence > maxConfidence) { + maxConfidence = confidence; + topType = type; + } + } + return topType; + } + + /** + * 应用智能 Span 并处理重叠冲突 + */ + private static void applySmartSpan(Spannable text, int start, int end, String scheme) { + // 1. 噪音修剪(特别是地点识别) + if (SCHEME_GEO.equals(scheme)) { + while (start < end && NOISE_PREFIXES.indexOf(text.charAt(start)) != -1) { + start++; + } + } + + if (start >= end) return; + + // 2. 处理重叠冲突 + URLSpan[] existing = text.getSpans(start, end, URLSpan.class); + if (existing.length > 0) { + for (URLSpan span : existing) { + int spanStart = text.getSpanStart(span); + int spanEnd = text.getSpanEnd(span); + + // 如果当前识别结果完全落在已有 span 内部,则跳过 + if (start >= spanStart && end <= spanEnd) { + return; + } + + // 如果当前识别结果包含了已有 span,尝试修剪当前结果的起始位置 + if (start < spanEnd && end > spanStart) { + // 如果重叠发生在开头,将起始位置移动到已有 span 之后 + if (start < spanEnd) { + start = spanEnd; + } + } + } + } + + // 再次检查修剪后的合法性 + if (start >= end) return; + + // 针对地点识别,修剪后可能剩下的是噪音或过短 + if (SCHEME_GEO.equals(scheme)) { + // 再次修剪新起点处的噪音 + while (start < end && NOISE_PREFIXES.indexOf(text.charAt(start)) != -1) { + start++; + } + // 如果剩下的文本太短(如只有 1 个字且不是后缀),则放弃 + if (end - start < 2) return; + } + + text.setSpan(new SmartURLSpan(scheme + text.subSequence(start, end)), + start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + /** + * 正则应用链接 + */ + private static void applyRegexLinks(Spannable text, Pattern pattern, String scheme) { + Matcher m = pattern.matcher(text); + while (m.find()) { + applySmartSpan(text, m.start(), m.end(), scheme); + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartURLSpan.java b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartURLSpan.java new file mode 100644 index 0000000..ed08191 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/tool/SmartURLSpan.java @@ -0,0 +1,122 @@ +package net.micode.notes.tool; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.provider.AlarmClock; +import android.text.style.URLSpan; +import android.util.Log; +import android.view.View; +import android.widget.Toast; + +import net.micode.notes.R; + +import java.util.Calendar; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 自定义的 URLSpan,用于处理智能识别出的时间、地点点击事件。 + */ +public class SmartURLSpan extends URLSpan { + private static final String TAG = "SmartURLSpan"; + + public SmartURLSpan(String url) { + super(url); + } + + @Override + public void onClick(View widget) { + String url = getURL(); + Context context = widget.getContext(); + + if (url.startsWith(SmartParser.SCHEME_TIME)) { + handleTimeClick(context, url.substring(SmartParser.SCHEME_TIME.length())); + } else if (url.startsWith(SmartParser.SCHEME_GEO)) { + handleGeoClick(context, url.substring(SmartParser.SCHEME_GEO.length())); + } else { + super.onClick(widget); + } + } + + /** + * 处理时间点击:跳转到系统闹钟设置页面 + */ + private void handleTimeClick(Context context, String timeStr) { + try { + int hour = -1; + int minute = 0; + + // 尝试解析小时和分钟 + // 支持格式如:14:30, 10点30分, 9点 + Pattern p = Pattern.compile("(\\d{1,2})[:点](\\d{0,2})"); + Matcher m = p.matcher(timeStr); + if (m.find()) { + hour = Integer.parseInt(m.group(1)); + String minStr = m.group(2); + if (minStr != null && !minStr.isEmpty()) { + minute = Integer.parseInt(minStr); + } + } + + // 处理上下午 + if (timeStr.contains("下午") && hour < 12) { + hour += 12; + } else if (timeStr.contains("上午") && hour == 12) { + hour = 0; + } + + if (hour == -1) { + // 如果没解析出来,默认打开闹钟主界面 + Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM); + context.startActivity(intent); + return; + } + + // 设置闹钟意图 + Intent intent = new Intent(AlarmClock.ACTION_SET_ALARM) + .putExtra(AlarmClock.EXTRA_HOUR, hour) + .putExtra(AlarmClock.EXTRA_MINUTES, minute) + .putExtra(AlarmClock.EXTRA_SKIP_UI, false); + + if (intent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(intent); + } else { + Log.w(TAG, "No activity found to handle set alarm, trying without resolveActivity"); + try { + context.startActivity(intent); + } catch (Exception e2) { + Toast.makeText(context, "无法打开闹钟应用", Toast.LENGTH_SHORT).show(); + } + } + + } catch (Exception e) { + Log.e(TAG, "Failed to set alarm", e); + Toast.makeText(context, "解析时间失败", Toast.LENGTH_SHORT).show(); + } + } + + /** + * 处理地点点击:跳转到地图应用 + */ + private void handleGeoClick(Context context, String location) { + try { + // 使用 geo:0,0?q=location 格式打开地图 + Uri gmmIntentUri = Uri.parse("geo:0,0?q=" + Uri.encode(location)); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + + if (mapIntent.resolveActivity(context.getPackageManager()) != null) { + context.startActivity(mapIntent); + } else { + Log.w(TAG, "No activity found to handle geo intent, trying web fallback"); + // 如果没有地图应用支持 geo 协议,尝试搜索 + Uri webUri = Uri.parse("https://www.google.com/maps/search/" + Uri.encode(location)); + Intent webIntent = new Intent(Intent.ACTION_VIEW, webUri); + context.startActivity(webIntent); + } + } catch (Exception e) { + Log.e(TAG, "Failed to open map", e); + Toast.makeText(context, "无法打开地图应用", Toast.LENGTH_SHORT).show(); + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java index b19c795..5a3b1c0 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditActivity.java @@ -83,6 +83,7 @@ import net.micode.notes.databinding.DialogBackgroundSelectorBinding; import net.micode.notes.databinding.DialogColorPickerBinding; import net.micode.notes.databinding.NoteEditBinding; import net.micode.notes.tool.RichTextHelper; +import net.micode.notes.tool.SmartParser; import net.micode.notes.data.FontManager; @@ -1285,6 +1286,10 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen */ private Spannable getHighlightQueryResult(String fullText, String userQuery) { SpannableString spannable = new SpannableString(fullText == null ? "" : fullText); + + // 应用智能解析(时间、地点识别) + SmartParser.parse(this, spannable); + if (!TextUtils.isEmpty(userQuery)) { mPattern = Pattern.compile(userQuery); Matcher m = mPattern.matcher(fullText); @@ -1681,21 +1686,53 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen }).start(); } + private void saveWallpaperToPrivateStorage(android.net.Uri uri) { + new Thread(() -> { + try { + java.io.InputStream is = getContentResolver().openInputStream(uri); + if (is == null) return; + + // Create wallpapers directory if not exists + java.io.File wallpapersDir = new java.io.File(getFilesDir(), "wallpapers"); + if (!wallpapersDir.exists()) { + wallpapersDir.mkdirs(); + } + + // Create a unique file name + String fileName = "wp_" + System.currentTimeMillis() + ".jpg"; + java.io.File destFile = new java.io.File(wallpapersDir, 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(() -> { + mWorkingNote.setWallpaper(filePath); + mNoteBgColorSelector.setVisibility(View.GONE); + }); + + } catch (Exception e) { + Log.e(TAG, "Failed to copy wallpaper", e); + runOnUiThread(() -> { + showToast(R.string.failed_sdcard_export); + }); + } + }).start(); + } + @Override 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(); if (uri != null) { - // Take persistent permissions - try { - getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } catch (SecurityException e) { - Log.e(TAG, "Failed to take persistable uri permission", e); - } - - mWorkingNote.setWallpaper(uri.toString()); - mNoteBgColorSelector.setVisibility(View.GONE); + saveWallpaperToPrivateStorage(uri); } } else if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK && data != null) { android.net.Uri uri = data.getData(); diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditText.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditText.java index e8efb9e..789b515 100644 --- a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditText.java +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NoteEditText.java @@ -59,6 +59,12 @@ import android.view.ScaleGestureDetector; import android.view.GestureDetector; import android.text.style.ImageSpan; import net.micode.notes.tool.RichTextHelper; +import net.micode.notes.tool.SmartParser; +import net.micode.notes.tool.SmartURLSpan; +import android.text.TextWatcher; +import android.text.Editable; +import android.text.Spannable; +import android.text.style.ClickableSpan; import android.app.AlertDialog; import android.widget.SeekBar; import android.widget.TextView; @@ -93,6 +99,8 @@ public class NoteEditText extends EditText implements ScaleGestureDetector.OnSca sSchemaActionResMap.put(SCHEME_TEL, R.string.note_link_tel); sSchemaActionResMap.put(SCHEME_HTTP, R.string.note_link_web); sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email); + sSchemaActionResMap.put(SmartParser.SCHEME_TIME, R.string.note_link_time); + sSchemaActionResMap.put(SmartParser.SCHEME_GEO, R.string.note_link_geo); } @Override @@ -213,6 +221,20 @@ public class NoteEditText extends EditText implements ScaleGestureDetector.OnSca private void init(Context context) { mScaleDetector = new ScaleGestureDetector(context, this); + setLinkTextColor(getResources().getColor(R.color.primary_color)); // 设置链接颜色 + addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + // 触发智能解析 + SmartParser.parse(getContext(), s); + } + }); mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDoubleTap(MotionEvent e) { @@ -396,6 +418,16 @@ public class NoteEditText extends EditText implements ScaleGestureDetector.OnSca int off = layout.getOffsetForHorizontal(line, x); // 设置文本选择光标位置 Selection.setSelection(getText(), off); + + // 检查是否有 ClickableSpan(如智能链接) + if (getText() instanceof Spannable) { + Spannable spannable = (Spannable) getText(); + ClickableSpan[] links = spannable.getSpans(off, off, ClickableSpan.class); + if (links.length != 0) { + links[0].onClick(this); + return true; + } + } break; } diff --git a/src/Notesmaster/app/src/main/res/values/strings.xml b/src/Notesmaster/app/src/main/res/values/strings.xml index 2c51ccd..80148b2 100644 --- a/src/Notesmaster/app/src/main/res/values/strings.xml +++ b/src/Notesmaster/app/src/main/res/values/strings.xml @@ -35,6 +35,8 @@ Send email Browse web Open map + 创建闹钟 + 查看地图 /MIUI/notes/ notes_%s.txt -- 2.34.1