();
@@ -712,6 +918,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mWorkingNote.markDeleted(true);
}
+ /**
+ * 判断当前是否处于同步模式(已配置 Google 账户)。
+ * 同步模式下删除会移动至回收站,非同步模式下直接物理删除。
+ *
+ * @return true 表示已配置同步账户
+ */
private boolean isSyncMode() {
return NotesPreferenceActivity.getSyncAccountName(this).trim().length() > 0;
}
@@ -721,15 +933,31 @@ public class NoteEditActivity extends Activity implements OnClickListener,
saveNote();
}
if (mWorkingNote.getNoteId() > 0) {
- Intent intent = new Intent(this, AlarmReceiver.class);
+ if (set && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(this,
+ new String[]{Manifest.permission.POST_NOTIFICATIONS}, 0);
+ }
+ }
+ Intent intent = new Intent(this, NoteReminderReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
- PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
+ int flags = PendingIntent.FLAG_UPDATE_CURRENT;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ flags |= PendingIntent.FLAG_IMMUTABLE;
+ }
+ int requestCode = (int) (mWorkingNote.getNoteId() % Integer.MAX_VALUE);
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(this, requestCode, intent, flags);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
alarmManager.cancel(pendingIntent);
} else {
- alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ } else {
+ alarmManager.set(AlarmManager.RTC_WAKEUP, date, pendingIntent);
+ }
}
} else {
Log.e(TAG, "Clock alert setting error");
@@ -766,6 +994,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
edit.append(text);
edit.requestFocus();
edit.setSelection(length);
+ updateCharCount();
}
// 处理清单项回车(新增一行)
@@ -783,6 +1012,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
((NoteEditText) mEditTextList.getChildAt(i).findViewById(R.id.et_edit_text))
.setIndex(i);
}
+ updateCharCount();
}
// 切换到清单模式:将文本拆分为列表项
@@ -801,21 +1031,26 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mNoteEditor.setVisibility(View.GONE);
mEditTextList.setVisibility(View.VISIBLE);
+ updateCharCount();
}
// 获取带有高亮的 Spannable 文本 (用于搜索结果高亮)
private Spannable getHighlightQueryResult(String fullText, String userQuery) {
- SpannableString spannable = new SpannableString(fullText == null ? "" : fullText);
+ String safeText = fullText == null ? "" : fullText;
+ SpannableString spannable = new SpannableString(safeText);
if (!TextUtils.isEmpty(userQuery)) {
- mPattern = Pattern.compile(userQuery);
- Matcher m = mPattern.matcher(fullText);
- int start = 0;
- while (m.find(start)) {
- spannable.setSpan(
- new BackgroundColorSpan(this.getResources().getColor(
- R.color.user_query_highlight)), m.start(), m.end(),
- Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
- start = m.end();
+ try {
+ mPattern = Pattern.compile(userQuery);
+ Matcher m = mPattern.matcher(safeText);
+ int start = 0;
+ while (m.find(start)) {
+ spannable.setSpan(
+ new BackgroundColorSpan(ContextCompat.getColor(this, R.color.user_query_highlight)),
+ m.start(), m.end(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ start = m.end();
+ }
+ } catch (Exception e) {
+ Log.w(TAG, "getHighlightQueryResult: " + e.getMessage());
}
}
return spannable;
@@ -850,6 +1085,17 @@ public class NoteEditActivity extends Activity implements OnClickListener,
edit.setOnTextViewChangeListener(this);
edit.setIndex(index);
edit.setText(getHighlightQueryResult(item, mUserQuery));
+ // 清单模式:每项内容变化时实时更新字数
+ edit.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) {
+ updateCharCount();
+ }
+ });
return view;
}
@@ -863,6 +1109,7 @@ public class NoteEditActivity extends Activity implements OnClickListener,
} else {
mEditTextList.getChildAt(index).findViewById(R.id.cb_edit_item).setVisibility(View.GONE);
}
+ updateCharCount();
}
public void onCheckListModeChanged(int oldMode, int newMode) {
@@ -877,8 +1124,15 @@ public class NoteEditActivity extends Activity implements OnClickListener,
mEditTextList.setVisibility(View.GONE);
mNoteEditor.setVisibility(View.VISIBLE);
}
+ updateCharCount();
}
+ /**
+ * 从 UI 获取当前编辑内容并同步到 WorkingNote。
+ * 清单模式会按行拼接复选框符号(√/□)和文本。
+ *
+ * @return 清单模式下是否至少有一项被勾选
+ */
private boolean getWorkingText() {
boolean hasChecked = false;
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
@@ -912,6 +1166,10 @@ public class NoteEditActivity extends Activity implements OnClickListener,
return saved;
}
+ /**
+ * 将当前便签以快捷方式形式发送到桌面。
+ * 需便签已保存(有 noteId),否则提示错误。
+ */
private void sendToDesktop() {
if (!mWorkingNote.existInDatabase()) {
saveNote();
@@ -937,6 +1195,12 @@ public class NoteEditActivity extends Activity implements OnClickListener,
}
}
+ /**
+ * 创建桌面快捷方式时使用的标题:去除复选框符号,最多取前 10 个字符。
+ *
+ * @param content 便签正文
+ * @return 截断后的标题
+ */
private String makeShortcutIconTitle(String content) {
content = content.replace(TAG_CHECKED, "");
content = content.replace(TAG_UNCHECKED, "");
@@ -944,10 +1208,19 @@ public class NoteEditActivity extends Activity implements OnClickListener,
SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
+ /**
+ * 显示短时 Toast 提示。
+ */
private void showToast(int resId) {
showToast(resId, Toast.LENGTH_SHORT);
}
+ /**
+ * 显示指定时长的 Toast 提示。
+ *
+ * @param resId 字符串资源 ID
+ * @param duration Toast.LENGTH_SHORT 或 Toast.LENGTH_LONG
+ */
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
@@ -1087,31 +1360,51 @@ public class NoteEditActivity extends Activity implements OnClickListener,
// 2. 启动相机
private void takePhoto() {
Intent takePictureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
- // 确保有相机应用能处理这个 Intent
- if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
- try {
- // 使用 MediaUtils 创建临时文件 (高内聚:Activity 不关心文件怎么创建的)
- java.io.File photoFile = net.micode.notes.tool.MediaUtils.createImageFile(this);
- mCurrentPhotoPath = photoFile.getAbsolutePath();
-
- // 获取安全的 Content Uri (使用我们刚配置好的 FileProvider)
- android.net.Uri photoURI = androidx.core.content.FileProvider.getUriForFile(this,
- "net.micode.notes.fileprovider",
- photoFile);
-
- takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI);
- startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
- } catch (java.io.IOException ex) {
- Toast.makeText(this, "创建图片文件失败", Toast.LENGTH_SHORT).show();
- }
- } else {
+ android.content.pm.ResolveInfo resolveInfo = getPackageManager().resolveActivity(
+ takePictureIntent, android.content.pm.PackageManager.MATCH_DEFAULT_ONLY);
+ if (resolveInfo == null || resolveInfo.activityInfo == null) {
Toast.makeText(this, "未找到相机应用", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ try {
+ java.io.File photoFile = net.micode.notes.tool.MediaUtils.createImageFile(this);
+ mCurrentPhotoPath = photoFile.getCanonicalPath();
+
+ android.net.Uri photoURI = androidx.core.content.FileProvider.getUriForFile(this,
+ "net.micode.notes.fileprovider",
+ photoFile);
+
+ takePictureIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, photoURI);
+ takePictureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ takePictureIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ // Android 7.1+ 部分机型依赖 ClipData 传递 URI 权限
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
+ takePictureIntent.setClipData(android.content.ClipData.newUri(getContentResolver(), "", photoURI));
+ }
+ // 显式授予已解析的相机应用对该 URI 的写权限,提高兼容性
+ grantUriPermission(resolveInfo.activityInfo.packageName, photoURI,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ startActivityForResult(takePictureIntent, REQUEST_CODE_TAKE_PHOTO);
+ } catch (java.io.IOException ex) {
+ Log.e(TAG, "takePhoto createImageFile", ex);
+ Toast.makeText(this, "创建图片文件失败", Toast.LENGTH_SHORT).show();
+ } catch (IllegalArgumentException ex) {
+ Log.e(TAG, "takePhoto FileProvider URI", ex);
+ Toast.makeText(this, "无法创建拍照存储路径", Toast.LENGTH_SHORT).show();
+ } catch (Exception ex) {
+ Log.e(TAG, "takePhoto", ex);
+ Toast.makeText(this, "无法启动相机: " + ex.getMessage(), Toast.LENGTH_SHORT).show();
}
}
- // 3. 打开相册
+ // 3. 打开相册(使用 GET_CONTENT 以更好兼容截图、微信等来源的图片)
private void pickFromGallery() {
- Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
+ Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
+ intent.setType("image/*");
+ intent.addCategory(Intent.CATEGORY_OPENABLE);
+ // 确保选图器返回的 Uri 可被本应用读取(部分相册/文件管理器依赖此标志)
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(intent, REQUEST_CODE_OPEN_ALBUM);
}
@Override
@@ -1119,26 +1412,33 @@ public class NoteEditActivity extends Activity implements OnClickListener,
if (resultCode == RESULT_OK) {
switch (requestCode) {
case REQUEST_CODE_TAKE_PHOTO:
- if (mCurrentPhotoPath != null) {
- // === [修改] 调用 NoteEditText 的接口插入图片 ===
+ if (mCurrentPhotoPath != null && mNoteEditor != null) {
mNoteEditor.insertImage(mCurrentPhotoPath);
- // 【新增】立刻把含标签的新文本保存到 Model,防止 onResume 覆盖
- mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ mNoteEditor.setTextWithImages(mNoteEditor.getText().toString());
+ if (mWorkingNote != null) {
+ mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ }
}
+ mCurrentPhotoPath = null;
break;
case REQUEST_CODE_OPEN_ALBUM:
if (data != null && data.getData() != null) {
android.net.Uri selectedImage = data.getData();
+ try {
+ final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (takeFlags != 0) {
+ getContentResolver().takePersistableUriPermission(selectedImage, takeFlags);
+ }
+ } catch (SecurityException ignored) { }
String localPath = net.micode.notes.tool.MediaUtils.copyUriToInternalStorage(this, selectedImage);
if (localPath != null) {
mNoteEditor.insertImage(localPath);
- // 【新增】立刻保存到 Model
+ mNoteEditor.setTextWithImages(mNoteEditor.getText().toString());
mWorkingNote.setWorkingText(mNoteEditor.getText().toString());
+ } else {
+ Toast.makeText(this, R.string.error_image_copy_failed, Toast.LENGTH_SHORT).show();
}
-
-
- // ============================================
}
break;
diff --git a/src/src/net/micode/notes/ui/NoteEditText.java b/src/app/src/main/java/net/micode/notes/ui/NoteEditText.java
similarity index 58%
rename from src/src/net/micode/notes/ui/NoteEditText.java
rename to src/app/src/main/java/net/micode/notes/ui/NoteEditText.java
index 217f887..9af7e4b 100644
--- a/src/src/net/micode/notes/ui/NoteEditText.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteEditText.java
@@ -7,10 +7,15 @@ package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Rect;
+import android.graphics.Typeface;
import android.text.Layout;
import android.text.Selection;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
+import android.text.style.RelativeSizeSpan;
+import android.text.style.StyleSpan;
import android.text.style.URLSpan;
import android.util.AttributeSet;
import android.util.Log;
@@ -19,6 +24,7 @@ import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
import android.view.MotionEvent;
+import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
@@ -85,6 +91,13 @@ public class NoteEditText extends EditText {
private OnTextViewChangeListener mOnTextViewChangeListener;
+ /** 伪 Placeholder:当光标定位到该行且内容完全匹配时自动清空该行 */
+ private String mPlaceholderLine1;
+ private String mPlaceholderLine2;
+
+ /**
+ * 代码创建的构造器,默认索引为 0。
+ */
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
@@ -111,6 +124,13 @@ public class NoteEditText extends EditText {
e.printStackTrace();
}
}
+
+ /**
+ * 设置富文本内容:解析文本中的 <img src="路径"/> 标签,将图片渲染为 CenterImageSpan。
+ * 用于加载便签时还原内嵌图片,以及插入图片后刷新显示。
+ *
+ * @param text 包含 img 标签的原始文本
+ */
public void setTextWithImages(String text) {
// 1. 先设置纯文本
setText(text);
@@ -154,23 +174,78 @@ public class NoteEditText extends EditText {
e.printStackTrace();
}
}
+ // 强制重新测量与绘制,避免长文本/多图时高度未更新导致内容不可见
+ requestLayout();
+ post(new Runnable() {
+ @Override
+ public void run() {
+ invalidate();
+ }
+ });
}
+
+ /**
+ * 设置当前编辑框在清单列表中的索引,用于 Enter/Delete 回调时标识行号。
+ *
+ * @param index 从 0 开始的索引
+ */
public void setIndex(int index) {
mIndex = index;
}
+ /**
+ * 设置文本变更监听器,用于在行首删除、回车换行、焦点变化时通知父容器。
+ *
+ * @param listener 监听器实例,可为 null 表示不接收回调
+ */
public void setOnTextViewChangeListener(OnTextViewChangeListener listener) {
mOnTextViewChangeListener = listener;
}
+ /**
+ * 设置“占位行”文案:当用户光标定位到该行且内容完全等于 line1 或 line2 时,自动清空该行便于输入。
+ */
+ public void setPlaceholderLinesForClear(String line1, String line2) {
+ mPlaceholderLine1 = line1;
+ mPlaceholderLine2 = line2;
+ }
+
+ /**
+ * XML 布局 inflate 时使用的双参数构造器。
+ */
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
}
+ /**
+ * 带 defStyle 的完整构造器,用于主题或样式继承场景。
+ */
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
+ /**
+ * 确保 EditText 按全文排版并报告完整内容高度,避免“第 36 行起全部消失”。
+ * 原因:父布局传入 AT_MOST(一屏高) 时,DynamicLayout 只会排到该高度内的行数(约 35 行),
+ * 超出部分未进入 Layout,绘制时表现为整页消失。故在 AT_MOST 时先用 UNSPECIFIED 测一次,
+ * 让内部按全文排版,再以实际内容高度作为 measuredHeight。
+ */
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ int heightMode = View.MeasureSpec.getMode(heightSpec);
+ if (heightMode == View.MeasureSpec.AT_MOST) {
+ int unboundedHeight = View.MeasureSpec.makeMeasureSpec(0x3FFFFFFF, View.MeasureSpec.UNSPECIFIED);
+ super.onMeasure(widthSpec, unboundedHeight);
+ } else {
+ super.onMeasure(widthSpec, heightSpec);
+ }
+ Layout layout = getLayout();
+ if (layout != null) {
+ int contentHeight = layout.getHeight() + getCompoundPaddingTop() + getCompoundPaddingBottom();
+ setMeasuredDimension(getMeasuredWidth(), contentHeight);
+ }
+ }
+
/**
* 处理触摸事件
*
@@ -190,15 +265,50 @@ public class NoteEditText extends EditText {
y += getScrollY();
Layout layout = getLayout();
- int line = layout.getLineForVertical(y);
- int off = layout.getOffsetForHorizontal(line, x);
- Selection.setSelection(getText(), off);
+ if (layout != null) {
+ int line = layout.getLineForVertical(y);
+ int off = layout.getOffsetForHorizontal(line, x);
+ Selection.setSelection(getText(), off);
+ // 伪 Placeholder:若当前行内容为占位文案则清空该行
+ post(new Runnable() {
+ @Override
+ public void run() {
+ checkAndClearPlaceholderLine();
+ }
+ });
+ }
break;
}
return super.onTouchEvent(event);
}
+ /**
+ * 若光标所在行内容完全等于 mPlaceholderLine1 或 mPlaceholderLine2,则清空该行。
+ * 只删除占位文案,保留行尾换行符,避免下方时间行顶上来导致无法书写标题。
+ */
+ private void checkAndClearPlaceholderLine() {
+ if (TextUtils.isEmpty(mPlaceholderLine1) && TextUtils.isEmpty(mPlaceholderLine2)) return;
+ android.text.Editable text = getText();
+ if (text == null) return;
+ Layout layout = getLayout();
+ if (layout == null) return;
+ int selStart = getSelectionStart();
+ if (selStart < 0) return;
+ int line = layout.getLineForOffset(selStart);
+ int lineStart = layout.getLineStart(line);
+ int lineEnd = layout.getLineEnd(line);
+ String lineContent = text.subSequence(lineStart, lineEnd).toString().trim();
+ if (lineContent.equals(mPlaceholderLine1) || lineContent.equals(mPlaceholderLine2)) {
+ // 只替换占位文案,保留行尾换行符,避免下一行(如时间)顶上来
+ int replaceEnd = lineEnd;
+ if (lineEnd > lineStart && lineEnd <= text.length() && text.charAt(lineEnd - 1) == '\n') {
+ replaceEnd = lineEnd - 1;
+ }
+ text.replace(lineStart, replaceEnd, "");
+ }
+ }
+
/**
* 键盘按下事件拦截
* 记录关键状态,实际逻辑在 onKeyUp 中执行。
@@ -314,4 +424,88 @@ public class NoteEditText extends EditText {
}
super.onCreateContextMenu(menu);
}
+
+ // ================= 富文本工具栏:加粗、字号变大、字号变小(仅运行时效果,不持久化) =================
+
+ /**
+ * 对选中区域应用或取消加粗 (StyleSpan BOLD)。
+ * 若选区已全部加粗则取消加粗,否则应用加粗。
+ */
+ public void applyBoldToSelection() {
+ applySpanToSelection(new StyleSpan(Typeface.BOLD), StyleSpan.class, true);
+ }
+
+ /**
+ * 对选中区域应用相对字号 1.2 倍 (RelativeSizeSpan)。
+ */
+ public void applyFontSizeUpToSelection() {
+ applyRelativeSizeToSelection(1.2f);
+ }
+
+ /**
+ * 对选中区域应用相对字号 0.8 倍 (RelativeSizeSpan)。
+ */
+ public void applyFontSizeDownToSelection() {
+ applyRelativeSizeToSelection(0.8f);
+ }
+
+ /**
+ * 对选中区域应用或移除指定倍率的 RelativeSizeSpan。
+ * 若选区已有相同比例的 span 则移除(切换效果),否则添加。
+ *
+ * @param proportion 相对字号倍率,如 1.2f 表示 1.2 倍
+ */
+ private void applyRelativeSizeToSelection(float proportion) {
+ android.text.Editable text = getText();
+ if (text == null || !(text instanceof Spannable)) return;
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start < 0 || end < 0 || start == end) return;
+ if (start > end) { int t = start; start = end; end = t; }
+ Spannable sp = (Spannable) text;
+ RelativeSizeSpan[] existing = sp.getSpans(start, end, RelativeSizeSpan.class);
+ // 若选区已有相同比例的 RelativeSizeSpan 则移除,否则添加
+ boolean hasSame = false;
+ for (RelativeSizeSpan s : existing) {
+ if (Math.abs(s.getSizeChange() - proportion) < 0.01f) {
+ sp.removeSpan(s);
+ hasSame = true;
+ }
+ }
+ if (!hasSame) {
+ sp.setSpan(new RelativeSizeSpan(proportion), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
+
+ /**
+ * 对选中区域应用或移除指定类型的 Span。
+ * @param addSpan 要添加的 span 实例(仅用于“添加”分支)
+ * @param spanClass 要检测/移除的 span 类型
+ * @param toggle 若为 true:若选区已全部为该 span 则移除,否则添加
+ */
+ private void applySpanToSelection(Object addSpan, Class> spanClass, boolean toggle) {
+ android.text.Editable text = getText();
+ if (text == null || !(text instanceof Spannable)) return;
+ int start = getSelectionStart();
+ int end = getSelectionEnd();
+ if (start < 0 || end < 0 || start == end) return;
+ if (start > end) { int t = start; start = end; end = t; }
+ Spannable sp = (Spannable) text;
+ Object[] existing = sp.getSpans(start, end, spanClass);
+ boolean allCovered = true;
+ for (Object s : existing) {
+ int sStart = sp.getSpanStart(s);
+ int sEnd = sp.getSpanEnd(s);
+ if (sStart > start || sEnd < end) {
+ allCovered = false;
+ break;
+ }
+ }
+ if (existing.length == 0) allCovered = false;
+ if (toggle && allCovered && existing.length > 0) {
+ for (Object s : existing) sp.removeSpan(s);
+ } else {
+ sp.setSpan(addSpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/NoteItemData.java b/src/app/src/main/java/net/micode/notes/ui/NoteItemData.java
similarity index 96%
rename from src/src/net/micode/notes/ui/NoteItemData.java
rename to src/app/src/main/java/net/micode/notes/ui/NoteItemData.java
index 68087a8..662a55d 100644
--- a/src/src/net/micode/notes/ui/NoteItemData.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteItemData.java
@@ -39,6 +39,7 @@ public class NoteItemData {
NoteColumns.TYPE,
NoteColumns.WIDGET_ID,
NoteColumns.WIDGET_TYPE,
+ NoteColumns.PINNED,
};
// 列索引常量,对应 PROJECTION 数组
@@ -54,6 +55,7 @@ public class NoteItemData {
private static final int TYPE_COLUMN = 9;
private static final int WIDGET_ID_COLUMN = 10;
private static final int WIDGET_TYPE_COLUMN = 11;
+ private static final int PINNED_COLUMN = 12;
// 数据字段
private long mId;
@@ -68,6 +70,7 @@ public class NoteItemData {
private int mType; // 类型 (便签/文件夹)
private int mWidgetId;
private int mWidgetType;
+ private boolean mPinned; // 是否置顶
private String mName; // 联系人名字 (仅通话记录便签有效)
private String mPhoneNumber; // 电话号码 (仅通话记录便签有效)
@@ -99,6 +102,7 @@ public class NoteItemData {
mType = cursor.getInt(TYPE_COLUMN);
mWidgetId = cursor.getInt(WIDGET_ID_COLUMN);
mWidgetType = cursor.getInt(WIDGET_TYPE_COLUMN);
+ mPinned = (cursor.getInt(PINNED_COLUMN) > 0);
mPhoneNumber = "";
// 特殊逻辑:如果是通话记录文件夹下的便签,需要查找联系人名称
@@ -240,6 +244,10 @@ public class NoteItemData {
return (mAlertDate > 0);
}
+ public boolean isPinned() {
+ return mPinned;
+ }
+
public boolean isCallRecord() {
return (mParentId == Notes.ID_CALL_RECORD_FOLDER && !TextUtils.isEmpty(mPhoneNumber));
}
diff --git a/src/app/src/main/java/net/micode/notes/ui/NoteReminderNotifier.java b/src/app/src/main/java/net/micode/notes/ui/NoteReminderNotifier.java
new file mode 100644
index 0000000..0ff82b5
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteReminderNotifier.java
@@ -0,0 +1,80 @@
+package net.micode.notes.ui;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+import androidx.core.app.NotificationManagerCompat;
+import androidx.core.content.ContextCompat;
+
+import net.micode.notes.R;
+import net.micode.notes.tool.DataUtils;
+
+/**
+ * 便签时间提醒通知:到点后发出一条通知,点击可打开闹钟弹窗。
+ * 需在应用信息中开启「通知」权限(Android 13+)后才会显示。
+ */
+public final class NoteReminderNotifier {
+ private static final String TAG = "NoteReminderNotifier";
+ public static final String CHANNEL_ID = "note_reminder_channel";
+ private static final int SNIPPET_MAX_LEN = 40;
+
+ public static void show(Context context, long noteId, Uri noteUri) {
+ if (context == null || noteUri == null) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (ContextCompat.checkSelfPermission(context, android.Manifest.permission.POST_NOTIFICATIONS)
+ != android.content.pm.PackageManager.PERMISSION_GRANTED) {
+ Log.d(TAG, "POST_NOTIFICATIONS not granted");
+ return;
+ }
+ }
+ String snippet;
+ try {
+ snippet = DataUtils.getSnippetById(context.getContentResolver(), noteId);
+ if (snippet == null) snippet = "";
+ snippet = snippet.trim();
+ if (snippet.length() > SNIPPET_MAX_LEN) {
+ snippet = snippet.substring(0, SNIPPET_MAX_LEN) + "…";
+ }
+ } catch (Exception e) {
+ snippet = context.getString(R.string.note_reminder_default_text);
+ }
+ NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+ if (nm == null) return;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ context.getString(R.string.note_reminder_channel_name),
+ NotificationManager.IMPORTANCE_HIGH);
+ channel.setDescription(context.getString(R.string.note_reminder_channel_desc));
+ nm.createNotificationChannel(channel);
+ }
+ Intent openIntent = new Intent(context, AlarmAlertActivity.class);
+ openIntent.setData(noteUri);
+ openIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ PendingIntent pending = PendingIntent.getActivity(
+ context,
+ (int) (noteId % Integer.MAX_VALUE),
+ openIntent,
+ PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
+ .setSmallIcon(R.drawable.icon_app)
+ .setContentTitle(context.getString(R.string.note_reminder_title))
+ .setContentText(snippet.isEmpty() ? context.getString(R.string.note_reminder_default_text) : snippet)
+ .setPriority(NotificationCompat.PRIORITY_HIGH)
+ .setCategory(NotificationCompat.CATEGORY_REMINDER)
+ .setAutoCancel(true)
+ .setContentIntent(pending);
+ try {
+ NotificationManagerCompat.from(context).notify((int) (noteId % Integer.MAX_VALUE), builder.build());
+ } catch (SecurityException e) {
+ Log.e(TAG, "Cannot show notification: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/app/src/main/java/net/micode/notes/ui/NoteReminderReceiver.java b/src/app/src/main/java/net/micode/notes/ui/NoteReminderReceiver.java
new file mode 100644
index 0000000..5705e20
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/NoteReminderReceiver.java
@@ -0,0 +1,40 @@
+package net.micode.notes.ui;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * 便签时间提醒广播:到点后先发通知,再尝试打开闹钟弹窗。
+ * 设置提醒时使用本 Receiver,确保到点有通知(需开启通知权限)。
+ */
+public class NoteReminderReceiver extends BroadcastReceiver {
+ private static final String TAG = "NoteReminderReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Uri data = intent.getData();
+ if (data == null || data.getPathSegments() == null || data.getPathSegments().size() < 2) {
+ Log.e(TAG, "Alarm intent has no note data");
+ return;
+ }
+ long noteId;
+ try {
+ noteId = Long.parseLong(data.getPathSegments().get(1));
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Invalid note id in alarm intent", e);
+ return;
+ }
+ NoteReminderNotifier.show(context, noteId, data);
+ Intent alertIntent = new Intent(context, AlarmAlertActivity.class);
+ alertIntent.setData(data);
+ alertIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ try {
+ context.startActivity(alertIntent);
+ } catch (Exception e) {
+ Log.d(TAG, "Could not start AlarmAlertActivity: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/src/net/micode/notes/ui/NotesListActivity.java b/src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
similarity index 90%
rename from src/src/net/micode/notes/ui/NotesListActivity.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
index ffcdc0d..48456c7 100644
--- a/src/src/net/micode/notes/ui/NotesListActivity.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListActivity.java
@@ -27,22 +27,21 @@ import android.util.Log;
import android.view.ActionMode;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
-import android.view.Display;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MenuItem.OnMenuItemClickListener;
-import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnCreateContextMenuListener;
-import android.view.View.OnTouchListener;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
+
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.PopupMenu;
@@ -87,6 +86,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private static final int MENU_FOLDER_DELETE = 0;
private static final int MENU_FOLDER_VIEW = 1;
private static final int MENU_FOLDER_CHANGE_NAME = 2;
+ private static final int MENU_FOLDER_PIN = 3;
+ private static final int MENU_FOLDER_UNPIN = 4;
private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
@@ -104,11 +105,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
private NotesListAdapter mNotesListAdapter;
private ListView mNotesListView;
- private Button mAddNewNote;
-
- private boolean mDispatch;
- private int mOriginY;
- private int mDispatchY;
+ private FloatingActionButton mFab;
private TextView mTitleBar;
private long mCurrentFolderId; // 当前所在的文件夹 ID
@@ -162,11 +159,13 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
StringBuilder sb = new StringBuilder();
InputStream in = null;
+ InputStreamReader isr = null;
+ BufferedReader br = null;
try {
in = getResources().openRawResource(R.raw.introduction);
if (in != null) {
- InputStreamReader isr = new InputStreamReader(in);
- BufferedReader br = new BufferedReader(isr);
+ isr = new InputStreamReader(in);
+ br = new BufferedReader(isr);
char [] buf = new char[1024];
int len = 0;
while ((len = br.read(buf)) > 0) {
@@ -180,12 +179,14 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
e.printStackTrace();
return;
} finally {
- if(in != null) {
- try {
- in.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
+ if (br != null) {
+ try { br.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (isr != null) {
+ try { isr.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (in != null) {
+ try { in.close(); } catch (IOException e) { e.printStackTrace(); }
}
}
@@ -220,12 +221,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mNotesListView.setOnItemLongClickListener(this);
mNotesListAdapter = new NotesListAdapter(this);
mNotesListView.setAdapter(mNotesListAdapter);
- mAddNewNote = (Button) findViewById(R.id.btn_new_note);
- mAddNewNote.setOnClickListener(this);
- mAddNewNote.setOnTouchListener(new NewNoteOnTouchListener());
- mDispatch = false;
- mDispatchY = 0;
- mOriginY = 0;
+ mFab = (FloatingActionButton) findViewById(R.id.btn_fab_new_note);
+ if (mFab != null) {
+ mFab.setOnClickListener(this);
+ }
mTitleBar = (TextView) findViewById(R.id.tv_title_bar);
mState = ListEditState.NOTE_LIST;
mModeCallBack = new ModeCallback();
@@ -243,6 +242,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
getMenuInflater().inflate(R.menu.note_list_options, menu);
menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.pin_to_top).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unpin).setOnMenuItemClickListener(this);
mMoveMenu = menu.findItem(R.id.move);
if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
|| DataUtils.getUserFolderCount(mContentResolver) == 0) {
@@ -254,7 +255,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mActionMode = mode;
mNotesListAdapter.setChoiceMode(true);
mNotesListView.setLongClickable(false);
- mAddNewNote.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.GONE);
// 自定义 ActionMode 的标题栏,植入全选下拉菜单
View customView = LayoutInflater.from(NotesListActivity.this).inflate(
@@ -301,7 +302,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
public void onDestroyActionMode(ActionMode mode) {
mNotesListAdapter.setChoiceMode(false);
mNotesListView.setLongClickable(true);
- mAddNewNote.setVisibility(View.VISIBLE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
}
public void finishActionMode() {
@@ -341,6 +342,12 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case R.id.move:
startQueryDestinationFolders();
break;
+ case R.id.pin_to_top:
+ batchSetPinned(true);
+ break;
+ case R.id.unpin:
+ batchSetPinned(false);
+ break;
default:
return false;
}
@@ -348,63 +355,16 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
}
- /**
- * 新建便签按钮的触摸监听器
- *
- * 逻辑说明 (HACKME):
- * "New Note" 按钮有一部分背景是透明的。为了让用户点到透明区域时能穿透点击到下方的列表项,
- * 这里通过计算坐标,手动将事件 Dispatch 给 ListView。
- * 公式 y = -0.12x + 94 是根据 UI 设计图的形状拟合出来的。
- */
- private class NewNoteOnTouchListener implements OnTouchListener {
-
- public boolean onTouch(View v, MotionEvent event) {
- switch (event.getAction()) {
- case MotionEvent.ACTION_DOWN: {
- Display display = getWindowManager().getDefaultDisplay();
- int screenHeight = display.getHeight();
- int newNoteViewHeight = mAddNewNote.getHeight();
- int start = screenHeight - newNoteViewHeight;
- int eventY = start + (int) event.getY();
- if (mState == ListEditState.SUB_FOLDER) {
- eventY -= mTitleBar.getHeight();
- start -= mTitleBar.getHeight();
- }
- if (event.getY() < (event.getX() * (-0.12) + 94)) {
- View view = mNotesListView.getChildAt(mNotesListView.getChildCount() - 1
- - mNotesListView.getFooterViewsCount());
- if (view != null && view.getBottom() > start
- && (view.getTop() < (start + 94))) {
- mOriginY = (int) event.getY();
- mDispatchY = eventY;
- event.setLocation(event.getX(), mDispatchY);
- mDispatch = true;
- return mNotesListView.dispatchTouchEvent(event);
- }
- }
- break;
- }
- case MotionEvent.ACTION_MOVE: {
- if (mDispatch) {
- mDispatchY += (int) event.getY() - mOriginY;
- event.setLocation(event.getX(), mDispatchY);
- return mNotesListView.dispatchTouchEvent(event);
- }
- break;
- }
- default: {
- if (mDispatch) {
- event.setLocation(event.getX(), mDispatchY);
- mDispatch = false;
- return mNotesListView.dispatchTouchEvent(event);
- }
- break;
- }
- }
- return false;
+ private void batchSetPinned(boolean pinned) {
+ HashSet ids = mNotesListAdapter.getSelectedItemIds();
+ if (ids == null || ids.isEmpty()) return;
+ if (DataUtils.batchSetPinned(mContentResolver, ids, pinned)) {
+ Toast.makeText(this, pinned ? getString(R.string.toast_pinned) : getString(R.string.toast_unpinned),
+ Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ mModeCallBack.finishActionMode();
}
-
- };
+ }
/**
* 启动异步便签列表查询 (核心逻辑)
@@ -422,7 +382,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, new String[] {
String.valueOf(mCurrentFolderId)
- }, NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
+ }, NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
}
private final class BackgroundQueryHandler extends AsyncQueryHandler {
@@ -552,9 +512,10 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
startAsyncNotesListQuery(); // 刷新列表,查询子文件夹内容
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mState = ListEditState.CALL_RECORD_FOLDER;
- mAddNewNote.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.GONE);
} else {
mState = ListEditState.SUB_FOLDER;
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
}
if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
mTitleBar.setText(R.string.call_record_folder_name);
@@ -565,12 +526,8 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
}
public void onClick(View v) {
- switch (v.getId()) {
- case R.id.btn_new_note:
- createNewNote();
- break;
- default:
- break;
+ if (v.getId() == R.id.btn_fab_new_note) {
+ createNewNote();
}
}
@@ -677,7 +634,7 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case CALL_RECORD_FOLDER:
mCurrentFolderId = Notes.ID_ROOT_FOLDER;
mState = ListEditState.NOTE_LIST;
- mAddNewNote.setVisibility(View.VISIBLE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
mTitleBar.setVisibility(View.GONE);
startAsyncNotesListQuery();
break;
@@ -713,6 +670,11 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
if (mFocusNoteDataItem != null) {
menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
+ if (mFocusNoteDataItem.isPinned()) {
+ menu.add(0, MENU_FOLDER_UNPIN, 0, R.string.menu_unpin);
+ } else {
+ menu.add(0, MENU_FOLDER_PIN, 0, R.string.menu_pin_to_top);
+ }
menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
}
@@ -754,6 +716,26 @@ public class NotesListActivity extends Activity implements OnClickListener, OnIt
case MENU_FOLDER_CHANGE_NAME:
showCreateOrModifyFolderDialog(false);
break;
+ case MENU_FOLDER_PIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, true)) {
+ Toast.makeText(this, getString(R.string.toast_pinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
+ break;
+ case MENU_FOLDER_UNPIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, false)) {
+ Toast.makeText(this, getString(R.string.toast_unpinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
+ break;
default:
break;
}
diff --git a/src/src/net/micode/notes/ui/NotesListAdapter.java b/src/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java
similarity index 98%
rename from src/src/net/micode/notes/ui/NotesListAdapter.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java
index 77c854c..ba34e75 100644
--- a/src/src/net/micode/notes/ui/NotesListAdapter.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListAdapter.java
@@ -98,6 +98,11 @@ public class NotesListAdapter extends CursorAdapter {
notifyDataSetChanged(); // 刷新 UI 以显示勾选框变化
}
+ /**
+ * 判断当前是否处于批量多选模式。
+ *
+ * @return true 表示多选模式,列表项显示复选框
+ */
public boolean isInChoiceMode() {
return mChoiceMode;
}
diff --git a/src/app/src/main/java/net/micode/notes/ui/NotesListFragment.java b/src/app/src/main/java/net/micode/notes/ui/NotesListFragment.java
new file mode 100644
index 0000000..503853c
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListFragment.java
@@ -0,0 +1,1070 @@
+package net.micode.notes.ui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.appwidget.AppWidgetManager;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.media.projection.MediaProjectionManager;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.View.OnCreateContextMenuListener;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import com.google.android.material.appbar.MaterialToolbar;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import net.micode.notes.R;
+import net.micode.notes.data.Notes;
+import net.micode.notes.data.Notes.NoteColumns;
+import net.micode.notes.gtask.remote.GTaskSyncService;
+import net.micode.notes.model.WorkingNote;
+import net.micode.notes.tool.BackupUtils;
+import net.micode.notes.tool.DataUtils;
+import net.micode.notes.tool.FloatingService;
+import net.micode.notes.tool.PrivacySpaceManager;
+import net.micode.notes.tool.ResourceParser;
+import net.micode.notes.ui.NotesListAdapter.AppWidgetAttribute;
+import net.micode.notes.widget.NoteWidgetProvider_2x;
+import net.micode.notes.widget.NoteWidgetProvider_4x;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.HashSet;
+
+public class NotesListFragment extends Fragment implements OnClickListener, OnItemLongClickListener {
+ private static final int FOLDER_NOTE_LIST_QUERY_TOKEN = 0;
+ private static final int FOLDER_LIST_QUERY_TOKEN = 1;
+ private static final int MENU_FOLDER_DELETE = 0;
+ private static final int MENU_FOLDER_VIEW = 1;
+ private static final int MENU_FOLDER_CHANGE_NAME = 2;
+ private static final int MENU_FOLDER_PIN = 3;
+ private static final int MENU_FOLDER_UNPIN = 4;
+ private static final String PREFERENCE_ADD_INTRODUCTION = "net.micode.notes.introduction";
+
+ // === [新增] 灵感球相关常量 ===
+
+ // ==========================
+
+ private enum ListEditState {
+ NOTE_LIST, SUB_FOLDER, CALL_RECORD_FOLDER, PRIVACY_SPACE
+ }
+
+ private ListEditState mState;
+ private BackgroundQueryHandler mBackgroundQueryHandler;
+ private NotesListAdapter mNotesListAdapter;
+ private ListView mNotesListView;
+ private MaterialToolbar mToolbar;
+ private FloatingActionButton mFab;
+ private FloatingActionButton mFabBack;
+ private FloatingActionButton mFabExitPrivacy;
+ private TextView mTitleBar;
+ private long mCurrentFolderId;
+ private ContentResolver mContentResolver;
+ private ModeCallback mModeCallBack;
+ private static final String TAG = "NotesListFragment";
+ private NoteItemData mFocusNoteDataItem;
+
+ private static final String NORMAL_SELECTION = NoteColumns.PARENT_ID + "=?";
+ private static final String ROOT_FOLDER_SELECTION = "(" + NoteColumns.TYPE + "<>"
+ + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?)" + " OR ("
+ + NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND "
+ + NoteColumns.NOTES_COUNT + ">0)";
+
+ private final static int REQUEST_CODE_OPEN_NODE = 102;
+ private final static int REQUEST_CODE_NEW_NODE = 103;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ View view = inflater.inflate(R.layout.note_list, container, false);
+ setHasOptionsMenu(true); // 允许 Fragment 处理菜单
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ initResources(view);
+ setAppInfoFromRawRes();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ startAsyncNotesListQuery();
+ }
+
+ // === [修改] onActivityResult 处理所有请求 ===
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // 只保留原本便签编辑返回刷新的逻辑
+ if (resultCode == Activity.RESULT_OK
+ && (requestCode == REQUEST_CODE_OPEN_NODE || requestCode == REQUEST_CODE_NEW_NODE)) {
+ mNotesListAdapter.changeCursor(null);
+ } else {
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+ }
+
+ private void initResources(View view) {
+ mContentResolver = getActivity().getContentResolver();
+ mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
+ // 若进程存活且处于隐私空间(如配置变更后重建),恢复状态
+ if (PrivacySpaceManager.isInPrivacySpace()) {
+ mCurrentFolderId = Notes.ID_PRIVACY_FOLDER;
+ mState = ListEditState.PRIVACY_SPACE;
+ applyPrivacySpaceTheme(view);
+ } else {
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ }
+ mNotesListView = (ListView) view.findViewById(R.id.notes_list);
+ mNotesListView.addFooterView(LayoutInflater.from(getActivity()).inflate(R.layout.note_list_footer, null),
+ null, false);
+ mNotesListView.setOnItemClickListener(new OnListItemClickListener());
+ mNotesListView.setOnItemLongClickListener(this);
+ mNotesListAdapter = new NotesListAdapter(getActivity());
+ mNotesListView.setAdapter(mNotesListAdapter);
+
+ mToolbar = (MaterialToolbar) view.findViewById(R.id.toolbar_notes);
+ mFab = (FloatingActionButton) view.findViewById(R.id.btn_fab_new_note);
+ mFabBack = (FloatingActionButton) view.findViewById(R.id.btn_fab_back);
+ mFabExitPrivacy = (FloatingActionButton) view.findViewById(R.id.btn_fab_exit_privacy);
+ mTitleBar = (TextView) view.findViewById(R.id.tv_title_bar);
+
+ if (mFabBack != null) {
+ mFabBack.setOnClickListener(v -> performBackToRoot());
+ mFabBack.setVisibility(View.GONE);
+ }
+ if (mFabExitPrivacy != null) {
+ mFabExitPrivacy.setOnClickListener(v -> performExitPrivacySpace());
+ mFabExitPrivacy.setVisibility(PrivacySpaceManager.isInPrivacySpace() ? View.VISIBLE : View.GONE);
+ }
+ if (mToolbar != null) {
+ mToolbar.setNavigationOnClickListener(v -> {
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).openDrawer();
+ }
+ });
+ }
+ if (mFab != null) {
+ mFab.setOnClickListener(v -> createNewNote());
+ }
+
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).setNotesListFragment(this);
+ }
+ if (!PrivacySpaceManager.isInPrivacySpace()) {
+ mState = ListEditState.NOTE_LIST;
+ }
+ mModeCallBack = new ModeCallback();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).setNotesListFragment(null);
+ }
+ }
+
+ // === [新增] 检查并启动灵感球服务 ===
+
+
+ // 处理菜单创建
+ @Override
+ public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
+ menu.clear();
+ if (mState == ListEditState.PRIVACY_SPACE) {
+ inflater.inflate(R.menu.note_list_privacy, menu);
+ } else if (mState == ListEditState.NOTE_LIST) {
+ inflater.inflate(R.menu.note_list, menu);
+ menu.findItem(R.id.menu_sync).setTitle(
+ GTaskSyncService.isSyncing() ? R.string.menu_sync_cancel : R.string.menu_sync);
+ } else if (mState == ListEditState.SUB_FOLDER) {
+ inflater.inflate(R.menu.sub_folder, menu);
+ } else if (mState == ListEditState.CALL_RECORD_FOLDER) {
+ inflater.inflate(R.menu.call_record_folder, menu);
+ }
+ }
+
+ // 处理菜单点击
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ switch (item.getItemId()) {
+
+ case R.id.menu_new_folder: {
+ showCreateOrModifyFolderDialog(true);
+ break;
+ }
+ case R.id.menu_export_text: {
+ exportNoteToText();
+ break;
+ }
+ case R.id.menu_sync: {
+ if (isSyncMode()) {
+ if (TextUtils.equals(item.getTitle(), getString(R.string.menu_sync))) {
+ GTaskSyncService.startSync(getActivity());
+ } else {
+ GTaskSyncService.cancelSync(getActivity());
+ }
+ } else {
+ startPreferenceActivity();
+ }
+ break;
+ }
+ case R.id.menu_setting: {
+ startPreferenceActivity();
+ break;
+ }
+ case R.id.menu_new_note: {
+ createNewNote();
+ break;
+ }
+ case R.id.menu_search:
+ onSearchRequested();
+ break;
+ default:
+ break;
+ }
+ return true;
+ }
+
+ // 搜索功能适配
+ public boolean onSearchRequested() {
+ startActivity(new Intent(getActivity(), NotesListActivity.class).setAction(Intent.ACTION_SEARCH));
+ return true;
+ }
+
+ // 首次使用的介绍便签
+ private void setAppInfoFromRawRes() {
+ SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ if (!sp.getBoolean(PREFERENCE_ADD_INTRODUCTION, false)) {
+ StringBuilder sb = new StringBuilder();
+ InputStream in = null;
+ InputStreamReader isr = null;
+ BufferedReader br = null;
+ try {
+ in = getResources().openRawResource(R.raw.introduction);
+ if (in != null) {
+ isr = new InputStreamReader(in);
+ br = new BufferedReader(isr);
+ char [] buf = new char[1024];
+ int len = 0;
+ while ((len = br.read(buf)) > 0) {
+ sb.append(buf, 0, len);
+ }
+ } else {
+ Log.e(TAG, "Read introduction file error");
+ return;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ return;
+ } finally {
+ if (br != null) {
+ try { br.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (isr != null) {
+ try { isr.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ if (in != null) {
+ try { in.close(); } catch (IOException e) { e.printStackTrace(); }
+ }
+ }
+
+ WorkingNote note = WorkingNote.createEmptyNote(getActivity(), Notes.ID_ROOT_FOLDER,
+ AppWidgetManager.INVALID_APPWIDGET_ID, Notes.TYPE_WIDGET_INVALIDE,
+ ResourceParser.RED);
+ note.setWorkingText(sb.toString());
+ if (note.saveNote()) {
+ sp.edit().putBoolean(PREFERENCE_ADD_INTRODUCTION, true).commit();
+ } else {
+ Log.e(TAG, "Save introduction note error");
+ return;
+ }
+ }
+ }
+
+ // --- 以下为原有的辅助类和方法,适配了 Context ---
+
+
+
+ private class ModeCallback implements ListView.MultiChoiceModeListener, MenuItem.OnMenuItemClickListener {
+ private DropdownMenu mDropDownMenu;
+ private ActionMode mActionMode;
+ private MenuItem mMoveMenu;
+
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ getActivity().getMenuInflater().inflate(R.menu.note_list_options, menu);
+ menu.findItem(R.id.delete).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.pin_to_top).setOnMenuItemClickListener(this);
+ menu.findItem(R.id.unpin).setOnMenuItemClickListener(this);
+ mMoveMenu = menu.findItem(R.id.move);
+ if (mFocusNoteDataItem.getParentId() == Notes.ID_CALL_RECORD_FOLDER
+ || DataUtils.getUserFolderCount(mContentResolver) == 0) {
+ mMoveMenu.setVisible(false);
+ } else {
+ mMoveMenu.setVisible(true);
+ mMoveMenu.setOnMenuItemClickListener(this);
+ }
+ mActionMode = mode;
+ mNotesListAdapter.setChoiceMode(true);
+ mNotesListView.setLongClickable(false);
+ if (mFab != null) mFab.setVisibility(View.GONE);
+
+ View customView = LayoutInflater.from(getActivity()).inflate(
+ R.layout.note_list_dropdown_menu, null);
+ mode.setCustomView(customView);
+ mDropDownMenu = new DropdownMenu(getActivity(),
+ (Button) customView.findViewById(R.id.selection_menu),
+ R.menu.note_list_dropdown);
+ mDropDownMenu.setOnDropdownMenuItemClickListener(new PopupMenu.OnMenuItemClickListener(){
+ public boolean onMenuItemClick(MenuItem item) {
+ mNotesListAdapter.selectAll(!mNotesListAdapter.isAllSelected());
+ updateMenu();
+ return true;
+ }
+
+ });
+ return true;
+ }
+
+ private void updateMenu() {
+ int selectedCount = mNotesListAdapter.getSelectedCount();
+ String format = getResources().getString(R.string.menu_select_title, selectedCount);
+ mDropDownMenu.setTitle(format);
+ MenuItem item = mDropDownMenu.findItem(R.id.action_select_all);
+ if (item != null) {
+ if (mNotesListAdapter.isAllSelected()) {
+ item.setChecked(true);
+ item.setTitle(R.string.menu_deselect_all);
+ } else {
+ item.setChecked(false);
+ item.setTitle(R.string.menu_select_all);
+ }
+ }
+ }
+
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return false;
+ }
+
+ public void onDestroyActionMode(ActionMode mode) {
+ mNotesListAdapter.setChoiceMode(false);
+ mNotesListView.setLongClickable(true);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
+ }
+
+ public void finishActionMode() {
+ mActionMode.finish();
+ }
+
+ public void onItemCheckedStateChanged(ActionMode mode, int position, long id,
+ boolean checked) {
+ mNotesListAdapter.setCheckedItem(position, checked);
+ updateMenu();
+ }
+
+ public boolean onMenuItemClick(MenuItem item) {
+ if (mNotesListAdapter.getSelectedCount() == 0) {
+ Toast.makeText(getActivity(), getString(R.string.menu_select_none),
+ Toast.LENGTH_SHORT).show();
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.delete:
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage(getString(R.string.alert_message_delete_notes,
+ mNotesListAdapter.getSelectedCount()));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+ batchDelete();
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ break;
+ case R.id.move:
+ startQueryDestinationFolders();
+ break;
+ case R.id.pin_to_top:
+ batchSetPinned(true);
+ break;
+ case R.id.unpin:
+ batchSetPinned(false);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+ }
+
+ private void batchSetPinned(boolean pinned) {
+ HashSet ids = mNotesListAdapter.getSelectedItemIds();
+ if (ids == null || ids.isEmpty()) return;
+ if (DataUtils.batchSetPinned(mContentResolver, ids, pinned)) {
+ Toast.makeText(getActivity(), pinned ? getString(R.string.toast_pinned) : getString(R.string.toast_unpinned),
+ Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ mModeCallBack.finishActionMode();
+ }
+ }
+
+ private void startAsyncNotesListQuery() {
+ String selection;
+ String[] selectionArgs;
+ if (mState == ListEditState.PRIVACY_SPACE) {
+ selection = NORMAL_SELECTION;
+ selectionArgs = new String[] { String.valueOf(Notes.ID_PRIVACY_FOLDER) };
+ } else if (mCurrentFolderId == Notes.ID_ROOT_FOLDER) {
+ selection = ROOT_FOLDER_SELECTION;
+ selectionArgs = new String[] { String.valueOf(mCurrentFolderId) };
+ } else {
+ selection = NORMAL_SELECTION;
+ selectionArgs = new String[] { String.valueOf(mCurrentFolderId) };
+ }
+ mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
+ Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
+ NoteColumns.PINNED + " DESC," + NoteColumns.TYPE + " DESC," + NoteColumns.BG_COLOR_ID + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");
+ }
+
+ private final class BackgroundQueryHandler extends AsyncQueryHandler {
+ public BackgroundQueryHandler(ContentResolver contentResolver) {
+ super(contentResolver);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ switch (token) {
+ case FOLDER_NOTE_LIST_QUERY_TOKEN:
+ mNotesListAdapter.changeCursor(cursor);
+ break;
+ case FOLDER_LIST_QUERY_TOKEN:
+ if (cursor != null && cursor.getCount() > 0) {
+ showFolderListMenu(cursor);
+ } else {
+ Log.e(TAG, "Query folder failed");
+ }
+ break;
+ default:
+ return;
+ }
+ }
+ }
+
+ private void showFolderListMenu(Cursor cursor) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(R.string.menu_title_select_folder);
+ final FoldersListAdapter adapter = new FoldersListAdapter(getActivity(), cursor);
+ builder.setAdapter(adapter, new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ DataUtils.batchMoveToFolder(mContentResolver,
+ mNotesListAdapter.getSelectedItemIds(), adapter.getItemId(which));
+ Toast.makeText(
+ getActivity(),
+ getString(R.string.format_move_notes_to_folder,
+ mNotesListAdapter.getSelectedCount(),
+ adapter.getFolderName(getActivity(), which)),
+ Toast.LENGTH_SHORT).show();
+ mModeCallBack.finishActionMode();
+ }
+ });
+ builder.show();
+ }
+
+ private void createNewNote() {
+ Intent intent = new Intent(getActivity(), NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_INSERT_OR_EDIT);
+ intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, mCurrentFolderId);
+ this.startActivityForResult(intent, REQUEST_CODE_NEW_NODE);
+ }
+
+ private void batchDelete() {
+ new AsyncTask>() {
+ protected HashSet doInBackground(Void... unused) {
+ HashSet widgets = mNotesListAdapter.getSelectedWidget();
+ if (!isSyncMode()) {
+ if (DataUtils.batchDeleteNotes(mContentResolver, mNotesListAdapter
+ .getSelectedItemIds())) {
+ } else {
+ Log.e(TAG, "Delete notes error, should not happens");
+ }
+ } else {
+ if (!DataUtils.batchMoveToFolder(mContentResolver, mNotesListAdapter
+ .getSelectedItemIds(), Notes.ID_TRASH_FOLER)) {
+ Log.e(TAG, "Move notes to trash folder error, should not happens");
+ }
+ }
+ return widgets;
+ }
+
+ @Override
+ protected void onPostExecute(HashSet widgets) {
+ if (widgets != null) {
+ for (AppWidgetAttribute widget : widgets) {
+ if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
+ updateWidget(widget.widgetId, widget.widgetType);
+ }
+ }
+ }
+ mModeCallBack.finishActionMode();
+ }
+ }.execute();
+ }
+
+ private void deleteFolder(long folderId) {
+ if (folderId == Notes.ID_ROOT_FOLDER) {
+ Log.e(TAG, "Wrong folder id, should not happen " + folderId);
+ return;
+ }
+
+ HashSet ids = new HashSet();
+ ids.add(folderId);
+ HashSet widgets = DataUtils.getFolderNoteWidget(mContentResolver,
+ folderId);
+ if (!isSyncMode()) {
+ DataUtils.batchDeleteNotes(mContentResolver, ids);
+ } else {
+ DataUtils.batchMoveToFolder(mContentResolver, ids, Notes.ID_TRASH_FOLER);
+ }
+ if (widgets != null) {
+ for (AppWidgetAttribute widget : widgets) {
+ if (widget.widgetId != AppWidgetManager.INVALID_APPWIDGET_ID
+ && widget.widgetType != Notes.TYPE_WIDGET_INVALIDE) {
+ updateWidget(widget.widgetId, widget.widgetType);
+ }
+ }
+ }
+ }
+
+ private void openNode(NoteItemData data) {
+ Intent intent = new Intent(getActivity(), NoteEditActivity.class);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.putExtra(Intent.EXTRA_UID, data.getId());
+ this.startActivityForResult(intent, REQUEST_CODE_OPEN_NODE);
+ }
+
+ private void openFolder(NoteItemData data) {
+ mCurrentFolderId = data.getId();
+ startAsyncNotesListQuery();
+ if (data.getId() == Notes.ID_CALL_RECORD_FOLDER) {
+ mState = ListEditState.CALL_RECORD_FOLDER;
+ if (mFab != null) mFab.setVisibility(View.GONE);
+ } else {
+ mState = ListEditState.SUB_FOLDER;
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
+ }
+ if (mFabBack != null) mFabBack.setVisibility(View.VISIBLE);
+ if (mToolbar != null) {
+ mToolbar.setTitle(data.getId() == Notes.ID_CALL_RECORD_FOLDER
+ ? getString(R.string.call_record_folder_name) : data.getSnippet());
+ }
+ if (mTitleBar != null) {
+ mTitleBar.setText(data.getId() == Notes.ID_CALL_RECORD_FOLDER
+ ? getString(R.string.call_record_folder_name) : data.getSnippet());
+ mTitleBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ public void onClick(View v) {
+ if (v.getId() == R.id.btn_fab_new_note) {
+ createNewNote();
+ }
+ }
+
+ private void showSoftInput() {
+ InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (inputMethodManager != null) {
+ inputMethodManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0);
+ }
+ }
+
+ private void hideSoftInput(View view) {
+ InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
+ inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+
+ private void showCreateOrModifyFolderDialog(final boolean create) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ View view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_edit_text, null);
+ final EditText etName = (EditText) view.findViewById(R.id.et_foler_name);
+ showSoftInput();
+ if (!create) {
+ if (mFocusNoteDataItem != null) {
+ etName.setText(mFocusNoteDataItem.getSnippet());
+ builder.setTitle(getString(R.string.menu_folder_change_name));
+ } else {
+ Log.e(TAG, "The long click data item is null");
+ return;
+ }
+ } else {
+ etName.setText("");
+ builder.setTitle(this.getString(R.string.menu_create_folder));
+ }
+
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ hideSoftInput(etName);
+ }
+ });
+
+ final AlertDialog dialog = builder.setView(view).show();
+ final Button positive = (Button)dialog.findViewById(android.R.id.button1);
+ positive.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ hideSoftInput(etName);
+ String name = etName.getText().toString();
+ if (DataUtils.checkVisibleFolderName(mContentResolver, name)) {
+ Toast.makeText(getActivity(), getString(R.string.folder_exist, name),
+ Toast.LENGTH_LONG).show();
+ etName.setSelection(0, etName.length());
+ return;
+ }
+ if (!create) {
+ if (!TextUtils.isEmpty(name)) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SNIPPET, name);
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ values.put(NoteColumns.LOCAL_MODIFIED, 1);
+ mContentResolver.update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID
+ + "=?", new String[] {
+ String.valueOf(mFocusNoteDataItem.getId())
+ });
+ }
+ } else if (!TextUtils.isEmpty(name)) {
+ ContentValues values = new ContentValues();
+ values.put(NoteColumns.SNIPPET, name);
+ values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
+ mContentResolver.insert(Notes.CONTENT_NOTE_URI, values);
+ }
+ dialog.dismiss();
+ }
+ });
+
+ if (TextUtils.isEmpty(etName.getText())) {
+ positive.setEnabled(false);
+ }
+ etName.addTextChangedListener(new TextWatcher() {
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ if (TextUtils.isEmpty(etName.getText())) {
+ positive.setEnabled(false);
+ } else {
+ positive.setEnabled(true);
+ }
+ }
+ public void afterTextChanged(Editable s) { }
+ });
+ }
+
+ /** 由 MainActivity 在进入隐私空间时调用 */
+ public void onEnterPrivacySpace() {
+ mCurrentFolderId = Notes.ID_PRIVACY_FOLDER;
+ mState = ListEditState.PRIVACY_SPACE;
+ if (getView() != null) {
+ applyPrivacySpaceTheme(getView());
+ }
+ if (mToolbar != null) {
+ mToolbar.setTitle(R.string.privacy_space_title);
+ }
+ if (mTitleBar != null) {
+ mTitleBar.setVisibility(View.GONE);
+ }
+ if (mFabExitPrivacy != null) {
+ mFabExitPrivacy.setVisibility(View.VISIBLE);
+ }
+ startAsyncNotesListQuery();
+ }
+
+ /** 主动退出隐私空间 */
+ private void performExitPrivacySpace() {
+ PrivacySpaceManager.exitPrivacySpace();
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ if (getView() != null) {
+ restoreNormalTheme(getView());
+ }
+ if (mToolbar != null) {
+ mToolbar.setTitle(R.string.notes_tab_title);
+ }
+ if (mFabExitPrivacy != null) {
+ mFabExitPrivacy.setVisibility(View.GONE);
+ }
+ startAsyncNotesListQuery();
+ if (getActivity() instanceof MainActivity) {
+ ((MainActivity) getActivity()).exitPrivacySpace();
+ }
+ Toast.makeText(getActivity(), R.string.privacy_space_exit, Toast.LENGTH_SHORT).show();
+ }
+
+ private void restoreNormalTheme(View rootView) {
+ if (rootView == null) return;
+ int bgColor = ContextCompat.getColor(requireContext(), R.color.background_light);
+ rootView.setBackgroundColor(bgColor);
+ View listView = rootView.findViewById(R.id.notes_list);
+ if (listView != null) {
+ listView.setBackgroundColor(bgColor);
+ }
+ }
+
+ private void applyPrivacySpaceTheme(View rootView) {
+ if (rootView == null) return;
+ int darkColor = ContextCompat.getColor(requireContext(), R.color.privacy_space_background);
+ rootView.setBackgroundColor(darkColor);
+ View listView = rootView.findViewById(R.id.notes_list);
+ if (listView != null) {
+ listView.setBackgroundColor(darkColor);
+ }
+ }
+
+ /**
+ * 返回上一级便签列表(与 onBackPressed 中子文件夹/通话记录逻辑一致)。
+ * 仅在子文件夹或通话记录文件夹内显示返回按钮时调用。
+ */
+ private void performBackToRoot() {
+ switch (mState) {
+ case SUB_FOLDER:
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ if (mFabBack != null) mFabBack.setVisibility(View.GONE);
+ startAsyncNotesListQuery();
+ if (mToolbar != null) mToolbar.setTitle(R.string.notes_tab_title);
+ if (mTitleBar != null) mTitleBar.setVisibility(View.GONE);
+ break;
+ case CALL_RECORD_FOLDER:
+ mCurrentFolderId = Notes.ID_ROOT_FOLDER;
+ mState = ListEditState.NOTE_LIST;
+ if (mFabBack != null) mFabBack.setVisibility(View.GONE);
+ if (mFab != null) mFab.setVisibility(View.VISIBLE);
+ startAsyncNotesListQuery();
+ if (mToolbar != null) mToolbar.setTitle(R.string.notes_tab_title);
+ if (mTitleBar != null) mTitleBar.setVisibility(View.GONE);
+ break;
+ default:
+ break;
+ }
+ }
+
+ public boolean onBackPressed() {
+ switch (mState) {
+ case PRIVACY_SPACE:
+ return false;
+ case SUB_FOLDER:
+ performBackToRoot();
+ return true;
+ case CALL_RECORD_FOLDER:
+ performBackToRoot();
+ return true;
+ case NOTE_LIST:
+ default:
+ return false;
+ }
+ }
+
+ /** 侧边栏「新建便签」入口 */
+ public void createNewNoteFromDrawer() {
+ createNewNote();
+ }
+
+ /** 侧边栏「新建文件夹」入口 */
+ public void showCreateOrModifyFolderDialogFromDrawer(boolean create) {
+ mFocusNoteDataItem = null;
+ showCreateOrModifyFolderDialog(create);
+ }
+
+ /** 侧边栏「导出」入口 */
+ public void exportNoteToTextFromDrawer() {
+ exportNoteToText();
+ }
+
+ /** 侧边栏「同步」入口 */
+ public void handleSyncFromDrawer() {
+ if (isSyncMode()) {
+ if (GTaskSyncService.isSyncing()) {
+ GTaskSyncService.cancelSync(getActivity());
+ } else {
+ GTaskSyncService.startSync(getActivity());
+ }
+ } else {
+ startPreferenceActivity();
+ }
+ }
+
+ private void updateWidget(int appWidgetId, int appWidgetType) {
+ Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
+ if (appWidgetType == Notes.TYPE_WIDGET_2X) {
+ intent.setClass(getActivity(), NoteWidgetProvider_2x.class);
+ } else if (appWidgetType == Notes.TYPE_WIDGET_4X) {
+ intent.setClass(getActivity(), NoteWidgetProvider_4x.class);
+ } else {
+ Log.e(TAG, "Unspported widget type");
+ return;
+ }
+
+ intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, new int[] {
+ appWidgetId
+ });
+
+ getActivity().sendBroadcast(intent);
+ getActivity().setResult(Activity.RESULT_OK, intent);
+ }
+
+ private final OnCreateContextMenuListener mFolderOnCreateContextMenuListener = new OnCreateContextMenuListener() {
+ public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
+ if (mFocusNoteDataItem != null) {
+ menu.setHeaderTitle(mFocusNoteDataItem.getSnippet());
+ menu.add(0, MENU_FOLDER_VIEW, 0, R.string.menu_folder_view);
+ if (mFocusNoteDataItem.isPinned()) {
+ menu.add(0, MENU_FOLDER_UNPIN, 0, R.string.menu_unpin);
+ } else {
+ menu.add(0, MENU_FOLDER_PIN, 0, R.string.menu_pin_to_top);
+ }
+ menu.add(0, MENU_FOLDER_DELETE, 0, R.string.menu_folder_delete);
+ menu.add(0, MENU_FOLDER_CHANGE_NAME, 0, R.string.menu_folder_change_name);
+ }
+ }
+ };
+
+ @Override
+ public boolean onContextItemSelected(@NonNull MenuItem item) {
+ if (mFocusNoteDataItem == null) {
+ Log.e(TAG, "The long click data item is null");
+ return false;
+ }
+ switch (item.getItemId()) {
+ case MENU_FOLDER_VIEW:
+ openFolder(mFocusNoteDataItem);
+ break;
+ case MENU_FOLDER_DELETE:
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getString(R.string.alert_title_delete));
+ builder.setIcon(android.R.drawable.ic_dialog_alert);
+ builder.setMessage(getString(R.string.alert_message_delete_folder));
+ builder.setPositiveButton(android.R.string.ok,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ deleteFolder(mFocusNoteDataItem.getId());
+ }
+ });
+ builder.setNegativeButton(android.R.string.cancel, null);
+ builder.show();
+ break;
+ case MENU_FOLDER_CHANGE_NAME:
+ showCreateOrModifyFolderDialog(false);
+ break;
+ case MENU_FOLDER_PIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, true)) {
+ Toast.makeText(getActivity(), getString(R.string.toast_pinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
+ break;
+ case MENU_FOLDER_UNPIN:
+ if (mFocusNoteDataItem != null && mFocusNoteDataItem.getId() > 0) {
+ HashSet ids = new HashSet();
+ ids.add(mFocusNoteDataItem.getId());
+ if (DataUtils.batchSetPinned(mContentResolver, ids, false)) {
+ Toast.makeText(getActivity(), getString(R.string.toast_unpinned), Toast.LENGTH_SHORT).show();
+ startAsyncNotesListQuery();
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return true;
+ }
+
+ private void exportNoteToText() {
+ final BackupUtils backup = BackupUtils.getInstance(getActivity());
+ new AsyncTask() {
+
+ @Override
+ protected Integer doInBackground(Void... unused) {
+ return backup.exportToText();
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (result == BackupUtils.STATE_SD_CARD_UNMOUONTED) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity()
+ .getString(R.string.failed_sdcard_export));
+ builder.setMessage(getActivity()
+ .getString(R.string.error_sdcard_unmounted));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ } else if (result == BackupUtils.STATE_SUCCESS) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity()
+ .getString(R.string.success_sdcard_export));
+ builder.setMessage(getActivity().getString(
+ R.string.format_exported_file_location, backup
+ .getExportedTextFileName(), backup.getExportedTextFileDir()));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ } else if (result == BackupUtils.STATE_SYSTEM_ERROR) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle(getActivity()
+ .getString(R.string.failed_sdcard_export));
+ builder.setMessage(getActivity()
+ .getString(R.string.error_sdcard_export));
+ builder.setPositiveButton(android.R.string.ok, null);
+ builder.show();
+ }
+ }
+
+ }.execute();
+ }
+
+ private boolean isSyncMode() {
+ return NotesPreferenceActivity.getSyncAccountName(getActivity()).trim().length() > 0;
+ }
+
+ private void startPreferenceActivity() {
+ Intent intent = new Intent(getActivity(), NotesPreferenceActivity.class);
+ startActivity(intent);
+ }
+
+ private class OnListItemClickListener implements AdapterView.OnItemClickListener {
+
+ public void onItemClick(AdapterView> parent, View view, int position, long id) {
+ if (view instanceof NotesListItem) {
+ NoteItemData item = ((NotesListItem) view).getItemData();
+ if (mNotesListAdapter.isInChoiceMode()) {
+ if (item.getType() == Notes.TYPE_NOTE) {
+ position = position - mNotesListView.getHeaderViewsCount();
+ mModeCallBack.onItemCheckedStateChanged(null, position, id,
+ !mNotesListAdapter.isSelectedItem(position));
+ }
+ return;
+ }
+
+ switch (mState) {
+ case NOTE_LIST:
+ if (item.getType() == Notes.TYPE_FOLDER
+ || item.getType() == Notes.TYPE_SYSTEM) {
+ openFolder(item);
+ } else if (item.getType() == Notes.TYPE_NOTE) {
+ openNode(item);
+ } else {
+ Log.e(TAG, "Wrong note type in NOTE_LIST");
+ }
+ break;
+ case SUB_FOLDER:
+ case CALL_RECORD_FOLDER:
+ if (item.getType() == Notes.TYPE_NOTE) {
+ openNode(item);
+ } else {
+ Log.e(TAG, "Wrong note type in SUB_FOLDER");
+ }
+ break;
+ case PRIVACY_SPACE:
+ if (item.getType() == Notes.TYPE_NOTE) {
+ openNode(item);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ }
+
+ public boolean onItemLongClick(AdapterView> parent, View view, int position, long id) {
+ if (view instanceof NotesListItem) {
+ mFocusNoteDataItem = ((NotesListItem) view).getItemData();
+ if (mFocusNoteDataItem.getType() == Notes.TYPE_NOTE && !mNotesListAdapter.isInChoiceMode()) {
+ if (mNotesListView.startActionMode(mModeCallBack) != null) {
+ mModeCallBack.onItemCheckedStateChanged(null, position, id, true);
+ mNotesListView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ } else {
+ Log.e(TAG, "startActionMode fails");
+ }
+ } else if (mFocusNoteDataItem.getType() == Notes.TYPE_FOLDER) {
+ mNotesListView.setOnCreateContextMenuListener(mFolderOnCreateContextMenuListener);
+ }
+ }
+ return false;
+ }
+ private void startQueryDestinationFolders() {
+ String selection = NoteColumns.TYPE + "=? AND " + NoteColumns.PARENT_ID + "<>? AND " + NoteColumns.ID + "<>?";
+ selection = (mState == ListEditState.NOTE_LIST) ? selection :
+ "(" + selection + ") OR (" + NoteColumns.ID + "=" + Notes.ID_ROOT_FOLDER + ")";
+
+ mBackgroundQueryHandler.startQuery(FOLDER_LIST_QUERY_TOKEN,
+ null,
+ Notes.CONTENT_NOTE_URI,
+ FoldersListAdapter.PROJECTION,
+ selection,
+ new String[] {
+ String.valueOf(Notes.TYPE_FOLDER),
+ String.valueOf(Notes.ID_TRASH_FOLER),
+ String.valueOf(mCurrentFolderId)
+ },
+ NoteColumns.MODIFIED_DATE + " DESC");
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/src/src/net/micode/notes/ui/NotesListItem.java b/src/app/src/main/java/net/micode/notes/ui/NotesListItem.java
similarity index 88%
rename from src/src/net/micode/notes/ui/NotesListItem.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesListItem.java
index 84b1571..8cc504b 100644
--- a/src/src/net/micode/notes/ui/NotesListItem.java
+++ b/src/app/src/main/java/net/micode/notes/ui/NotesListItem.java
@@ -8,6 +8,8 @@ package net.micode.notes.ui;
import android.content.Context;
import android.text.format.DateUtils;
import android.view.View;
+
+import androidx.core.content.ContextCompat;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.LinearLayout;
@@ -17,6 +19,7 @@ import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser.NoteItemBgResources;
+import net.micode.notes.tool.ResourceParser;
/**
* 便签列表项视图 (UI Component)
@@ -34,6 +37,7 @@ public class NotesListItem extends LinearLayout {
private TextView mCallName; // 联系人名称 (仅通话记录便签显示)
private NoteItemData mItemData;
private CheckBox mCheckBox; // 批量选择模式下的复选框
+ private View mPinnedBar; // 置顶时左侧加深色条
public NotesListItem(Context context) {
super(context);
@@ -44,6 +48,7 @@ public class NotesListItem extends LinearLayout {
mTime = (TextView) findViewById(R.id.tv_time);
mCallName = (TextView) findViewById(R.id.tv_name);
mCheckBox = (CheckBox) findViewById(android.R.id.checkbox);
+ mPinnedBar = findViewById(R.id.note_pinned_bar);
}
/**
@@ -116,6 +121,17 @@ public class NotesListItem extends LinearLayout {
// 设置相对时间显示 (例如 "刚刚", "昨天")
mTime.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate()));
+ // 置顶时显示左侧加深色条
+ if (data.isPinned() && mPinnedBar != null) {
+ mPinnedBar.setVisibility(View.VISIBLE);
+ int colorResId = (data.getType() == Notes.TYPE_FOLDER || data.getType() == Notes.TYPE_SYSTEM)
+ ? ResourceParser.NoteItemBgResources.getPinnedBarColorRes(ResourceParser.YELLOW)
+ : ResourceParser.NoteItemBgResources.getPinnedBarColorRes(data.getBgColorId());
+ mPinnedBar.setBackgroundColor(ContextCompat.getColor(context, colorResId));
+ } else if (mPinnedBar != null) {
+ mPinnedBar.setVisibility(View.GONE);
+ }
+
// 设置动态背景
setBackground(data);
}
diff --git a/src/src/net/micode/notes/ui/NotesPreferenceActivity.java b/src/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
similarity index 100%
rename from src/src/net/micode/notes/ui/NotesPreferenceActivity.java
rename to src/app/src/main/java/net/micode/notes/ui/NotesPreferenceActivity.java
diff --git a/src/app/src/main/java/net/micode/notes/ui/SplashActivity.java b/src/app/src/main/java/net/micode/notes/ui/SplashActivity.java
new file mode 100644
index 0000000..8c35f3a
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/SplashActivity.java
@@ -0,0 +1,168 @@
+package net.micode.notes.ui;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.content.Intent;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.view.View;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import net.micode.notes.R;
+
+/**
+ * 开场动画页
+ * 简约高级感:深色背景 + Logo 缩放淡入 + 标题淡入 + 细线点缀,短暂停留后淡出并进入主界面。
+ * 已做防闪退与 ANR:空指针检查、防重复跳转、安全超时、使用 post 替代 GlobalLayoutListener。
+ */
+public class SplashActivity extends AppCompatActivity {
+
+ private static final int PHASE_ENTER_MS = 900;
+ private static final int HOLD_MS = 1100;
+ private static final int PHASE_EXIT_MS = 450;
+ /** 最大等待时间,超时则直接进入主界面,防止卡死 */
+ private static final int SAFETY_TIMEOUT_MS = 4000;
+
+ private View mContentRoot;
+ private ImageView mLogo;
+ private TextView mTitle;
+ private View mAccent;
+
+ private final Handler mHandler = new Handler(Looper.getMainLooper());
+ private boolean mEnterAnimationStarted;
+ private boolean mGoMainCalled;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_splash);
+
+ mContentRoot = findViewById(R.id.splash_content);
+ mLogo = findViewById(R.id.splash_logo);
+ mTitle = findViewById(R.id.splash_title);
+ mAccent = findViewById(R.id.splash_accent);
+
+ if (mContentRoot == null || mLogo == null || mTitle == null || mAccent == null) {
+ goToMain();
+ return;
+ }
+
+ if (mLogo != null) {
+ mLogo.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
+ }
+
+ // 使用 post 在下一帧启动动画,避免与 GlobalLayoutListener 的 API 差异导致不触发
+ mContentRoot.post(this::startEnterAnimation);
+
+ // 安全超时:若动画或跳转异常,最多等待 SAFETY_TIMEOUT_MS 后强制进入主界面
+ mHandler.postDelayed(this::goToMain, SAFETY_TIMEOUT_MS);
+ }
+
+ private void startEnterAnimation() {
+ if (mEnterAnimationStarted || isFinishing()) return;
+ if (mLogo == null || mTitle == null || mAccent == null) {
+ goToMain();
+ return;
+ }
+ mEnterAnimationStarted = true;
+
+ // 1. Logo:缩放 + 淡入
+ mLogo.setScaleX(0.82f);
+ mLogo.setScaleY(0.82f);
+ mLogo.setAlpha(0f);
+
+ ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLogo, View.SCALE_X, 0.82f, 1f);
+ ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLogo, View.SCALE_Y, 0.82f, 1f);
+ ObjectAnimator alphaLogo = ObjectAnimator.ofFloat(mLogo, View.ALPHA, 0f, 1f);
+ scaleX.setDuration(520);
+ scaleY.setDuration(520);
+ alphaLogo.setDuration(420);
+ AccelerateDecelerateInterpolator interpolator = new AccelerateDecelerateInterpolator();
+ scaleX.setInterpolator(interpolator);
+ scaleY.setInterpolator(interpolator);
+ alphaLogo.setInterpolator(interpolator);
+ scaleX.start();
+ scaleY.start();
+ alphaLogo.start();
+
+ // 2. 标题:延迟淡入 + 上移
+ mTitle.setAlpha(0f);
+ mTitle.setTranslationY(12f);
+ mTitle.animate()
+ .alpha(1f)
+ .translationY(0f)
+ .setStartDelay(280)
+ .setDuration(400)
+ .setInterpolator(interpolator)
+ .start();
+
+ // 3. 底部细线:淡入
+ mAccent.setAlpha(0f);
+ mAccent.animate()
+ .alpha(1f)
+ .setStartDelay(460)
+ .setDuration(320)
+ .setInterpolator(new LinearInterpolator())
+ .start();
+
+ // 4. 停留后退场并跳转
+ mHandler.postDelayed(this::startExitAnimation, PHASE_ENTER_MS + HOLD_MS);
+ }
+
+ private void startExitAnimation() {
+ if (isFinishing() || mGoMainCalled) return;
+ if (mContentRoot == null) {
+ goToMain();
+ return;
+ }
+ mContentRoot.animate()
+ .alpha(0f)
+ .setDuration(PHASE_EXIT_MS)
+ .setInterpolator(new AccelerateDecelerateInterpolator())
+ .setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (mContentRoot != null) {
+ mContentRoot.animate().setListener(null);
+ }
+ goToMain();
+ }
+ })
+ .start();
+ }
+
+ private void goToMain() {
+ if (mGoMainCalled) return;
+ mGoMainCalled = true;
+ mHandler.removeCallbacksAndMessages(null);
+
+ if (isFinishing()) return;
+
+ try {
+ startActivity(new Intent(this, MainActivity.class));
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ } catch (Exception e) {
+ overridePendingTransition(0, 0);
+ }
+ finish();
+ }
+
+ @Override
+ protected void onDestroy() {
+ mHandler.removeCallbacksAndMessages(null);
+ mContentRoot = null;
+ mLogo = null;
+ mTitle = null;
+ mAccent = null;
+ super.onDestroy();
+ }
+}
diff --git a/src/app/src/main/java/net/micode/notes/ui/TodoEditActivity.java b/src/app/src/main/java/net/micode/notes/ui/TodoEditActivity.java
new file mode 100644
index 0000000..4757b93
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/TodoEditActivity.java
@@ -0,0 +1,215 @@
+package net.micode.notes.ui;
+
+/**
+ * 待办编辑/新建页面
+ *
+ * 支持新增或编辑待办事项,可设置时间提醒(DatePicker + TimePicker)或地点提醒(Geofence)。
+ * 保存时调用 TodoReminderManager 注册 AlarmManager/Geofencing 提醒。
+ *
+ *
+ * @see TodoItem
+ * @see TodoReminderManager
+ * @see TodoDao
+ */
+import android.Manifest;
+import android.app.DatePickerDialog;
+import android.app.TimePickerDialog;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.widget.Button;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+
+import android.widget.EditText;
+
+import net.micode.notes.R;
+import net.micode.notes.data.TodoDao;
+import net.micode.notes.todo.TodoItem;
+import net.micode.notes.todo.TodoReminderManager;
+
+import java.util.Calendar;
+
+public class TodoEditActivity extends AppCompatActivity {
+ private static final String EXTRA_TODO_ID = "todo_id";
+
+ private EditText mEtContent;
+ private Button mBtnAddReminder;
+ private TextView mTvReminderInfo;
+ private Button mBtnSave;
+
+ private TodoDao mTodoDao;
+ private TodoItem mItem;
+ private boolean mIsEdit;
+
+ private final ActivityResultLauncher mLocationPermissionLauncher =
+ registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
+ if (Boolean.TRUE.equals(result.get(Manifest.permission.ACCESS_FINE_LOCATION))) {
+ applyMockLocation();
+ } else {
+ Toast.makeText(this, "需要位置权限才能设置地点提醒", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ public static Intent newIntent(Context context) {
+ return new Intent(context, TodoEditActivity.class);
+ }
+
+ public static Intent newIntent(Context context, long todoId) {
+ Intent i = new Intent(context, TodoEditActivity.class);
+ i.putExtra(EXTRA_TODO_ID, todoId);
+ return i;
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_todo_edit);
+ if (getSupportActionBar() != null) getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ mTodoDao = new TodoDao(this);
+ Intent intent = getIntent();
+ long todoId = (intent != null) ? intent.getLongExtra(EXTRA_TODO_ID, 0) : 0;
+ mIsEdit = todoId > 0;
+ mItem = mIsEdit ? mTodoDao.getById(todoId) : new TodoItem();
+ if (mItem == null) mItem = new TodoItem();
+
+ mEtContent = findViewById(R.id.et_todo_content);
+ mBtnAddReminder = findViewById(R.id.btn_add_reminder);
+ mTvReminderInfo = findViewById(R.id.tv_reminder_info);
+ mBtnSave = findViewById(R.id.btn_save);
+
+ if (mIsEdit) mEtContent.setText(mItem.getContent());
+ updateReminderInfo();
+
+ mBtnAddReminder.setOnClickListener(v -> showReminderOptions());
+ mBtnSave.setOnClickListener(v -> save());
+ }
+
+ @Override
+ public boolean onSupportNavigateUp() {
+ finish();
+ return true;
+ }
+
+ private void showReminderOptions() {
+ String[] options = new String[]{
+ getString(R.string.todo_reminder_time),
+ getString(R.string.todo_reminder_location),
+ getString(R.string.todo_reminder_none)
+ };
+ new android.app.AlertDialog.Builder(this)
+ .setTitle(R.string.todo_add_reminder)
+ .setItems(options, (dialog, which) -> {
+ if (which == 0) pickTime();
+ else if (which == 1) pickLocation();
+ else clearReminder();
+ })
+ .show();
+ }
+
+ private void clearReminder() {
+ mItem.setReminderType(TodoItem.REMINDER_NONE);
+ mItem.setReminderTimestamp(0);
+ mItem.setLatitude(0);
+ mItem.setLongitude(0);
+ mItem.setLocationName("");
+ updateReminderInfo();
+ }
+
+ private void pickTime() {
+ Calendar cal = Calendar.getInstance();
+ if (mItem.getReminderTimestamp() > 0) cal.setTimeInMillis(mItem.getReminderTimestamp());
+
+ DatePickerDialog dateDialog = new DatePickerDialog(this,
+ (v, year, month, dayOfMonth) -> {
+ cal.set(year, month, dayOfMonth);
+ TimePickerDialog timeDialog = new TimePickerDialog(this,
+ (tv, hour, minute) -> {
+ cal.set(Calendar.HOUR_OF_DAY, hour);
+ cal.set(Calendar.MINUTE, minute);
+ cal.set(Calendar.SECOND, 0);
+ mItem.setReminderType(TodoItem.REMINDER_TIME);
+ mItem.setReminderTimestamp(cal.getTimeInMillis());
+ mItem.setLatitude(0);
+ mItem.setLongitude(0);
+ mItem.setLocationName("");
+ updateReminderInfo();
+ },
+ cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE),
+ DateFormat.is24HourFormat(this));
+ timeDialog.show();
+ },
+ cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH));
+ dateDialog.show();
+ }
+
+ private void pickLocation() {
+ if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
+ != PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ mLocationPermissionLauncher.launch(new String[]{
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION
+ });
+ } else {
+ mLocationPermissionLauncher.launch(new String[]{Manifest.permission.ACCESS_FINE_LOCATION});
+ }
+ } else {
+ applyMockLocation();
+ }
+ }
+
+ private void applyMockLocation() {
+ // 模拟选点:返回虚拟坐标和地点名称(无真实地图 SDK)
+ mItem.setReminderType(TodoItem.REMINDER_LOCATION);
+ mItem.setReminderTimestamp(0);
+ mItem.setLatitude(39.9042);
+ mItem.setLongitude(116.4074);
+ mItem.setLocationName("附近超市");
+ updateReminderInfo();
+ Toast.makeText(this, "已设置地点提醒:附近超市", Toast.LENGTH_SHORT).show();
+ }
+
+ private void updateReminderInfo() {
+ if (mItem.getReminderType() == TodoItem.REMINDER_TIME && mItem.getReminderTimestamp() > 0) {
+ mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
+ mTvReminderInfo.setText("时间提醒: " + DateFormat.format("yyyy-MM-dd HH:mm", mItem.getReminderTimestamp()));
+ } else if (mItem.getReminderType() == TodoItem.REMINDER_LOCATION) {
+ mTvReminderInfo.setVisibility(android.view.View.VISIBLE);
+ mTvReminderInfo.setText("地点提醒: " + mItem.getLocationName());
+ } else {
+ mTvReminderInfo.setVisibility(android.view.View.GONE);
+ }
+ }
+
+ private void save() {
+ String content = mEtContent.getText() != null ? mEtContent.getText().toString().trim() : "";
+ if (TextUtils.isEmpty(content)) {
+ Toast.makeText(this, "请输入待办内容", Toast.LENGTH_SHORT).show();
+ return;
+ }
+ mItem.setContent(content);
+ if (mItem.getCreatedTime() == 0) mItem.setCreatedTime(System.currentTimeMillis());
+
+ if (mIsEdit && mItem.getId() > 0) {
+ mTodoDao.update(mItem);
+ } else {
+ long id = mTodoDao.insert(mItem);
+ mItem.setId(id);
+ }
+
+ TodoReminderManager.scheduleReminders(this, mItem);
+ setResult(RESULT_OK);
+ finish();
+ }
+}
diff --git a/src/app/src/main/java/net/micode/notes/ui/TodoFragment.java b/src/app/src/main/java/net/micode/notes/ui/TodoFragment.java
new file mode 100644
index 0000000..7e2dc0f
--- /dev/null
+++ b/src/app/src/main/java/net/micode/notes/ui/TodoFragment.java
@@ -0,0 +1,238 @@
+package net.micode.notes.ui;
+
+/**
+ * 待办事项列表 Fragment
+ *
+ * 主界面底部导航「待办」页,展示未完成与已完成待办,支持增删改查、时间/地点提醒。
+ * 提供游戏化反馈:全部完成后触发震动、提示音、撒花动画。
+ * 长按进入多选模式,可批量删除。
+ *
+ *
+ * @see TodoAdapter
+ * @see TodoEditActivity
+ * @see TodoReminderManager
+ */
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.appbar.MaterialToolbar;
+import com.google.android.material.floatingactionbutton.FloatingActionButton;
+
+import net.micode.notes.R;
+import net.micode.notes.data.TodoDao;
+import net.micode.notes.todo.TodoReminderManager;
+import net.micode.notes.todo.ConfettiView;
+import net.micode.notes.todo.TodoAdapter;
+import net.micode.notes.todo.TodoItem;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TodoFragment extends Fragment {
+ private static final int REQUEST_EDIT = 2001;
+
+ private RecyclerView mRecyclerView;
+ private TodoAdapter mAdapter;
+ private FloatingActionButton mFab;
+ private MaterialToolbar mToolbar;
+ private ConfettiView mConfettiView;
+
+ private TodoDao mTodoDao;
+ private List mUndoneList = new ArrayList<>();
+ private List mDoneList = new ArrayList<>();
+
+ private ActionModeCallback mActionModeCallback;
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_todo, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mTodoDao = new TodoDao(requireContext());
+ mRecyclerView = view.findViewById(R.id.recycler_todo);
+ mFab = view.findViewById(R.id.fab_add_todo);
+ mToolbar = view.findViewById(R.id.toolbar_todo);
+ mConfettiView = view.findViewById(R.id.confetti_view);
+
+ mRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
+ mAdapter = new TodoAdapter();
+ mRecyclerView.setAdapter(mAdapter);
+
+ mAdapter.setOnItemClickListener(new TodoAdapter.OnItemClickListener() {
+ @Override
+ public void onItemClick(TodoItem item, int position) {
+ if (!mAdapter.isChoiceMode()) {
+ Intent intent = TodoEditActivity.newIntent(requireContext(), item.getId());
+ startActivityForResult(intent, REQUEST_EDIT);
+ }
+ }
+
+ @Override
+ public void onCheckChanged(TodoItem item, boolean checked) {
+ if (mAdapter.isChoiceMode()) return;
+ if (item == null || item.getId() <= 0) return;
+ item.setDone(checked);
+ mTodoDao.update(item);
+ refreshData();
+ if (checked) {
+ checkAndTriggerGamification();
+ }
+ }
+ });
+
+ mAdapter.setOnItemLongClickListener((item, position) -> {
+ if (!mAdapter.isChoiceMode() && mActionModeCallback == null) {
+ mActionModeCallback = new ActionModeCallback();
+ requireActivity().startActionMode(mActionModeCallback);
+ mAdapter.setChoiceMode(true);
+ mAdapter.setChecked(position, true);
+ mFab.setVisibility(View.GONE);
+ }
+ });
+
+ mFab.setOnClickListener(v -> {
+ if (getActivity() == null || !isAdded()) return;
+ Intent intent = new Intent(getActivity(), TodoEditActivity.class);
+ startActivityForResult(intent, REQUEST_EDIT);
+ });
+
+ if (mToolbar != null && getActivity() instanceof MainActivity) {
+ mToolbar.setNavigationOnClickListener(v -> ((MainActivity) getActivity()).openDrawer());
+ }
+
+ mConfettiView.setOnClickListener(v -> mConfettiView.setVisibility(View.GONE));
+
+ refreshData();
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (requestCode == REQUEST_EDIT && resultCode == Activity.RESULT_OK) {
+ refreshData();
+ }
+ }
+
+ private void refreshData() {
+ mUndoneList = mTodoDao.getUndone();
+ mDoneList = mTodoDao.getDone();
+ mAdapter.setData(mUndoneList, mDoneList);
+ }
+
+ private void checkAndTriggerGamification() {
+ int undoneCount = mTodoDao.getUndoneCount();
+ if (undoneCount == 0) {
+ triggerGamification();
+ }
+ }
+
+ private void triggerGamification() {
+ vibrate();
+ playSuccessSound();
+ showConfetti();
+ }
+
+ private void vibrate() {
+ try {
+ if (getContext() == null) return;
+ Vibrator v = (Vibrator) getContext().getSystemService(android.content.Context.VIBRATOR_SERVICE);
+ if (v != null && v.hasVibrator()) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ v.vibrate(VibrationEffect.createOneShot(150, VibrationEffect.DEFAULT_AMPLITUDE));
+ } else {
+ v.vibrate(150);
+ }
+ }
+ } catch (Exception e) { /* ignore */ }
+ }
+
+ /** 撒花时的短促有力提示音(系统 ToneGenerator,无需 raw 资源) */
+ private static final int SUCCESS_TONE_DURATION_MS = 120;
+
+ private void playSuccessSound() {
+ try {
+ Context ctx = getContext();
+ if (ctx == null) return;
+ ToneGenerator tone = new ToneGenerator(AudioManager.STREAM_NOTIFICATION, 85);
+ tone.startTone(ToneGenerator.TONE_PROP_ACK, SUCCESS_TONE_DURATION_MS);
+ new Handler(Looper.getMainLooper()).postDelayed(() -> {
+ try {
+ tone.release();
+ } catch (Exception ignored) { }
+ }, SUCCESS_TONE_DURATION_MS + 80);
+ } catch (Exception e) { /* 无扬声器或权限时静默 */ }
+ }
+
+ private void showConfetti() {
+ if (mConfettiView != null) {
+ mConfettiView.post(() -> {
+ mConfettiView.start();
+ });
+ }
+ }
+
+ private class ActionModeCallback implements android.view.ActionMode.Callback {
+ @Override
+ public boolean onCreateActionMode(android.view.ActionMode mode, android.view.Menu menu) {
+ requireActivity().getMenuInflater().inflate(R.menu.todo_list_options, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(android.view.ActionMode mode, android.view.Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(android.view.ActionMode mode, android.view.MenuItem item) {
+ if (item.getItemId() == R.id.delete) {
+ List ids = mAdapter.getSelectedIds();
+ if (ids.isEmpty()) {
+ Toast.makeText(requireContext(), R.string.menu_select_none, Toast.LENGTH_SHORT).show();
+ return true;
+ }
+ new android.app.AlertDialog.Builder(requireContext())
+ .setTitle(R.string.alert_title_delete)
+ .setMessage(getString(R.string.todo_delete_confirm, ids.size()))
+ .setPositiveButton(android.R.string.ok, (d, w) -> {
+ for (long id : ids) TodoReminderManager.cancelAllForTodo(requireContext(), id);
+ mTodoDao.deleteByIds(ids);
+ refreshData();
+ mode.finish();
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(android.view.ActionMode mode) {
+ mAdapter.setChoiceMode(false);
+ mFab.setVisibility(View.VISIBLE);
+ mActionModeCallback = null;
+ }
+ }
+}
diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider.java b/src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java
similarity index 100%
rename from src/src/net/micode/notes/widget/NoteWidgetProvider.java
rename to src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider.java
diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java b/src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java
similarity index 100%
rename from src/src/net/micode/notes/widget/NoteWidgetProvider_2x.java
rename to src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_2x.java
diff --git a/src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java b/src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java
similarity index 100%
rename from src/src/net/micode/notes/widget/NoteWidgetProvider_4x.java
rename to src/app/src/main/java/net/micode/notes/widget/NoteWidgetProvider_4x.java
diff --git a/src/app/src/main/res/color/bottom_nav_color.xml b/src/app/src/main/res/color/bottom_nav_color.xml
new file mode 100644
index 0000000..aeb73c4
--- /dev/null
+++ b/src/app/src/main/res/color/bottom_nav_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/src/res/color/primary_text_dark.xml b/src/app/src/main/res/color/primary_text_dark.xml
similarity index 100%
rename from src/res/color/primary_text_dark.xml
rename to src/app/src/main/res/color/primary_text_dark.xml
diff --git a/src/res/color/secondary_text_dark.xml b/src/app/src/main/res/color/secondary_text_dark.xml
similarity index 100%
rename from src/res/color/secondary_text_dark.xml
rename to src/app/src/main/res/color/secondary_text_dark.xml
diff --git a/src/res/drawable-hdpi/bg_btn_set_color.png b/src/app/src/main/res/drawable-hdpi/bg_btn_set_color.png
similarity index 100%
rename from src/res/drawable-hdpi/bg_btn_set_color.png
rename to src/app/src/main/res/drawable-hdpi/bg_btn_set_color.png
diff --git a/src/res/drawable-hdpi/bg_color_btn_mask.png b/src/app/src/main/res/drawable-hdpi/bg_color_btn_mask.png
similarity index 100%
rename from src/res/drawable-hdpi/bg_color_btn_mask.png
rename to src/app/src/main/res/drawable-hdpi/bg_color_btn_mask.png
diff --git a/src/res/drawable-hdpi/call_record.png b/src/app/src/main/res/drawable-hdpi/call_record.png
similarity index 100%
rename from src/res/drawable-hdpi/call_record.png
rename to src/app/src/main/res/drawable-hdpi/call_record.png
diff --git a/src/res/drawable-hdpi/clock.png b/src/app/src/main/res/drawable-hdpi/clock.png
similarity index 100%
rename from src/res/drawable-hdpi/clock.png
rename to src/app/src/main/res/drawable-hdpi/clock.png
diff --git a/src/res/drawable-hdpi/delete.png b/src/app/src/main/res/drawable-hdpi/delete.png
similarity index 100%
rename from src/res/drawable-hdpi/delete.png
rename to src/app/src/main/res/drawable-hdpi/delete.png
diff --git a/src/res/drawable-hdpi/dropdown_icon.9.png b/src/app/src/main/res/drawable-hdpi/dropdown_icon.9.png
similarity index 100%
rename from src/res/drawable-hdpi/dropdown_icon.9.png
rename to src/app/src/main/res/drawable-hdpi/dropdown_icon.9.png
diff --git a/src/res/drawable-hdpi/edit_blue.9.png b/src/app/src/main/res/drawable-hdpi/edit_blue.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_blue.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_blue.9.png
diff --git a/src/res/drawable-hdpi/edit_green.9.png b/src/app/src/main/res/drawable-hdpi/edit_green.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_green.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_green.9.png
diff --git a/src/res/drawable-hdpi/edit_red.9.png b/src/app/src/main/res/drawable-hdpi/edit_red.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_red.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_red.9.png
diff --git a/src/res/drawable-hdpi/edit_title_blue.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_blue.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_blue.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_blue.9.png
diff --git a/src/res/drawable-hdpi/edit_title_green.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_green.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_green.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_green.9.png
diff --git a/src/res/drawable-hdpi/edit_title_red.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_red.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_red.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_red.9.png
diff --git a/src/res/drawable-hdpi/edit_title_white.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_white.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_white.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_white.9.png
diff --git a/src/res/drawable-hdpi/edit_title_yellow.9.png b/src/app/src/main/res/drawable-hdpi/edit_title_yellow.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_title_yellow.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_title_yellow.9.png
diff --git a/src/res/drawable-hdpi/edit_white.9.png b/src/app/src/main/res/drawable-hdpi/edit_white.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_white.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_white.9.png
diff --git a/src/res/drawable-hdpi/edit_yellow.9.png b/src/app/src/main/res/drawable-hdpi/edit_yellow.9.png
similarity index 100%
rename from src/res/drawable-hdpi/edit_yellow.9.png
rename to src/app/src/main/res/drawable-hdpi/edit_yellow.9.png
diff --git a/src/res/drawable-hdpi/font_large.png b/src/app/src/main/res/drawable-hdpi/font_large.png
similarity index 100%
rename from src/res/drawable-hdpi/font_large.png
rename to src/app/src/main/res/drawable-hdpi/font_large.png
diff --git a/src/res/drawable-hdpi/font_normal.png b/src/app/src/main/res/drawable-hdpi/font_normal.png
similarity index 100%
rename from src/res/drawable-hdpi/font_normal.png
rename to src/app/src/main/res/drawable-hdpi/font_normal.png
diff --git a/src/res/drawable-hdpi/font_size_selector_bg.9.png b/src/app/src/main/res/drawable-hdpi/font_size_selector_bg.9.png
similarity index 100%
rename from src/res/drawable-hdpi/font_size_selector_bg.9.png
rename to src/app/src/main/res/drawable-hdpi/font_size_selector_bg.9.png
diff --git a/src/res/drawable-hdpi/font_small.png b/src/app/src/main/res/drawable-hdpi/font_small.png
similarity index 100%
rename from src/res/drawable-hdpi/font_small.png
rename to src/app/src/main/res/drawable-hdpi/font_small.png
diff --git a/src/res/drawable-hdpi/font_super.png b/src/app/src/main/res/drawable-hdpi/font_super.png
similarity index 100%
rename from src/res/drawable-hdpi/font_super.png
rename to src/app/src/main/res/drawable-hdpi/font_super.png
diff --git a/src/res/drawable-hdpi/icon_app.png b/src/app/src/main/res/drawable-hdpi/icon_app.png
similarity index 100%
rename from src/res/drawable-hdpi/icon_app.png
rename to src/app/src/main/res/drawable-hdpi/icon_app.png
diff --git a/src/res/drawable-hdpi/list_background.png b/src/app/src/main/res/drawable-hdpi/list_background.png
similarity index 100%
rename from src/res/drawable-hdpi/list_background.png
rename to src/app/src/main/res/drawable-hdpi/list_background.png
diff --git a/src/res/drawable-hdpi/list_blue_down.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_down.9.png
diff --git a/src/res/drawable-hdpi/list_blue_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_middle.9.png
diff --git a/src/res/drawable-hdpi/list_blue_single.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_single.9.png
diff --git a/src/res/drawable-hdpi/list_blue_up.9.png b/src/app/src/main/res/drawable-hdpi/list_blue_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_blue_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_blue_up.9.png
diff --git a/src/res/drawable-hdpi/list_folder.9.png b/src/app/src/main/res/drawable-hdpi/list_folder.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_folder.9.png
rename to src/app/src/main/res/drawable-hdpi/list_folder.9.png
diff --git a/src/res/drawable-hdpi/list_footer_bg.9.png b/src/app/src/main/res/drawable-hdpi/list_footer_bg.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_footer_bg.9.png
rename to src/app/src/main/res/drawable-hdpi/list_footer_bg.9.png
diff --git a/src/res/drawable-hdpi/list_green_down.9.png b/src/app/src/main/res/drawable-hdpi/list_green_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_down.9.png
diff --git a/src/res/drawable-hdpi/list_green_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_green_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_middle.9.png
diff --git a/src/res/drawable-hdpi/list_green_single.9.png b/src/app/src/main/res/drawable-hdpi/list_green_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_single.9.png
diff --git a/src/res/drawable-hdpi/list_green_up.9.png b/src/app/src/main/res/drawable-hdpi/list_green_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_green_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_green_up.9.png
diff --git a/src/res/drawable-hdpi/list_red_down.9.png b/src/app/src/main/res/drawable-hdpi/list_red_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_down.9.png
diff --git a/src/res/drawable-hdpi/list_red_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_red_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_middle.9.png
diff --git a/src/res/drawable-hdpi/list_red_single.9.png b/src/app/src/main/res/drawable-hdpi/list_red_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_single.9.png
diff --git a/src/res/drawable-hdpi/list_red_up.9.png b/src/app/src/main/res/drawable-hdpi/list_red_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_red_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_red_up.9.png
diff --git a/src/res/drawable-hdpi/list_white_down.9.png b/src/app/src/main/res/drawable-hdpi/list_white_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_down.9.png
diff --git a/src/res/drawable-hdpi/list_white_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_white_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_middle.9.png
diff --git a/src/res/drawable-hdpi/list_white_single.9.png b/src/app/src/main/res/drawable-hdpi/list_white_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_single.9.png
diff --git a/src/res/drawable-hdpi/list_white_up.9.png b/src/app/src/main/res/drawable-hdpi/list_white_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_white_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_white_up.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_down.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_down.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_down.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_down.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_middle.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_middle.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_middle.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_middle.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_single.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_single.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_single.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_single.9.png
diff --git a/src/res/drawable-hdpi/list_yellow_up.9.png b/src/app/src/main/res/drawable-hdpi/list_yellow_up.9.png
similarity index 100%
rename from src/res/drawable-hdpi/list_yellow_up.9.png
rename to src/app/src/main/res/drawable-hdpi/list_yellow_up.9.png
diff --git a/src/res/drawable-hdpi/menu_delete.png b/src/app/src/main/res/drawable-hdpi/menu_delete.png
similarity index 100%
rename from src/res/drawable-hdpi/menu_delete.png
rename to src/app/src/main/res/drawable-hdpi/menu_delete.png
diff --git a/src/res/drawable-hdpi/menu_move.png b/src/app/src/main/res/drawable-hdpi/menu_move.png
similarity index 100%
rename from src/res/drawable-hdpi/menu_move.png
rename to src/app/src/main/res/drawable-hdpi/menu_move.png
diff --git a/src/res/drawable-hdpi/new_note_normal.png b/src/app/src/main/res/drawable-hdpi/new_note_normal.png
similarity index 100%
rename from src/res/drawable-hdpi/new_note_normal.png
rename to src/app/src/main/res/drawable-hdpi/new_note_normal.png
diff --git a/src/res/drawable-hdpi/new_note_pressed.png b/src/app/src/main/res/drawable-hdpi/new_note_pressed.png
similarity index 100%
rename from src/res/drawable-hdpi/new_note_pressed.png
rename to src/app/src/main/res/drawable-hdpi/new_note_pressed.png
diff --git a/src/res/drawable-hdpi/note_edit_color_selector_panel.png b/src/app/src/main/res/drawable-hdpi/note_edit_color_selector_panel.png
similarity index 100%
rename from src/res/drawable-hdpi/note_edit_color_selector_panel.png
rename to src/app/src/main/res/drawable-hdpi/note_edit_color_selector_panel.png
diff --git a/src/res/drawable-hdpi/notification.png b/src/app/src/main/res/drawable-hdpi/notification.png
similarity index 100%
rename from src/res/drawable-hdpi/notification.png
rename to src/app/src/main/res/drawable-hdpi/notification.png
diff --git a/src/res/drawable-hdpi/search_result.png b/src/app/src/main/res/drawable-hdpi/search_result.png
similarity index 100%
rename from src/res/drawable-hdpi/search_result.png
rename to src/app/src/main/res/drawable-hdpi/search_result.png
diff --git a/src/res/drawable-hdpi/selected.png b/src/app/src/main/res/drawable-hdpi/selected.png
similarity index 100%
rename from src/res/drawable-hdpi/selected.png
rename to src/app/src/main/res/drawable-hdpi/selected.png
diff --git a/src/res/drawable-hdpi/title_alert.png b/src/app/src/main/res/drawable-hdpi/title_alert.png
similarity index 100%
rename from src/res/drawable-hdpi/title_alert.png
rename to src/app/src/main/res/drawable-hdpi/title_alert.png
diff --git a/src/res/drawable-hdpi/title_bar_bg.9.png b/src/app/src/main/res/drawable-hdpi/title_bar_bg.9.png
similarity index 100%
rename from src/res/drawable-hdpi/title_bar_bg.9.png
rename to src/app/src/main/res/drawable-hdpi/title_bar_bg.9.png
diff --git a/src/res/drawable-hdpi/widget_2x_blue.png b/src/app/src/main/res/drawable-hdpi/widget_2x_blue.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_blue.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_blue.png
diff --git a/src/res/drawable-hdpi/widget_2x_green.png b/src/app/src/main/res/drawable-hdpi/widget_2x_green.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_green.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_green.png
diff --git a/src/res/drawable-hdpi/widget_2x_red.png b/src/app/src/main/res/drawable-hdpi/widget_2x_red.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_red.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_red.png
diff --git a/src/res/drawable-hdpi/widget_2x_white.png b/src/app/src/main/res/drawable-hdpi/widget_2x_white.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_white.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_white.png
diff --git a/src/res/drawable-hdpi/widget_2x_yellow.png b/src/app/src/main/res/drawable-hdpi/widget_2x_yellow.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_2x_yellow.png
rename to src/app/src/main/res/drawable-hdpi/widget_2x_yellow.png
diff --git a/src/res/drawable-hdpi/widget_4x_blue.png b/src/app/src/main/res/drawable-hdpi/widget_4x_blue.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_blue.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_blue.png
diff --git a/src/res/drawable-hdpi/widget_4x_green.png b/src/app/src/main/res/drawable-hdpi/widget_4x_green.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_green.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_green.png
diff --git a/src/res/drawable-hdpi/widget_4x_red.png b/src/app/src/main/res/drawable-hdpi/widget_4x_red.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_red.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_red.png
diff --git a/src/res/drawable-hdpi/widget_4x_white.png b/src/app/src/main/res/drawable-hdpi/widget_4x_white.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_white.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_white.png
diff --git a/src/res/drawable-hdpi/widget_4x_yellow.png b/src/app/src/main/res/drawable-hdpi/widget_4x_yellow.png
similarity index 100%
rename from src/res/drawable-hdpi/widget_4x_yellow.png
rename to src/app/src/main/res/drawable-hdpi/widget_4x_yellow.png
diff --git a/src/app/src/main/res/drawable/bg_floating_ball.xml b/src/app/src/main/res/drawable/bg_floating_ball.xml
new file mode 100644
index 0000000..181dcad
--- /dev/null
+++ b/src/app/src/main/res/drawable/bg_floating_ball.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/drawable/ic_add_24dp.xml b/src/app/src/main/res/drawable/ic_add_24dp.xml
new file mode 100644
index 0000000..ea55c4e
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_add_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_arrow_back_24dp.xml b/src/app/src/main/res/drawable/ic_arrow_back_24dp.xml
new file mode 100644
index 0000000..4e13e4a
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_arrow_back_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_edit_note_24dp.xml b/src/app/src/main/res/drawable/ic_edit_note_24dp.xml
new file mode 100644
index 0000000..7e51a3c
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_edit_note_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_exit_privacy_24dp.xml b/src/app/src/main/res/drawable/ic_exit_privacy_24dp.xml
new file mode 100644
index 0000000..99b02d7
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_exit_privacy_24dp.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_export_24dp.xml b/src/app/src/main/res/drawable/ic_export_24dp.xml
new file mode 100644
index 0000000..5c3bfe4
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_export_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_folder_24dp.xml b/src/app/src/main/res/drawable/ic_folder_24dp.xml
new file mode 100644
index 0000000..591115a
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_folder_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_menu_24dp.xml b/src/app/src/main/res/drawable/ic_menu_24dp.xml
new file mode 100644
index 0000000..ecb13cc
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_menu_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_search_24dp.xml b/src/app/src/main/res/drawable/ic_search_24dp.xml
new file mode 100644
index 0000000..0a6ea2c
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_search_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_settings_24dp.xml b/src/app/src/main/res/drawable/ic_settings_24dp.xml
new file mode 100644
index 0000000..5aa8318
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_settings_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/app/src/main/res/drawable/ic_sync_24dp.xml b/src/app/src/main/res/drawable/ic_sync_24dp.xml
new file mode 100644
index 0000000..3633fd0
--- /dev/null
+++ b/src/app/src/main/res/drawable/ic_sync_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/res/drawable/new_note.xml b/src/app/src/main/res/drawable/new_note.xml
similarity index 100%
rename from src/res/drawable/new_note.xml
rename to src/app/src/main/res/drawable/new_note.xml
diff --git a/src/app/src/main/res/drawable/splash_accent_line.xml b/src/app/src/main/res/drawable/splash_accent_line.xml
new file mode 100644
index 0000000..3c5bf62
--- /dev/null
+++ b/src/app/src/main/res/drawable/splash_accent_line.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/src/app/src/main/res/drawable/splash_background.xml b/src/app/src/main/res/drawable/splash_background.xml
new file mode 100644
index 0000000..90642cb
--- /dev/null
+++ b/src/app/src/main/res/drawable/splash_background.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/src/res/layout/account_dialog_title.xml b/src/app/src/main/res/layout/account_dialog_title.xml
similarity index 100%
rename from src/res/layout/account_dialog_title.xml
rename to src/app/src/main/res/layout/account_dialog_title.xml
diff --git a/src/app/src/main/res/layout/activity_main.xml b/src/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..5a6601a
--- /dev/null
+++ b/src/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,52 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/activity_splash.xml b/src/app/src/main/res/layout/activity_splash.xml
new file mode 100644
index 0000000..6be03d3
--- /dev/null
+++ b/src/app/src/main/res/layout/activity_splash.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/activity_todo_edit.xml b/src/app/src/main/res/layout/activity_todo_edit.xml
new file mode 100644
index 0000000..9185c33
--- /dev/null
+++ b/src/app/src/main/res/layout/activity_todo_edit.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/res/layout/add_account_text.xml b/src/app/src/main/res/layout/add_account_text.xml
similarity index 100%
rename from src/res/layout/add_account_text.xml
rename to src/app/src/main/res/layout/add_account_text.xml
diff --git a/src/res/layout/datetime_picker.xml b/src/app/src/main/res/layout/datetime_picker.xml
similarity index 100%
rename from src/res/layout/datetime_picker.xml
rename to src/app/src/main/res/layout/datetime_picker.xml
diff --git a/src/res/layout/dialog_edit_text.xml b/src/app/src/main/res/layout/dialog_edit_text.xml
similarity index 100%
rename from src/res/layout/dialog_edit_text.xml
rename to src/app/src/main/res/layout/dialog_edit_text.xml
diff --git a/src/app/src/main/res/layout/drawer_header.xml b/src/app/src/main/res/layout/drawer_header.xml
new file mode 100644
index 0000000..250e079
--- /dev/null
+++ b/src/app/src/main/res/layout/drawer_header.xml
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/src/res/layout/folder_list_item.xml b/src/app/src/main/res/layout/folder_list_item.xml
similarity index 100%
rename from src/res/layout/folder_list_item.xml
rename to src/app/src/main/res/layout/folder_list_item.xml
diff --git a/src/app/src/main/res/layout/fragment_inspiration.xml b/src/app/src/main/res/layout/fragment_inspiration.xml
new file mode 100644
index 0000000..f608b43
--- /dev/null
+++ b/src/app/src/main/res/layout/fragment_inspiration.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/layout/fragment_mi_steward.xml b/src/app/src/main/res/layout/fragment_mi_steward.xml
new file mode 100644
index 0000000..540e8dc
--- /dev/null
+++ b/src/app/src/main/res/layout/fragment_mi_steward.xml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/layout/fragment_todo.xml b/src/app/src/main/res/layout/fragment_todo.xml
new file mode 100644
index 0000000..3bd0460
--- /dev/null
+++ b/src/app/src/main/res/layout/fragment_todo.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/item_chat_ai.xml b/src/app/src/main/res/layout/item_chat_ai.xml
new file mode 100644
index 0000000..4c4c527
--- /dev/null
+++ b/src/app/src/main/res/layout/item_chat_ai.xml
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/layout/item_chat_user.xml b/src/app/src/main/res/layout/item_chat_user.xml
new file mode 100644
index 0000000..313f0f3
--- /dev/null
+++ b/src/app/src/main/res/layout/item_chat_user.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/layout/item_todo.xml b/src/app/src/main/res/layout/item_todo.xml
new file mode 100644
index 0000000..03c615f
--- /dev/null
+++ b/src/app/src/main/res/layout/item_todo.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/item_todo_section.xml b/src/app/src/main/res/layout/item_todo_section.xml
new file mode 100644
index 0000000..df262f5
--- /dev/null
+++ b/src/app/src/main/res/layout/item_todo_section.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/src/app/src/main/res/layout/layout_floating_ball.xml b/src/app/src/main/res/layout/layout_floating_ball.xml
new file mode 100644
index 0000000..0f11a0c
--- /dev/null
+++ b/src/app/src/main/res/layout/layout_floating_ball.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/layout/layout_floating_input.xml b/src/app/src/main/res/layout/layout_floating_input.xml
new file mode 100644
index 0000000..a4ccd83
--- /dev/null
+++ b/src/app/src/main/res/layout/layout_floating_input.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/layout/note_edit.xml b/src/app/src/main/res/layout/note_edit.xml
similarity index 88%
rename from src/res/layout/note_edit.xml
rename to src/app/src/main/res/layout/note_edit.xml
index 0b5bcdc..fe7a1a6 100644
--- a/src/res/layout/note_edit.xml
+++ b/src/app/src/main/res/layout/note_edit.xml
@@ -31,12 +31,42 @@
android:layout_width="fill_parent"
android:layout_height="wrap_content">
+
+
+
+
+
@@ -81,11 +111,12 @@
android:scrollbars="none"
android:overScrollMode="never"
android:layout_gravity="left|top"
- android:fadingEdgeLength="0dip">
+ android:fadingEdgeLength="0dip"
+ android:fillViewport="true">
+ android:layout_height="wrap_content">
+
+
+
+ android:gravity="center_vertical"
+ android:orientation="horizontal">
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/res/layout/note_list_dropdown_menu.xml b/src/app/src/main/res/layout/note_list_dropdown_menu.xml
similarity index 100%
rename from src/res/layout/note_list_dropdown_menu.xml
rename to src/app/src/main/res/layout/note_list_dropdown_menu.xml
diff --git a/src/res/layout/note_list_footer.xml b/src/app/src/main/res/layout/note_list_footer.xml
similarity index 83%
rename from src/res/layout/note_list_footer.xml
rename to src/app/src/main/res/layout/note_list_footer.xml
index 5ca7b22..29dead6 100644
--- a/src/res/layout/note_list_footer.xml
+++ b/src/app/src/main/res/layout/note_list_footer.xml
@@ -17,8 +17,7 @@
\ No newline at end of file
+ android:background="@color/background_light" />
\ No newline at end of file
diff --git a/src/res/layout/settings_header.xml b/src/app/src/main/res/layout/settings_header.xml
similarity index 100%
rename from src/res/layout/settings_header.xml
rename to src/app/src/main/res/layout/settings_header.xml
diff --git a/src/res/layout/widget_2x.xml b/src/app/src/main/res/layout/widget_2x.xml
similarity index 100%
rename from src/res/layout/widget_2x.xml
rename to src/app/src/main/res/layout/widget_2x.xml
diff --git a/src/res/layout/widget_4x.xml b/src/app/src/main/res/layout/widget_4x.xml
similarity index 100%
rename from src/res/layout/widget_4x.xml
rename to src/app/src/main/res/layout/widget_4x.xml
diff --git a/src/app/src/main/res/menu/bottom_nav_menu.xml b/src/app/src/main/res/menu/bottom_nav_menu.xml
new file mode 100644
index 0000000..d98017d
--- /dev/null
+++ b/src/app/src/main/res/menu/bottom_nav_menu.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/menu/call_note_edit.xml b/src/app/src/main/res/menu/call_note_edit.xml
similarity index 100%
rename from src/res/menu/call_note_edit.xml
rename to src/app/src/main/res/menu/call_note_edit.xml
diff --git a/src/res/menu/call_record_folder.xml b/src/app/src/main/res/menu/call_record_folder.xml
similarity index 100%
rename from src/res/menu/call_record_folder.xml
rename to src/app/src/main/res/menu/call_record_folder.xml
diff --git a/src/app/src/main/res/menu/drawer_menu.xml b/src/app/src/main/res/menu/drawer_menu.xml
new file mode 100644
index 0000000..d099ba7
--- /dev/null
+++ b/src/app/src/main/res/menu/drawer_menu.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
diff --git a/src/res/menu/note_edit.xml b/src/app/src/main/res/menu/note_edit.xml
similarity index 100%
rename from src/res/menu/note_edit.xml
rename to src/app/src/main/res/menu/note_edit.xml
diff --git a/src/res/menu/note_list.xml b/src/app/src/main/res/menu/note_list.xml
similarity index 100%
rename from src/res/menu/note_list.xml
rename to src/app/src/main/res/menu/note_list.xml
diff --git a/src/res/menu/note_list_dropdown.xml b/src/app/src/main/res/menu/note_list_dropdown.xml
similarity index 100%
rename from src/res/menu/note_list_dropdown.xml
rename to src/app/src/main/res/menu/note_list_dropdown.xml
diff --git a/src/res/menu/note_list_options.xml b/src/app/src/main/res/menu/note_list_options.xml
similarity index 79%
rename from src/res/menu/note_list_options.xml
rename to src/app/src/main/res/menu/note_list_options.xml
index daac008..95e89b6 100644
--- a/src/res/menu/note_list_options.xml
+++ b/src/app/src/main/res/menu/note_list_options.xml
@@ -17,6 +17,14 @@
+
+
-
+
+
+
+
diff --git a/src/res/menu/sub_folder.xml b/src/app/src/main/res/menu/sub_folder.xml
similarity index 100%
rename from src/res/menu/sub_folder.xml
rename to src/app/src/main/res/menu/sub_folder.xml
diff --git a/src/app/src/main/res/menu/todo_list_options.xml b/src/app/src/main/res/menu/todo_list_options.xml
new file mode 100644
index 0000000..9ec9c59
--- /dev/null
+++ b/src/app/src/main/res/menu/todo_list_options.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/src/res/raw-zh-rCN/introduction b/src/app/src/main/res/raw-zh-rCN/introduction
similarity index 100%
rename from src/res/raw-zh-rCN/introduction
rename to src/app/src/main/res/raw-zh-rCN/introduction
diff --git a/src/res/raw/introduction b/src/app/src/main/res/raw/introduction
similarity index 100%
rename from src/res/raw/introduction
rename to src/app/src/main/res/raw/introduction
diff --git a/src/res/values-zh-rCN/arrays.xml b/src/app/src/main/res/values-zh-rCN/arrays.xml
similarity index 100%
rename from src/res/values-zh-rCN/arrays.xml
rename to src/app/src/main/res/values-zh-rCN/arrays.xml
diff --git a/src/res/values-zh-rCN/strings.xml b/src/app/src/main/res/values-zh-rCN/strings.xml
similarity index 81%
rename from src/res/values-zh-rCN/strings.xml
rename to src/app/src/main/res/values-zh-rCN/strings.xml
index 09f75ed..a78effd 100644
--- a/src/res/values-zh-rCN/strings.xml
+++ b/src/app/src/main/res/values-zh-rCN/strings.xml
@@ -24,9 +24,29 @@
访客模式下,便签内容不可见
...
新建便签
+ 便签
+ 返回上一级
+ Mi 管家
+ 便签
+ 待办
+ 待办事项
+ 未完成
+ 已完成
+ 输入待办内容...
+ 添加提醒
+ 时间提醒
+ 地点提醒
+ 保存
+ 删除
+ 确定删除选中的 %d 项?
+ 无提醒
成功删除提醒
创建提醒
已过期
+ 便签提醒
+ 您有一条便签提醒
+ 便签提醒
+ 时间提醒到点后在此渠道显示通知
yyyyMMdd
MM月dd日 kk:mm
知道了
@@ -43,6 +63,10 @@
设置
搜索
删除
+ 置顶
+ 取消置顶
+ 已置顶
+ 已取消置顶
移动到文件夹
选中了 %d 项
没有选中项,操作无效
@@ -53,6 +77,11 @@
正常
大
超大
+ 加粗
+ 字号变大
+ 字号变小
+ 无标题
+ 图片便签
进入清单模式
退出清单模式
查看文件夹
@@ -77,6 +106,7 @@
要查看的便签不存在
不能为空便签设置闹钟提醒
不能将空便签发送到桌面
+ 无法读取该图片,请换一张试试
导出成功
导出失败
已将文本文件(%1$s)输出至SD卡(%2$s)目录
diff --git a/src/res/values-zh-rTW/arrays.xml b/src/app/src/main/res/values-zh-rTW/arrays.xml
similarity index 100%
rename from src/res/values-zh-rTW/arrays.xml
rename to src/app/src/main/res/values-zh-rTW/arrays.xml
diff --git a/src/res/values-zh-rTW/strings.xml b/src/app/src/main/res/values-zh-rTW/strings.xml
similarity index 95%
rename from src/res/values-zh-rTW/strings.xml
rename to src/app/src/main/res/values-zh-rTW/strings.xml
index 3c41894..2e7bd9e 100644
--- a/src/res/values-zh-rTW/strings.xml
+++ b/src/app/src/main/res/values-zh-rTW/strings.xml
@@ -24,6 +24,7 @@
訪客模式下,便籤內容不可見
...
新建便簽
+ 返回上一級
成功刪除提醒
創建提醒
已過期
@@ -44,6 +45,10 @@
設置
搜尋
刪除
+ 置頂
+ 取消置頂
+ 已置頂
+ 已取消置頂
移動到文件夾
選中了 %d 項
沒有選中項,操作無效
@@ -76,6 +81,7 @@
要查看的便籤不存在
不能爲空便籤設置鬧鐘提醒
不能將空便籤發送到桌面
+ 無法讀取該圖片,請換一張試試
導出成功
導出失敗
已將文本文件(%1$s)導出至SD(%2$s)目錄
diff --git a/src/res/values/arrays.xml b/src/app/src/main/res/values/arrays.xml
similarity index 100%
rename from src/res/values/arrays.xml
rename to src/app/src/main/res/values/arrays.xml
diff --git a/src/app/src/main/res/values/colors.xml b/src/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..d26450b
--- /dev/null
+++ b/src/app/src/main/res/values/colors.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+ #335b5b5b
+
+ #CCB8860B
+ #CC1565C0
+ #CC616161
+ #CC2E7D32
+ #CCC62828
+
+
+ #FFFFFF
+ #F7F8FA
+ #EFF1F3
+ #1D1D1D
+ #657786
+ #1DA1F2
+ #1A91DA
+
+ #657786
+ #1DA1F2
+ #657786
+ #1D1D1D
+
+
+ #FF1C1C1E
+
diff --git a/src/res/values/dimens.xml b/src/app/src/main/res/values/dimens.xml
similarity index 100%
rename from src/res/values/dimens.xml
rename to src/app/src/main/res/values/dimens.xml
diff --git a/src/res/values/strings.xml b/src/app/src/main/res/values/strings.xml
similarity index 79%
rename from src/res/values/strings.xml
rename to src/app/src/main/res/values/strings.xml
index 55df868..158acfc 100644
--- a/src/res/values/strings.xml
+++ b/src/app/src/main/res/values/strings.xml
@@ -24,9 +24,33 @@
Privacy mode,can not see note content
...
Add note
+ Notes
+ Back to note list
+ 隐私空间
+ 退出隐私空间
+ Mi 管家
+ 便签
+ 待办
+ 待办事项
+ 未完成
+ 已完成
+ 输入待办内容...
+ 添加提醒
+ 时间提醒
+ 地点提醒
+ 保存
+ 删除
+ 确定删除选中的 %d 项?
+ 无提醒
+ 全部完成!
+ 太棒了,所有待办都已完成!
Delete reminder successfully
Set reminder
Expired
+ 便签提醒
+ 您有一条便签提醒
+ 便签提醒
+ 时间提醒到点后在此渠道显示通知
yyyyMMdd
MMMd kk:mm
Got it
@@ -47,6 +71,10 @@
Settings
Search
Delete
+ Pin to top
+ Unpin
+ Pinned to top
+ Unpinned
Move to folder
%d selected
Nothing selected, the operation is invalid
@@ -57,6 +85,11 @@
Medium
Large
Super
+ Bold
+ Larger text
+ Smaller text
+ Untitled
+ Image note
Enter check list
Leave check list
View folder
@@ -81,6 +114,7 @@
The note is not exist
Sorry, can not set clock on empty note
Sorry, can not send and empty note to home
+ Failed to load image, please try another one
Export successful
Export fail
Export text file (%1$s) to SD (%2$s) directory
@@ -119,6 +153,7 @@
Delete
Call notes
Input name
+ 共 %d 字
Searching Notes
Search notes
diff --git a/src/res/values/styles.xml b/src/app/src/main/res/values/styles.xml
similarity index 73%
rename from src/res/values/styles.xml
rename to src/app/src/main/res/values/styles.xml
index d750e65..fe9df45 100644
--- a/src/res/values/styles.xml
+++ b/src/app/src/main/res/values/styles.xml
@@ -66,4 +66,23 @@
- gone
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/app/src/main/res/xml/file_paths.xml b/src/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..fb653e4
--- /dev/null
+++ b/src/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/res/xml/preferences.xml b/src/app/src/main/res/xml/preferences.xml
similarity index 100%
rename from src/res/xml/preferences.xml
rename to src/app/src/main/res/xml/preferences.xml
diff --git a/src/res/xml/searchable.xml b/src/app/src/main/res/xml/searchable.xml
similarity index 100%
rename from src/res/xml/searchable.xml
rename to src/app/src/main/res/xml/searchable.xml
diff --git a/src/res/xml/widget_2x_info.xml b/src/app/src/main/res/xml/widget_2x_info.xml
similarity index 100%
rename from src/res/xml/widget_2x_info.xml
rename to src/app/src/main/res/xml/widget_2x_info.xml
diff --git a/src/res/xml/widget_4x_info.xml b/src/app/src/main/res/xml/widget_4x_info.xml
similarity index 100%
rename from src/res/xml/widget_4x_info.xml
rename to src/app/src/main/res/xml/widget_4x_info.xml
diff --git a/src/app/src/test/java/net/micode/notes/ExampleUnitTest.java b/src/app/src/test/java/net/micode/notes/ExampleUnitTest.java
new file mode 100644
index 0000000..296adc2
--- /dev/null
+++ b/src/app/src/test/java/net/micode/notes/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package net.micode.notes;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/src/build.gradle.kts b/src/build.gradle.kts
new file mode 100644
index 0000000..3756278
--- /dev/null
+++ b/src/build.gradle.kts
@@ -0,0 +1,4 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ alias(libs.plugins.android.application) apply false
+}
\ No newline at end of file
diff --git a/src/docs/置顶功能类图.md b/src/docs/置顶功能类图.md
new file mode 100644
index 0000000..e69de29
diff --git a/src/docs/置顶功能类图.puml b/src/docs/置顶功能类图.puml
new file mode 100644
index 0000000..e69de29
diff --git a/src/gradle.properties b/src/gradle.properties
new file mode 100644
index 0000000..00d252e
--- /dev/null
+++ b/src/gradle.properties
@@ -0,0 +1,22 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. For more details, visit
+# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
+android.nonFinalResIds=false
\ No newline at end of file
diff --git a/src/gradle/libs.versions.toml b/src/gradle/libs.versions.toml
new file mode 100644
index 0000000..1bcc061
--- /dev/null
+++ b/src/gradle/libs.versions.toml
@@ -0,0 +1,22 @@
+[versions]
+agp = "8.13.1"
+junit = "4.13.2"
+junitVersion = "1.3.0"
+espressoCore = "3.7.0"
+appcompat = "1.7.1"
+material = "1.13.0"
+activity = "1.11.0"
+constraintlayout = "2.2.1"
+
+[libraries]
+junit = { group = "junit", name = "junit", version.ref = "junit" }
+ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
+espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
+appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
+activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
+constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
+
+[plugins]
+android-application = { id = "com.android.application", version.ref = "agp" }
+
diff --git a/src/gradle/wrapper/gradle-wrapper.jar b/src/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..8bdaf60
Binary files /dev/null and b/src/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/src/gradle/wrapper/gradle-wrapper.properties b/src/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..884f9ef
--- /dev/null
+++ b/src/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Wed Nov 19 20:15:31 CST 2025
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/src/gradlew b/src/gradlew
new file mode 100644
index 0000000..ef07e01
--- /dev/null
+++ b/src/gradlew
@@ -0,0 +1,251 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH="\\\"\\\""
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/src/gradlew.bat b/src/gradlew.bat
new file mode 100644
index 0000000..db3a6ac
--- /dev/null
+++ b/src/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/src/local.properties b/src/local.properties
new file mode 100644
index 0000000..349146b
--- /dev/null
+++ b/src/local.properties
@@ -0,0 +1,8 @@
+## This file must *NOT* be checked into Version Control Systems,
+# as it contains information specific to your local configuration.
+#
+# Location of the SDK. This is only used by Gradle.
+# For customization when using a Version Control System, please read the
+# header note.
+#Sat Nov 29 11:00:45 CST 2025
+sdk.dir=D\:\\Android\\Android SDK
diff --git a/src/res/layout/note_list.xml b/src/res/layout/note_list.xml
deleted file mode 100644
index 6b25d38..0000000
--- a/src/res/layout/note_list.xml
+++ /dev/null
@@ -1,58 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/res/values/colors.xml b/src/res/values/colors.xml
deleted file mode 100644
index 123ffbf..0000000
--- a/src/res/values/colors.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
- #335b5b5b
-
diff --git a/src/settings.gradle.kts b/src/settings.gradle.kts
new file mode 100644
index 0000000..bcbfede
--- /dev/null
+++ b/src/settings.gradle.kts
@@ -0,0 +1,23 @@
+pluginManagement {
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "Notes-master"
+include(":app")