修改了富文本功能

pull/22/head
AetherPendragon 2 months ago
parent 1ccf31c177
commit 9fcad72595

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M2,20h20v4H2V20zM5.49,17h2.42l1.27,-3.58h5.65L16.09,17h2.42L13.25,3h-2.5L5.49,17zM9.91,11.39l2.03,-5.79h0.12l2.03,5.79H9.91z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M15.6,10.79c0.97,-0.67 1.65,-1.77 1.65,-2.79 0,-2.26 -1.75,-4 -4,-4L7,4v14h7.04c2.09,0 3.71,-1.7 3.71,-3.79 0,-1.52 -0.86,-2.82 -2.15,-3.42zM10,6.5h3c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5h-3v-3zM13.5,15.5L10,15.5v-3h3.5c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M10,4v3h2.21l-3.42,8H6v3h8v-3h-2.21l3.42,-8H18V4z"/>
</vector>

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M12.5,8c-2.65,0 -5.05,0.99 -6.9,2.6L2,7v9h9l-3.62,-3.62c1.39,-1.16 3.16,-1.88 5.12,-1.88 3.54,0 6.55,2.31 7.6,5.5l2.37,-0.78C21.08,11.03 17.15,8 12.5,8z"/>
</vector>

@ -67,11 +67,21 @@
android:layout_marginRight="8dip" />
<ImageButton
android:id="@+id/btn_set_bg_color_header"
android:id="@+id/btn_set_bg_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/bg_btn_set_color" />
<!-- Insert image button OMO -->
<ImageButton
android:id="@+id/btn_insert_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@android:drawable/ic_menu_gallery"
android:background="@drawable/bg_btn_set_color"
android:layout_marginRight="8dip" />
</LinearLayout>
<LinearLayout
@ -85,40 +95,6 @@
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
<!-- 富文本工具栏 -->
<HorizontalScrollView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="#F5F5F5"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="4dp">
<ImageButton
android:id="@+id/btn_bold"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_edit"
android:contentDescription="@string/formatting_bold" />
<ImageButton
android:id="@+id/btn_italic"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_edit"
android:contentDescription="@string/formatting_italic" />
<ImageButton
android:id="@+id/btn_underline"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?android:attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_edit"
android:contentDescription="@string/formatting_underline" />
</LinearLayout>
</HorizontalScrollView>
<ScrollView
android:layout_width="fill_parent"
@ -133,17 +109,17 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<net.micode.notes.ui.NoteEditText
<jp.wasabeef.richeditor.RichEditor
android:id="@+id/note_edit_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:autoLink="all"
android:background="@null"
android:gravity="left|top"
android:lineSpacingMultiplier="1.2"
android:background="@null"
android:autoLink="all"
android:linksClickable="false"
android:minLines="12"
android:textAppearance="@style/TextAppearancePrimaryItem" />
android:textAppearance="@style/TextAppearancePrimaryItem"
android:lineSpacingMultiplier="1.2" />
<LinearLayout
android:id="@+id/note_edit_list"
@ -159,6 +135,67 @@
android:layout_width="fill_parent"
android:layout_height="7dip"
android:background="@drawable/bg_color_btn_mask" />
<!-- 横向滚动的富文本工具栏 -->
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="#f5f5f5"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingHorizontal="8dp">
<!-- 撤销按钮 -->
<Button
android:id="@+id/action_undo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:background="@drawable/action_undo"
android:backgroundTint="@null"
android:focusable="false"
android:clickable="true"/>
<!-- 加粗按钮 -->
<Button
android:id="@+id/action_bold"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:background="@drawable/action_bold"
android:backgroundTint="@null"
android:focusable="false"
android:clickable="true"/>
<!-- 斜体按钮 -->
<Button
android:id="@+id/action_italic"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="4dp"
android:background="@drawable/action_italic"
android:backgroundTint="@null"
android:focusable="false"
android:clickable="true"/>
<!-- 背景色按钮 -->
<Button
android:id="@+id/action_bg_color"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginStart="4dp"
android:background="@drawable/action_bg_color"
android:backgroundTint="@null"
android:focusable="false"
android:clickable="true"/>
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
</LinearLayout>

@ -84,8 +84,6 @@
<string name="error_note_not_exist">要查看的便签不存在</string>
<string name="error_note_empty_for_clock">不能为空便签设置闹钟提醒</string>
<string name="error_note_empty_for_send_to_desktop">不能将空便签发送到桌面</string>
<string name="error_formatting_not_supported_in_list_mode">清单模式下不支持格式化</string>
<string name="error_please_select_text">请先选择文本</string>
<string name="success_sdcard_export">导出成功</string>
<string name="failed_sdcard_export">导出失败</string>
<string name="format_exported_file_location">已将文本文件(%1$s)输出至SD卡(%2$s)目录</string>
@ -139,14 +137,6 @@
<string name="password_set_dialog_message">请输入密码</string>
<string name="password_error">密码错误,请重试</string>
<string name="password_empty">密码不能为空</string>
<!-- Rich text formatting -->
<string name="formatting_bold">加粗</string>
<string name="formatting_italic">斜体</string>
<string name="formatting_underline">下划线</string>
<string name="formatting_bullets">符号列表</string>
<string name="formatting_numbers">编号列表</string>
<string name="formatting_text_color">文字颜色</string>
<string name="formatting_bg_color">背景颜色</string>
<plurals name="search_results_title">
<item quantity="other"><xliff:g id="NUMBER">%1$s</xliff:g> 条符合“<xliff:g id="SEARCH">%2$s</xliff:g>”的搜索结果</item>
</plurals>

@ -88,8 +88,6 @@
<string name="error_note_not_exist">The note is not exist</string>
<string name="error_note_empty_for_clock">Sorry, can not set clock on empty note</string>
<string name="error_note_empty_for_send_to_desktop">Sorry, can not send and empty note to home</string>
<string name="error_formatting_not_supported_in_list_mode">Formatting is not supported in list mode</string>
<string name="error_please_select_text">Please select text first</string>
<string name="success_sdcard_export">Export successful</string>
<string name="failed_sdcard_export">Export fail</string>
<string name="format_exported_file_location">Export text file (%1$s) to SD (%2$s) directory</string>
@ -146,14 +144,6 @@
<string name="password_set_dialog_message">Enter password please</string>
<string name="password_error">Error, try again</string>
<string name="password_empty">The password can not be empty</string>
<!-- Rich text formatting -->
<string name="formatting_bold">Bold</string>
<string name="formatting_italic">Italic</string>
<string name="formatting_underline">Underline</string>
<string name="formatting_bullets">Bullet list</string>
<string name="formatting_numbers">Numbered list</string>
<string name="formatting_text_color">Text color</string>
<string name="formatting_bg_color">Background color</string>
<plurals name="search_results_title">
<item quantity="one"><xliff:g id="number" example="1">%1$s</xliff:g> result for \"<xliff:g id="search" example="???">%2$s</xliff:g>\"</item>
<!-- Case of 0 or 2 or more results. -->

@ -30,7 +30,7 @@ import net.micode.notes.data.Notes.NoteColumns;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 6;
private static final int DB_VERSION = 7;
public interface TABLE {
public static final String NOTE = "note";
@ -39,6 +39,8 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
public static final String TRASH = "trash_note";
public static final String TRASH_DATA = "trash_data";
public static final String ENCRYPTED_NOTE_PASSWORD = "encrypted_note_password";
}
@ -110,6 +112,21 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
"deleted_date INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)" +
")";
private static final String CREATE_TRASH_DATA_TABLE_SQL =
"CREATE TABLE " + TABLE.TRASH_DATA + "(" +
DataColumns.ID + " INTEGER PRIMARY KEY," +
DataColumns.MIME_TYPE + " TEXT NOT NULL," +
DataColumns.NOTE_ID + " INTEGER NOT NULL DEFAULT 0," +
NoteColumns.CREATED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
NoteColumns.MODIFIED_DATE + " INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)," +
DataColumns.CONTENT + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA1 + " INTEGER," +
DataColumns.DATA2 + " INTEGER," +
DataColumns.DATA3 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA4 + " TEXT NOT NULL DEFAULT ''," +
DataColumns.DATA5 + " TEXT NOT NULL DEFAULT ''" +
")";
private static final String CREATE_ENCRYPTED_NOTE_PASSWORD_TABLE_SQL =
"CREATE TABLE " + TABLE.ENCRYPTED_NOTE_PASSWORD + "(" +
NoteColumns.ID + " INTEGER PRIMARY KEY," +
@ -317,6 +334,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "trash_note table has been created");
}
public void createTrashDataTable(SQLiteDatabase db) {
db.execSQL(CREATE_TRASH_DATA_TABLE_SQL);
Log.d(TAG, "trash_data table has been created");
}
private void reCreateDataTableTriggers(SQLiteDatabase db) {
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_insert");
db.execSQL("DROP TRIGGER IF EXISTS update_note_content_on_update");
@ -339,6 +361,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
createNoteTable(db);
createDataTable(db);
createTrashTable(db);
createTrashDataTable(db);
createEncryptedNotePasswordTable(db);
}
@ -374,6 +397,11 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
if (oldVersion == 6) {
upgradeToV7(db);
oldVersion++;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
@ -434,6 +462,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.d(TAG, "Upgraded to version 6: added is_encrypted column and encrypted_note_password table");
}
private void upgradeToV7(SQLiteDatabase db) {
// 创建trash_data表用于存储被删除便签的DATA数据
createTrashDataTable(db);
Log.d(TAG, "Upgraded to version 7: created trash_data table");
}
public void createEncryptedNotePasswordTable(SQLiteDatabase db) {
db.execSQL(CREATE_ENCRYPTED_NOTE_PASSWORD_TABLE_SQL);
Log.d(TAG, "encrypted_note_password table has been created");

@ -379,7 +379,7 @@ public class GTaskManager {
// one
// clear local delete table
if (!mCancelled) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap)) {
if (!DataUtils.batchDeleteNotes(mContentResolver, mLocalDeleteIdMap, mContext)) {
throw new ActionFailureException("failed to batch-delete local deleted notes");
}
}

@ -21,15 +21,20 @@ import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.OperationApplicationException;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.RemoteException;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.CallNote;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TrashColumns;
import net.micode.notes.data.NotesDatabaseHelper;
import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
import java.util.ArrayList;
@ -42,13 +47,31 @@ public class DataUtils {
* Move notes to trash table instead of directly deleting them
*/
public static boolean batchMoveToTrash(ContentResolver resolver, HashSet<Long> ids) {
// 为了保持向后兼容如果没有Context使用旧逻辑不备份DATA数据
return batchMoveToTrash(resolver, ids, null);
}
/**
* Move notes to trash table instead of directly deleting them
* @param resolver ContentResolver
* @param ids Note IDs to move to trash
* @param context Context (optional, needed for backing up DATA table)
*/
public static boolean batchMoveToTrash(ContentResolver resolver, HashSet<Long> ids, Context context) {
if (ids == null || ids.size() == 0) {
Log.d(TAG, "the ids is null or empty");
return true;
}
Cursor cursor = null;
Cursor dataCursor = null;
SQLiteDatabase db = null;
try {
// 如果提供了Context获取数据库实例用于备份DATA数据
if (context != null) {
db = NotesDatabaseHelper.getInstance(context).getWritableDatabase();
}
for (long id : ids) {
if(id == Notes.ID_ROOT_FOLDER || id <= 0) {
Log.e(TAG, "Don't move system folder root or invalid id: " + id);
@ -93,7 +116,51 @@ public class DataUtils {
// Insert into trash table
resolver.insert(Notes.CONTENT_TRASH_URI, trashValues);
// Delete from note table
// 在删除note之前先备份DATA表的数据
if (db != null) {
// 查询该note的所有DATA数据
dataCursor = db.query(NotesDatabaseHelper.TABLE.DATA, null,
DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(id)},
null, null, null);
if (dataCursor != null && dataCursor.moveToFirst()) {
do {
// 复制DATA数据到trash_data表
ContentValues trashDataValues = new ContentValues();
for (String column : dataCursor.getColumnNames()) {
int columnIndex = dataCursor.getColumnIndex(column);
if (columnIndex >= 0) {
int type = dataCursor.getType(columnIndex);
switch (type) {
case Cursor.FIELD_TYPE_NULL:
break;
case Cursor.FIELD_TYPE_INTEGER:
trashDataValues.put(column, dataCursor.getLong(columnIndex));
break;
case Cursor.FIELD_TYPE_FLOAT:
trashDataValues.put(column, dataCursor.getDouble(columnIndex));
break;
case Cursor.FIELD_TYPE_STRING:
trashDataValues.put(column, dataCursor.getString(columnIndex));
break;
case Cursor.FIELD_TYPE_BLOB:
trashDataValues.put(column, dataCursor.getBlob(columnIndex));
break;
}
}
}
// 插入到trash_data表
db.insert(NotesDatabaseHelper.TABLE.TRASH_DATA, null, trashDataValues);
} while (dataCursor.moveToNext());
}
if (dataCursor != null) {
dataCursor.close();
dataCursor = null;
}
}
// Delete from note table (this will trigger deletion of DATA table via trigger)
resolver.delete(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id), null, null);
}
@ -110,12 +177,20 @@ public class DataUtils {
if (cursor != null) {
cursor.close();
}
if (dataCursor != null) {
dataCursor.close();
}
}
}
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids) {
// Instead of directly deleting, move to trash
return batchMoveToTrash(resolver, ids);
return batchMoveToTrash(resolver, ids, null);
}
public static boolean batchDeleteNotes(ContentResolver resolver, HashSet<Long> ids, Context context) {
// Instead of directly deleting, move to trash
return batchMoveToTrash(resolver, ids, context);
}
public static void moveNoteToFoler(ContentResolver resolver, long id, long srcFolderId, long desFolderId) {
@ -293,8 +368,26 @@ public class DataUtils {
* Restore note from trash table back to note table
*/
public static boolean restoreNoteFromTrash(ContentResolver resolver, long trashId) {
// 为了保持向后兼容如果没有Context使用旧逻辑不恢复DATA数据
return restoreNoteFromTrash(resolver, trashId, null);
}
/**
* Restore note from trash table back to note table
* @param resolver ContentResolver
* @param trashId Trash note ID to restore
* @param context Context (optional, needed for restoring DATA table)
*/
public static boolean restoreNoteFromTrash(ContentResolver resolver, long trashId, Context context) {
Cursor cursor = null;
Cursor dataCursor = null;
SQLiteDatabase db = null;
try {
// 如果提供了Context获取数据库实例用于恢复DATA数据
if (context != null) {
db = NotesDatabaseHelper.getInstance(context).getWritableDatabase();
}
// Query the note from trash table
cursor = resolver.query(ContentUris.withAppendedId(Notes.CONTENT_TRASH_URI, trashId),
null, null, null, null);
@ -302,6 +395,7 @@ public class DataUtils {
if (cursor != null && cursor.moveToFirst()) {
// Read all columns from the trash note
ContentValues noteValues = new ContentValues();
long originalNoteId = -1;
// Copy all columns from trash to note (except deleted_date)
for (String column : cursor.getColumnNames()) {
@ -315,7 +409,12 @@ public class DataUtils {
case Cursor.FIELD_TYPE_NULL:
break;
case Cursor.FIELD_TYPE_INTEGER:
noteValues.put(column, cursor.getLong(columnIndex));
long value = cursor.getLong(columnIndex);
noteValues.put(column, value);
// 保存原始note ID
if (NoteColumns.ID.equals(column)) {
originalNoteId = value;
}
break;
case Cursor.FIELD_TYPE_FLOAT:
noteValues.put(column, cursor.getDouble(columnIndex));
@ -331,7 +430,73 @@ public class DataUtils {
}
// Insert back to note table
resolver.insert(Notes.CONTENT_NOTE_URI, noteValues);
Uri restoredUri = resolver.insert(Notes.CONTENT_NOTE_URI, noteValues);
if (restoredUri == null) {
Log.e(TAG, "Failed to restore note to note table");
return false;
}
// 获取恢复后的note ID可能是新的ID
long restoredNoteId = ContentUris.parseId(restoredUri);
if (restoredNoteId <= 0) {
Log.e(TAG, "Invalid restored note ID: " + restoredNoteId);
return false;
}
// 恢复DATA表的数据
if (db != null && originalNoteId > 0) {
// 查询trash_data表中该note的所有数据
dataCursor = db.query(NotesDatabaseHelper.TABLE.TRASH_DATA, null,
DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(originalNoteId)},
null, null, null);
if (dataCursor != null && dataCursor.moveToFirst()) {
do {
// 复制DATA数据回data表但使用新的note ID
ContentValues dataValues = new ContentValues();
for (String column : dataCursor.getColumnNames()) {
int columnIndex = dataCursor.getColumnIndex(column);
if (columnIndex >= 0) {
int type = dataCursor.getType(columnIndex);
switch (type) {
case Cursor.FIELD_TYPE_NULL:
break;
case Cursor.FIELD_TYPE_INTEGER:
long value = dataCursor.getLong(columnIndex);
// 如果是NOTE_ID列使用恢复后的note ID
if (DataColumns.NOTE_ID.equals(column)) {
dataValues.put(column, restoredNoteId);
} else {
dataValues.put(column, value);
}
break;
case Cursor.FIELD_TYPE_FLOAT:
dataValues.put(column, dataCursor.getDouble(columnIndex));
break;
case Cursor.FIELD_TYPE_STRING:
dataValues.put(column, dataCursor.getString(columnIndex));
break;
case Cursor.FIELD_TYPE_BLOB:
dataValues.put(column, dataCursor.getBlob(columnIndex));
break;
}
}
}
// 插入到data表
db.insert(NotesDatabaseHelper.TABLE.DATA, null, dataValues);
} while (dataCursor.moveToNext());
// 删除trash_data表中的数据
db.delete(NotesDatabaseHelper.TABLE.TRASH_DATA,
DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(originalNoteId)});
}
if (dataCursor != null) {
dataCursor.close();
dataCursor = null;
}
}
// Delete from trash table
resolver.delete(ContentUris.withAppendedId(Notes.CONTENT_TRASH_URI, trashId), null, null);
@ -346,6 +511,9 @@ public class DataUtils {
if (cursor != null) {
cursor.close();
}
if (dataCursor != null) {
dataCursor.close();
}
}
}
@ -353,7 +521,25 @@ public class DataUtils {
* Permanently delete note from trash table
*/
public static boolean permanentlyDeleteFromTrash(ContentResolver resolver, long trashId) {
return permanentlyDeleteFromTrash(resolver, trashId, null);
}
/**
* Permanently delete note from trash table
* @param resolver ContentResolver
* @param trashId Trash note ID to permanently delete
* @param context Context (optional, needed for deleting trash_data table)
*/
public static boolean permanentlyDeleteFromTrash(ContentResolver resolver, long trashId, Context context) {
try {
// 如果提供了Context先删除trash_data表中的数据
if (context != null) {
SQLiteDatabase db = NotesDatabaseHelper.getInstance(context).getWritableDatabase();
db.delete(NotesDatabaseHelper.TABLE.TRASH_DATA,
DataColumns.NOTE_ID + "=?",
new String[]{String.valueOf(trashId)});
}
int count = resolver.delete(ContentUris.withAppendedId(Notes.CONTENT_TRASH_URI, trashId), null, null);
return count > 0;
} catch (Exception e) {
@ -366,7 +552,22 @@ public class DataUtils {
* Clear all notes from trash table
*/
public static boolean clearAllTrash(ContentResolver resolver) {
return clearAllTrash(resolver, null);
}
/**
* Clear all notes from trash table
* @param resolver ContentResolver
* @param context Context (optional, needed for clearing trash_data table)
*/
public static boolean clearAllTrash(ContentResolver resolver, Context context) {
try {
// 如果提供了Context先清空trash_data表
if (context != null) {
SQLiteDatabase db = NotesDatabaseHelper.getInstance(context).getWritableDatabase();
db.delete(NotesDatabaseHelper.TABLE.TRASH_DATA, null, null);
}
int count = resolver.delete(Notes.CONTENT_TRASH_URI, null, null);
Log.d(TAG, "Cleared " + count + " notes from trash");
// Return true even if count is 0, as it might mean the trash was already empty

@ -33,12 +33,8 @@ import android.preference.PreferenceManager;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.Html;
import android.text.format.DateUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.StyleSpan;
import android.text.style.UnderlineSpan;
import android.graphics.Typeface;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@ -68,7 +64,16 @@ import android.content.ContentResolver;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import java.io.InputStream;
import java.io.FileOutputStream;
import java.io.OutputStream;
import android.os.Environment;
import android.text.format.DateUtils;
import android.view.ViewGroup;
import android.content.ActivityNotFoundException;
import jp.wasabeef.richeditor.RichEditor;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
@ -161,22 +166,20 @@ public class NoteEditActivity extends Activity implements OnClickListener,
private static final String TAG = "NoteEditActivity"; // 日志标签
private static final int REQUEST_CODE_PICK_IMAGE = 100; // 图片选择请求码 OMO
private static final int PHOTO_REQUEST = 100; // 请求照片
private HeadViewHolder mNoteHeaderHolder; // 头部视图持有者
private View mHeadViewPanel; // 头部视图面板
private View mNoteBgColorSelector; // 背景颜色选择器
private View mFontSizeSelector; // 字体大小选择器
private EditText mNoteEditor; // 笔记编辑器
private RichEditor mNoteEditor; // 富文本编辑器
private View mNoteEditorPanel; // 笔记编辑器面板
private WorkingNote mWorkingNote; // 工作笔记对象
private SharedPreferences mSharedPrefs; // 共享偏好设置
private int mFontSizeId; // 字体大小ID
private ImageButton mBtnInsertImage; // 插入图片按钮 OMO
// 富文本工具栏按钮
private ImageButton mBtnBold; // 加粗按钮
private ImageButton mBtnItalic; // 斜体按钮
private ImageButton mBtnUnderline; // 下划线按钮
private String mText; // 用于存储富文本内容
private int mNoteLength; // 文本长度
private static final String PREFERENCE_FONT_SIZE = "pref_font_size"; // 字体大小偏好设置键
private static final int SHORTCUT_ICON_TITLE_MAX_LEN = 10; // 快捷图标标题最大长度
@ -348,96 +351,64 @@ public class NoteEditActivity extends Activity implements OnClickListener,
*
*/
private void initNoteScreen() {
// 设置笔记编辑器的文本外观
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
// 检查必要的视图是否已初始化
if (mHeadViewPanel == null || mNoteEditorPanel == null || mNoteEditor == null) {
Log.e(TAG, "Some views are not initialized! Check initResources method.");
return;
}
// 设置富文本编辑器字体大小
setRichEditorFontSize(mFontSizeId);
// 根据笔记模式设置显示方式
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent()); // 切换到清单模式
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
} else {
// 设置笔记内容并高亮查询结果
String content = mWorkingNote.getContent();
Log.d(TAG, "Initializing note content: " + content);
// 切换到富文本模式
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
// 获取笔记原始内容(为空则赋空字符串,避免空指针)
String content = mWorkingNote.getContent() == null ? "" : mWorkingNote.getContent();
if (TextUtils.isEmpty(content)) {
mNoteEditor.setText("");
mNoteEditor.setSelection(0);
} else {
Spannable spannable;
// 检查是否是HTML格式包含HTML标签
if (content.contains("<") && content.contains(">")) {
try {
// 从HTML格式恢复SpannableString保留样式加粗、斜体、下划线
// Html.fromHtml会自动将HTML标签转换为对应的Span
spannable = (Spannable) Html.fromHtml(content);
} catch (Exception e) {
// 如果HTML解析失败使用普通文本
Log.e(TAG, "Error parsing HTML content: " + e.getMessage());
spannable = new SpannableString(content);
}
} else {
// 普通文本格式直接创建SpannableString
spannable = new SpannableString(content);
}
// 处理图片:将包含[IMAGE]标签的文本转换为包含ImageSpan的SpannableString
// 注意convertTextToSpannableWithImages会处理[IMAGE]标记,但会保留文本内容
String textContent = spannable.toString();
SpannableString finalSpannable = convertTextToSpannableWithImages(textContent);
// 将样式从原始Spannable复制到处理后的SpannableString
// 由于convertTextToSpannableWithImages可能改变了文本长度用空格替换了[IMAGE]标签),
// 我们需要基于文本内容来映射样式位置
if (textContent.length() == finalSpannable.length()) {
// 文本长度相同,直接复制样式
StyleSpan[] styleSpans = spannable.getSpans(0, spannable.length(), StyleSpan.class);
UnderlineSpan[] underlineSpans = spannable.getSpans(0, spannable.length(), UnderlineSpan.class);
for (StyleSpan span : styleSpans) {
int start = spannable.getSpanStart(span);
int end = spannable.getSpanEnd(span);
if (start >= 0 && end <= finalSpannable.length() && start < end) {
finalSpannable.setSpan(new StyleSpan(span.getStyle()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
for (UnderlineSpan span : underlineSpans) {
int start = spannable.getSpanStart(span);
int end = spannable.getSpanEnd(span);
if (start >= 0 && end <= finalSpannable.length() && start < end) {
finalSpannable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else {
// 文本长度不同,说明有图片被处理了
// 这种情况下,样式位置需要重新计算
// 为了简化,我们基于原始文本位置映射样式(可能会有偏差,但应该能处理大部分情况)
StyleSpan[] styleSpans = spannable.getSpans(0, spannable.length(), StyleSpan.class);
UnderlineSpan[] underlineSpans = spannable.getSpans(0, spannable.length(), UnderlineSpan.class);
for (StyleSpan span : styleSpans) {
int start = Math.max(0, Math.min(spannable.getSpanStart(span), finalSpannable.length()));
int end = Math.max(start, Math.min(spannable.getSpanEnd(span), finalSpannable.length()));
if (start < end) {
finalSpannable.setSpan(new StyleSpan(span.getStyle()), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
for (UnderlineSpan span : underlineSpans) {
int start = Math.max(0, Math.min(spannable.getSpanStart(span), finalSpannable.length()));
int end = Math.max(start, Math.min(spannable.getSpanEnd(span), finalSpannable.length()));
if (start < end) {
finalSpannable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
// 检查内容是否已经是HTML格式如果是则直接加载
String finalHtml = content;
// 如果内容不是HTML格式比如旧笔记则处理图片标记
if (!TextUtils.isEmpty(content) && (!content.startsWith("<") || !content.contains("</"))) {
// 处理 [IMAGE] 标签,转换为 HTML img 标签
Pattern imgPattern = Pattern.compile("\\[IMAGE\\]([^\\[]+)\\[/IMAGE\\]");
Matcher imgMatcher = imgPattern.matcher(content);
StringBuffer htmlContent = new StringBuffer();
// 遍历替换所有[IMAGE]标记为<img>标签
while (imgMatcher.find()) {
String imgUri = imgMatcher.group(1); // 提取图片URI
// 检查是否为本地文件路径
if (imgUri.startsWith("file://") || imgUri.startsWith("/")) {
String imgPath = imgUri.startsWith("file://") ? imgUri.substring(7) : imgUri;
File imgFile = new File(imgPath);
if (imgFile.exists() && imgFile.isFile()) {
String imgHtmlUrl = "file://" + imgPath;
String imgHtmlTag = "<img src=\"" + imgHtmlUrl + "\" width=\"200\" height=\"200\"/><br/>";
imgMatcher.appendReplacement(htmlContent, imgHtmlTag);
} else {
// 如果文件不存在,保留原标记
imgMatcher.appendReplacement(htmlContent, imgMatcher.group(0));
}
} else {
// URI格式直接使用
String imgHtmlTag = "<img src=\"" + imgUri + "\" width=\"200\" height=\"200\"/><br/>";
imgMatcher.appendReplacement(htmlContent, imgHtmlTag);
}
}
mNoteEditor.setText(finalSpannable);
// 将光标定位到文本末尾
mNoteEditor.setSelection(finalSpannable.length());
imgMatcher.appendTail(htmlContent); // 拼接剩余文本
finalHtml = htmlContent.toString();
}
// 用RichEditor加载HTML内容
mNoteEditor.setHtml(finalHtml);
}
// 隐藏所有背景选择的选中状态
@ -641,7 +612,36 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteHeaderHolder.tvAlertDate = (TextView) findViewById(R.id.tv_alert_date);
mNoteHeaderHolder.ibSetBgColor = (ImageView) findViewById(R.id.btn_set_bg_color);
mNoteHeaderHolder.ibSetBgColor.setOnClickListener(this);
mNoteEditor = (EditText) findViewById(R.id.note_edit_view);
// 初始化富文本编辑器
mNoteEditor = (RichEditor) findViewById(R.id.note_edit_view);
if (mNoteEditor == null) {
Log.e(TAG, "RichEditor is null! Check layout file.");
return;
}
// 初始化富文本编辑器配置
initRichEditor();
// 设置富文本编辑器监听器
mNoteEditor.setOnTextChangeListener(new RichEditor.OnTextChangeListener() {
@Override
public void onTextChange(String text) {
mText = text;
mNoteLength = text.length();
// 更新修改时间和字符数显示
mNoteHeaderHolder.tvModified.setText(
DateUtils.formatDateTime(NoteEditActivity.this,
mWorkingNote.getModifiedDate(),
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NUMERIC_DATE
| DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_SHOW_YEAR)
+ "\n字符数" + mNoteLength
);
}
});
// 开启图文混排支持
mNoteEditor.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
mNoteEditorPanel = findViewById(R.id.sv_note_edit);
mNoteBgColorSelector = findViewById(R.id.note_bg_color_selector);
@ -680,21 +680,8 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mBtnInsertImage.setOnClickListener(this);
}
// 初始化富文本工具栏按钮
mBtnBold = (ImageButton) findViewById(R.id.btn_bold);
mBtnItalic = (ImageButton) findViewById(R.id.btn_italic);
mBtnUnderline = (ImageButton) findViewById(R.id.btn_underline);
// 为富文本工具栏按钮设置点击监听器
if (mBtnBold != null) {
mBtnBold.setOnClickListener(this);
}
if (mBtnItalic != null) {
mBtnItalic.setOnClickListener(this);
}
if (mBtnUnderline != null) {
mBtnUnderline.setOnClickListener(this);
}
// 初始化富文本功能按钮
initRichEditorButtons();
}
/**
@ -765,22 +752,13 @@ public class NoteEditActivity extends Activity implements OnClickListener,
getWorkingText();
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setTextAppearance(this,
TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
// 设置富文本编辑器字体大小
setRichEditorFontSize(mFontSizeId);
}
mFontSizeSelector.setVisibility(View.GONE);
} else if (id == R.id.btn_insert_image) {
// 处理插入图片按钮点击事件 OMO
pickImageFromGallery();
} else if (id == R.id.btn_bold) {
// 富文本工具栏 - 加粗按钮
applyBoldStyle();
} else if (id == R.id.btn_italic) {
// 富文本工具栏 - 斜体按钮
applyItalicStyle();
} else if (id == R.id.btn_underline) {
// 富文本工具栏 - 下划线按钮
applyUnderlineStyle();
}
}
@ -969,7 +947,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
if (!isSyncMode()) {
// 非同步模式下直接删除
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids)) {
if (!DataUtils.batchDeleteNotes(getContentResolver(), ids, NoteEditActivity.this)) {
Log.e(TAG, "Delete Note error");
}
} else {
@ -1007,7 +985,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (mWorkingNote.getNoteId() > 0) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
@ -1221,8 +1199,9 @@ public class NoteEditActivity extends Activity implements OnClickListener,
String text = "";
if (newMode == TextNote.MODE_CHECK_LIST) {
// 安全获取文本避免空指针异常OMO
if (mNoteEditor.getText() != null) {
text = mNoteEditor.getText().toString();
String htmlContent = mNoteEditor.getHtml();
if (htmlContent != null) {
text = htmlContent;
}
switchToListMode(text);
} else {
@ -1232,12 +1211,35 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mWorkingNote.setWorkingText(content.replace(TAG_UNCHECKED + " ", ""));
}
}
String content = mWorkingNote.getContent();
if (content != null) {
mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery));
} else {
mNoteEditor.setText("");
String content = mWorkingNote.getContent() == null ? "" : mWorkingNote.getContent();
// 处理内容为HTML格式
String finalHtml = content;
if (!TextUtils.isEmpty(content) && (!content.startsWith("<") || !content.contains("</"))) {
// 处理 [IMAGE] 标签
Pattern imgPattern = Pattern.compile("\\[IMAGE\\]([^\\[]+)\\[/IMAGE\\]");
Matcher imgMatcher = imgPattern.matcher(content);
StringBuffer htmlContent = new StringBuffer();
while (imgMatcher.find()) {
String imgUri = imgMatcher.group(1);
if (imgUri.startsWith("file://") || imgUri.startsWith("/")) {
String imgPath = imgUri.startsWith("file://") ? imgUri.substring(7) : imgUri;
File imgFile = new File(imgPath);
if (imgFile.exists() && imgFile.isFile()) {
String imgHtmlUrl = "file://" + imgPath;
String imgHtmlTag = "<img src=\"" + imgHtmlUrl + "\" width=\"200\" height=\"200\"/><br/>";
imgMatcher.appendReplacement(htmlContent, imgHtmlTag);
} else {
imgMatcher.appendReplacement(htmlContent, imgMatcher.group(0));
}
} else {
String imgHtmlTag = "<img src=\"" + imgUri + "\" width=\"200\" height=\"200\"/><br/>";
imgMatcher.appendReplacement(htmlContent, imgHtmlTag);
}
}
imgMatcher.appendTail(htmlContent);
finalHtml = htmlContent.toString();
}
mNoteEditor.setHtml(finalHtml);
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
@ -1266,16 +1268,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
mWorkingNote.setWorkingText(sb.toString());
} else {
// 保存时将SpannableString转换为HTML格式以保留样式信息
Spannable spannable = mNoteEditor.getText();
if (spannable != null && spannable.length() > 0) {
// 将Spannable转换为HTML格式保留加粗、斜体、下划线等样式
// Html.toHtml会自动处理StyleSpan和UnderlineSpan
String htmlContent = Html.toHtml(spannable);
mWorkingNote.setWorkingText(htmlContent);
} else {
mWorkingNote.setWorkingText("");
}
// 确保获取最新的富文本内容
String currentHtml = mNoteEditor.getHtml();
mWorkingNote.setWorkingText(currentHtml);
mText = currentHtml; // 更新mText变量确保保存时使用最新内容
}
return hasChecked;
}
@ -1373,8 +1369,22 @@ public class NoteEditActivity extends Activity implements OnClickListener,
* OMO
*/
private void pickImageFromGallery() {
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
try {
// 意图:打开系统相册选择图片
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
intent.setType("image/*"); // 只显示图片类型
startActivityForResult(intent, PHOTO_REQUEST); // 启动相册,等待返回结果
} catch (ActivityNotFoundException e) {
// 如果没有相册应用,尝试使用通用选择器
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
try {
startActivityForResult(intent, PHOTO_REQUEST);
} catch (ActivityNotFoundException ex) {
showToast(R.string.error_note_not_exist);
Log.e(TAG, "No image picker available", ex);
}
}
}
/**
@ -1383,204 +1393,177 @@ public class NoteEditActivity extends Activity implements OnClickListener,
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK && data != null) {
Log.d(TAG, "Image selected successfully, data: " + data.toString());
Uri imageUri = data.getData();
if (imageUri != null) {
Log.d(TAG, "Image URI: " + imageUri.toString());
try {
// 将图片路径添加到笔记内容中
String currentContent = mNoteEditor.getText().toString();
String imagePath = "[IMAGE]" + imageUri.toString() + "[/IMAGE]";
String newContent = currentContent + (currentContent.isEmpty() ? "" : "\n") + imagePath;
Log.d(TAG, "New content: " + newContent);
// 直接设置文本不使用ImageSpan这样可以确保内容被保存
mNoteEditor.setText(newContent);
mNoteEditor.setSelection(newContent.length());
// 保存笔记内容,确保图片路径被正确存储
saveNote();
showToast(R.string.info_image_inserted);
} catch (Exception e) {
Log.e(TAG, "Error inserting image: " + e.toString());
showToast(R.string.error_note_not_exist);
if (requestCode == PHOTO_REQUEST && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();
String localImagePath = saveImageToLocal(uri);
if (localImagePath == null) return; // 保存失败就退出
// 核心修改适配RichEditor替换EditText的ImageSpan逻辑
// 1. 拼接RichEditor支持的<img>标签必须加file://前缀)
String imgUrl = "file://" + localImagePath;
String imgHtmlTag = "<img src=\"" + imgUrl + "\" width=\"200\" height=\"200\"/><br/>";
// 2. 插入图片到RichEditor
String curHtml = mNoteEditor.getHtml(); // 获取当前内容
String newHtml = curHtml + imgHtmlTag; // 追加图片标签
mNoteEditor.setHtml(newHtml); // 重新设置内容,实现插入
// 弹窗依然保留
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("图片选择成功!");
ImageView imageView = new ImageView(this);
imageView.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)); // 加布局参数,避免图片显示不全
imageView.setImageURI(Uri.fromFile(new File(localImagePath)));//弹窗也显示本地图片
imageView.setScaleType(ImageView.ScaleType.FIT_CENTER); // 适配图片大小
builder.setView(imageView);
builder.setPositiveButton("确认保存", (dialog, which) -> {
String currentHtml = mNoteEditor.getHtml(); // 替换原EditText的getText()
String newContent = mWorkingNote.getContent() == null ? "" : mWorkingNote.getContent();
newContent += "\n[IMAGE]" + localImagePath + "[/IMAGE]"; // 保留原有[IMAGE]标记,供后续加载解析
Log.d("NoteDebug", "准备保存的内容:" + newContent); // 看Logcat里的输出
// 执行保存操作
mWorkingNote.setWorkingText(newContent);
boolean isSaved = mWorkingNote.saveNote();
// 根据保存结果提示(更友好)
if (isSaved) {
Toast.makeText(this, "图片信息已保存!", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "保存失败,请重试", Toast.LENGTH_SHORT).show();
}
} else {
Log.e(TAG, "Image URI is null");
showToast(R.string.error_note_not_exist);
}
} else {
Log.d(TAG, "Image selection canceled or failed, resultCode: " + resultCode);
}
}
});
/**
* 线
* @param styleType Typeface.BOLD, Typeface.ITALIC, -1 线
*/
private void applyTextStyle(int styleType) {
if (mNoteEditor == null) {
return;
}
// 检查是否在清单模式,如果是则提示用户切换到普通模式
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
showToast(R.string.error_formatting_not_supported_in_list_mode);
return;
builder.show();
}
}
Editable editable = mNoteEditor.getText();
if (editable == null) {
return;
}
int selectionStart = mNoteEditor.getSelectionStart();
int selectionEnd = mNoteEditor.getSelectionEnd();
// 如果没有选中文本,则选中光标所在的单词或当前字符
if (selectionStart == selectionEnd) {
// 尝试选中光标所在的单词
String text = editable.toString();
int start = selectionStart;
int end = selectionEnd;
// 向前查找单词开始位置
while (start > 0 && Character.isLetterOrDigit(text.charAt(start - 1))) {
start--;
}
// 向后查找单词结束位置
while (end < text.length() && Character.isLetterOrDigit(text.charAt(end))) {
end++;
}
// 如果找到了单词,则选中它;否则选中当前字符
if (start < end) {
selectionStart = start;
selectionEnd = end;
mNoteEditor.setSelection(selectionStart, selectionEnd);
} else {
// 如果没有找到单词,提示用户先选择文本
showToast(R.string.error_please_select_text);
return;
// 新增工具方法把临时URI的图片复制到应用私有目录返回真实路径
private String saveImageToLocal(Uri uri) {
try {
// 1. 创建应用专属图片目录(不会被系统清理)
File appDir = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "note_images");
if (!appDir.exists()) appDir.mkdirs();
// 2. 生成唯一文件名(避免重复)
String fileName = "note_" + System.currentTimeMillis() + ".jpg";
File targetFile = new File(appDir, fileName);
// 3. 复制图片文件从临时URI到本地目录
InputStream is = getContentResolver().openInputStream(uri);
OutputStream os = new FileOutputStream(targetFile);
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
os.write(buffer, 0, len);
}
is.close();
os.close();
// 返回图片的真实本地路径不是URI
return targetFile.getAbsolutePath();
} catch (Exception e) {
Log.e("NoteEdit", "保存图片失败", e);
Toast.makeText(this, "图片保存失败", Toast.LENGTH_SHORT).show();
return null;
}
}
// 确保选择范围有效
if (selectionStart < 0 || selectionEnd > editable.length() || selectionStart >= selectionEnd) {
showToast(R.string.error_please_select_text);
return;
// 自定义方法给RichEditor设置字体大小对应原EditText的setTextAppearance
private void setRichEditorFontSize(int fontSizeId) {
switch (fontSizeId) {
case ResourceParser.TEXT_SMALL:
mNoteEditor.setEditorFontSize(14); // 小字体
break;
case ResourceParser.TEXT_MEDIUM:
mNoteEditor.setEditorFontSize(18); // 中字体(默认)
break;
case ResourceParser.TEXT_LARGE:
mNoteEditor.setEditorFontSize(22); // 大字体
break;
case ResourceParser.TEXT_SUPER:
mNoteEditor.setEditorFontSize(26); // 超大字体
break;
default:
mNoteEditor.setEditorFontSize(18); // 默认值
}
}
// 应用样式
if (styleType == -1) {
// 下划线样式
UnderlineSpan[] underlineSpans = editable.getSpans(selectionStart, selectionEnd, UnderlineSpan.class);
if (underlineSpans.length > 0) {
// 移除下划线
for (UnderlineSpan span : underlineSpans) {
int spanStart = editable.getSpanStart(span);
int spanEnd = editable.getSpanEnd(span);
editable.removeSpan(span);
// 如果移除的span范围大于选中范围需要重新应用样式到剩余部分
if (spanStart < selectionStart) {
editable.setSpan(new UnderlineSpan(), spanStart, selectionStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (spanEnd > selectionEnd) {
editable.setSpan(new UnderlineSpan(), selectionEnd, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
} else {
// 添加下划线
editable.setSpan(new UnderlineSpan(), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
} else {
// 加粗或斜体样式
StyleSpan[] styleSpans = editable.getSpans(selectionStart, selectionEnd, StyleSpan.class);
// 检查选中范围内是否已经有该样式
boolean hasStyle = false;
int existingStyle = 0;
// 收集选中范围内的所有样式
for (StyleSpan span : styleSpans) {
int spanStart = editable.getSpanStart(span);
int spanEnd = editable.getSpanEnd(span);
// 如果span与选中范围重叠
if (spanStart < selectionEnd && spanEnd > selectionStart) {
int spanStyle = span.getStyle();
existingStyle |= spanStyle;
// 检查是否包含目标样式
if ((spanStyle & styleType) == styleType) {
hasStyle = true;
}
}
// 初始化富文本编辑器配置
private void initRichEditor() {
mNoteEditor.setEditorHeight(600); // 设置编辑器高度
mNoteEditor.setEditorFontSize(16); // 字体大小
mNoteEditor.setEditorFontColor(Color.BLACK); // 字体颜色
mNoteEditor.setPadding(10, 10, 10, 10); // 内边距
mNoteEditor.setPlaceholder("请输入笔记内容..."); // 占位提示
mNoteEditor.setInputEnabled(true); // 允许输入
}
// 添加富文本功能按钮初始化方法
private void initRichEditorButtons() {
// 撤销功能
findViewById(R.id.action_undo).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNoteEditor.undo();
}
// 移除选中范围内的所有StyleSpan以便重新应用
for (int i = styleSpans.length - 1; i >= 0; i--) {
StyleSpan span = styleSpans[i];
int spanStart = editable.getSpanStart(span);
int spanEnd = editable.getSpanEnd(span);
// 如果span与选中范围重叠
if (spanStart < selectionEnd && spanEnd > selectionStart) {
editable.removeSpan(span);
// 处理span范围大于选中范围的情况保留未选中部分的样式
int spanStyle = span.getStyle();
if (spanStart < selectionStart) {
// 保留选中范围之前的样式
editable.setSpan(new StyleSpan(spanStyle), spanStart, selectionStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
if (spanEnd > selectionEnd) {
// 保留选中范围之后的样式
editable.setSpan(new StyleSpan(spanStyle), selectionEnd, spanEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
// 加粗功能
findViewById(R.id.action_bold).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNoteEditor.setBold();
}
// 切换样式:如果有则移除,如果没有则添加
int finalStyle = existingStyle;
if (hasStyle) {
// 移除目标样式
finalStyle &= ~styleType;
} else {
// 添加目标样式
finalStyle |= styleType;
});
// 斜体功能
findViewById(R.id.action_italic).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNoteEditor.setItalic();
}
// 应用最终样式
if (finalStyle != 0) {
editable.setSpan(new StyleSpan(finalStyle), selectionStart, selectionEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
});
// 背景色功能
findViewById(R.id.action_bg_color).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mNoteEditor.focusEditor(); // 获取焦点
new AlertDialog.Builder(NoteEditActivity.this)
.setTitle("选择字体背景颜色")
.setItems(new String[]{"红色", "黄色", "蓝色", "绿色", "黑色", "白色"},
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
switch (which) {
case 0: // 红
mNoteEditor.setTextBackgroundColor(Color.RED);
break;
case 1: // 黄
mNoteEditor.setTextBackgroundColor(Color.YELLOW);
break;
case 2: // 蓝
mNoteEditor.setTextBackgroundColor(Color.BLUE);
break;
case 3: // 绿
mNoteEditor.setTextBackgroundColor(Color.GREEN);
break;
case 4: // 黑
mNoteEditor.setTextBackgroundColor(Color.BLACK);
break;
case 5: // 白
mNoteEditor.setTextBackgroundColor(Color.WHITE);
break;
}
dialog.dismiss(); // 选择后关闭对话框
}
})
.show();
}
}
// 保持选中状态
mNoteEditor.setSelection(selectionStart, selectionEnd);
}
/**
*
*/
private void applyBoldStyle() {
applyTextStyle(Typeface.BOLD);
}
/**
*
*/
private void applyItalicStyle() {
applyTextStyle(Typeface.ITALIC);
}
/**
* 线
*/
private void applyUnderlineStyle() {
applyTextStyle(-1); // -1 表示下划线
});
}
}
}

@ -684,13 +684,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
private void batchDelete() {
final NotesListActivity activity = this;
new AsyncTask<Void, Void, HashSet<AppWidgetAttribute>>() {
protected HashSet<AppWidgetAttribute> doInBackground(Void... unused) {
HashSet<AppWidgetAttribute> widgets = mNotesListAdapter.getSelectedWidget();
if (!isSyncMode()) {
// if not synced, delete notes directly
if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
.getSelectedItemIds())) {
.getSelectedItemIds(), activity)) {
} else {
Log.e(TAG, "Delete notes error, should not happens");
}
@ -736,7 +737,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
folderId);
if (!isSyncMode()) {
// if not synced, delete folder directly
DataUtils.batchDeleteNotes(mContentResolver, ids);
DataUtils.batchDeleteNotes(mContentResolver, ids, this);
} else {
// in sync mode, we'll move the deleted folder into the trash folder
DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);

@ -140,7 +140,7 @@ public class TrashActivity extends Activity implements OnClickListener {
}
private void permanentlyDeleteNote(long noteId) {
if (DataUtils.permanentlyDeleteFromTrash(mContentResolver, noteId)) {
if (DataUtils.permanentlyDeleteFromTrash(mContentResolver, noteId, this)) {
Toast.makeText(this, getString(R.string.menu_delete) + " " + getString(R.string.menu_success),
Toast.LENGTH_SHORT).show();
startAsyncTrashListQuery(); // Refresh list
@ -151,7 +151,7 @@ public class TrashActivity extends Activity implements OnClickListener {
}
private void recoverNote(long noteId) {
if (DataUtils.restoreNoteFromTrash(mContentResolver, noteId)) {
if (DataUtils.restoreNoteFromTrash(mContentResolver, noteId, this)) {
Toast.makeText(this, getString(R.string.button_recovery) + " " + getString(R.string.menu_success),
Toast.LENGTH_SHORT).show();
startAsyncTrashListQuery(); // Refresh list
@ -177,7 +177,7 @@ public class TrashActivity extends Activity implements OnClickListener {
}
private void clearAllTrash() {
if (DataUtils.clearAllTrash(mContentResolver)) {
if (DataUtils.clearAllTrash(mContentResolver, this)) {
Toast.makeText(this, getString(R.string.button_clean_all) + " " + getString(R.string.menu_success),
Toast.LENGTH_SHORT).show();
startAsyncTrashListQuery(); // Refresh list to clear UI

Loading…
Cancel
Save