Compare commits

...

17 Commits

Author SHA1 Message Date
包尔俊 aff9ac63d1 Merge https://bdgit.educoder.net/p7tupf26b/git into merge-branch
3 months ago
包尔俊 4c3e4cae68 merge
3 months ago
p7tupf26b 795bee106a Merge pull request '将之前没有提交的代码文件提交' (#30) from jiangtianxiang_branch into master
3 months ago
蒋天翔 7922ccee55 新增搜索便签功能
3 months ago
包尔俊 573add61f1 Merge branch 'baoerjun_branch' into merge-branch
3 months ago
p7tupf26b b2a4fbb5ed Merge pull request '新增笔记模板和图片插入功能' (#29) from jiangtianxiang_branch into master
3 months ago
蒋天翔 0089ea3f22 新增笔记模板和插入图片功能
3 months ago
p7tupf26b c871709b17 Merge pull request '待办功能' (#28) from jiangtianxiang_branch into master
3 months ago
蒋天翔 dce63cb3f1 新增待办事项功能
3 months ago
p7tupf26b 0eb9a179f6 Merge pull request '新增功能' (#27) from jiangtianxiang_branch into master
3 months ago
蒋天翔 3277d60c33 新增富文本编辑、主题管理、撤销重做等功能
3 months ago
蒋天翔 d1dd9362cd 新增功能:
3 months ago
蒋天翔 269d287c71 搜索功能
3 months ago
蒋天翔 19721c364f 新增搜索便签功能
3 months ago
mbls3xqnp 2e86cf6334 Merge pull request '迁移至ViewBinding' (#24) from baoerjun_branch into master
3 months ago
mbls3xqnp 9ffb066211 Merge pull request '移除gtask' (#23) from baoerjun_branch into master
3 months ago
mbls3xqnp 64f7651f2d 更新共享开发文档
3 months ago

BIN
.gitignore vendored

Binary file not shown.

@ -3,18 +3,20 @@
## 概述
本文档规划了小米笔记应用的潜在功能扩展,按优先级和时间线组织。核心功能优先,高级功能作为后续迭代。
## 项目当前状态2026-01-21
## 项目当前状态2026-01-28
### 已实现的核心功能 ✅
**基础功能**
- ✅ 笔记创建和编辑
- ✅ 笔记列表显示
- ✅ 笔记创建和编辑(支持富文本)
- ✅ 笔记列表显示(支持左滑菜单操作)
- ✅ 文件夹管理(树形结构、展开收起、面包屑导航)
- ✅ 笔记提醒(闹钟功能)
- ✅ 笔记背景颜色(5种颜色
- ✅ 笔记背景颜色(10种颜色 + 自定义颜色 + 壁纸
- ✅ 笔记字体样式4种大小
- ✅ 本地数据存储SQLite + ContentProvider
- ✅ 便签标题编辑独立TITLE字段
- ✅ 便签重命名(左滑菜单 + 编辑界面)
**高级功能**
- ✅ MVVM架构重构ViewModel + Repository Pattern
@ -27,7 +29,23 @@
- ✅ 搜索功能ContentProvider支持
- ✅ 回收站功能
- ✅ 多语言支持(简体中文、繁体中文、英文)
- ✅ 材料设计UIMaterial Design
- ✅ 材料设计UIMaterial Design 3
- ✅ 待办任务管理TaskListActivity/TaskEditActivity
- ✅ 撤销/重做功能UndoRedoManager
- ✅ 搜索历史管理SearchHistoryManager
**富文本编辑功能**
- ✅ 粗体、斜体、下划线
- ✅ 删除线
- ✅ 标题层级 (H1-H6)
- ✅ 列表(无序、有序)
- ✅ 引用块
- ✅ 代码块
- ✅ 链接插入
- ✅ 分割线
- ✅ 文本颜色
- ✅ 文本背景色
- ✅ 图片插入和缩放
**技术架构**
- ✅ MVVM架构模式
@ -36,62 +54,62 @@
- ✅ ContentProvider标准API
- ✅ SQLiteOpenHelper数据库管理
- ✅ ExecutorService异步操作
- ✅ 48个Java源文件
- ✅ 135个资源文件
- ✅ ViewBinding100%迁移完成)
- ✅ 55个Java源文件
- ✅ 176个资源文件
- ✅ 数据库版本5含10个触发器
### 项目统计
| 类别 | 数量 | 说明 |
|------|-------|------|
| Java源文件 | 48个 | 包括data、ui、viewmodel、model、tool、widget、gtask |
| 资源文件 | 135个 | layout、values、drawable、menu、xml、raw |
| Android组件 | 14个 | 8个Activity、3个Receiver、1个Service、2个Widget |
| Java源文件 | 55个 | UI层28个、数据层7个、ViewModel2个、Model6个、工具7个、Widget3个、其他2个 |
| 资源文件 | 176个 | layout、values、drawable、menu、xml、raw、anim |
| Android组件 | 16个 | 10个Activity、3个Receiver、1个Service、2个Widget |
| 测试文件 | 4个 | 1个单元测试、2个数据层测试、1个集成测试 |
| 数据库表 | 2个 | note表(21字段)、data表(11字段) |
| 数据库表 | 2个 | note表(22字段)、data表(11字段) |
| 系统文件夹 | 4个 | 根(0)、临时(-1)、通话记录(-2)、回收站(-3) |
## 功能分类
### 核心功能 (Phase 1 - 已完成)
- ✅ 笔记创建和编辑
- ✅ 笔记列表显示
- ✅ 笔记创建和编辑(支持富文本)
- ✅ 笔记列表显示(左滑菜单:置顶、锁定、移动、删除、重命名)
- ✅ 文件夹管理(树形结构、面包屑导航)
- ✅ 笔记提醒(闹钟)
- ✅ 笔记背景颜色(黄/红/蓝/绿/白
- ✅ 笔记背景颜色(10种预设 + 自定义 + 壁纸
- ✅ 笔记字体样式(小/中/大/超大)
- ✅ 本地数据存储SQLite + ContentProvider
- ✅ 便签标题编辑独立TITLE字段
- ✅ 便签重命名(左滑菜单 + 编辑界面)
- ✅ 笔记锁定功能
- ✅ 笔记置顶功能
- ✅ 回收站功能
- ✅ 待办任务管理
- ✅ 撤销/重做功能
- ✅ 搜索历史
- ✅ Google Tasks同步
## 短期扩展 (Phase 2 - 1-2个月)
### P0 - 必须实现
#### 2.1 搜索功能增强 ⚠️ 部分实现
#### 2.1 搜索功能增强 ✅ 已实现
**描述**: 提供强大的搜索功能,支持全文搜索、筛选和排序
**当前状态**:
- ✅ 基础搜索功能ContentProvider支持search URI
- ✅ 搜索建议功能
- ❌ 搜索历史记录
- ❌ 高级筛选选项
- ❌ 搜索结果高亮
**待实现功能点**:
- [ ] 搜索历史记录(本地存储常用搜索词)
- [ ] 搜索结果高亮显示
- [ ] 搜索频率排序
- ✅ 搜索历史记录SearchHistoryManager
- ✅ 搜索结果高亮
**技术方案**:
- 使用 SharedPreferences 存储搜索历史
- 使用 SearchHistoryManager 存储搜索历史
- 扩展 NotesRepository 搜索逻辑
- 实现搜索 UI 筛选面板
**优先级**:
**工作量**: 2-3
**优先级**: 已完成
**工作量**: 0
#### 2.2 导入导出功能增强 ✅ 已实现基础版本
**描述**: 支持笔记的导入导出,便于数据迁移和分享
@ -131,20 +149,18 @@
**描述**: 支持简单的笔记撤回操作
**功能点**:
- [ ] 撤回上一次编辑
- [ ] 撤回历史栈可连续撤回10-20次
- [ ] 重做功能
- [ ] 撤回/重做状态提示
- [ ] 清空撤回历史
- ✅ 撤回上一次编辑
- ✅ 撤回历史栈UndoRedoManager
- ✅ 重做功能
- ✅ 撤回/重做状态提示
**技术方案**:
- 实现 UndoStack 数据结构
- 在 NoteEditText 中记录编辑历史
- 实现 UndoRedoManager 管理器
- 使用 Command Pattern 实现撤回逻辑
- 添加撤回/重做 UI 按钮
- 在 NoteEditActivity 中集成撤回/重做按钮
**优先级**:
**工作量**: 2-3
**优先级**: 已完成
**工作量**: 0
### P1 - 应该实现
@ -245,62 +261,68 @@
**优先级**: 低
**工作量**: 3-4天
#### 2.8 快捷操作优化 ⚠️ 部分实现
#### 2.8 快捷操作优化 ✅ 已实现
**描述**: 添加快捷操作,提高效率
**当前状态**:
- ✅ 长按笔记快捷菜单(复制、分享、删除、移动、设置提醒)
- ✅ 桌面小组件2x2, 4x4
- ✅ 左滑菜单(置顶、锁定、移动、删除、重命名)
- ❌ 通知栏快捷操作
- ❌ 文本格式快捷工具栏
**待实现功能点**:
- [ ] 通知栏快捷操作(创建笔记、语音输入)
- [ ] 快捷方式Launcher Shortcuts API
- [ ] 快捷手势(左滑删除、右滑置顶)
**优先级**: 低
**工作量**: 2天
**工作量**: 1-2天
## 中期扩展 (Phase 3 - 3-4个月)
### P1 - 应该实现
#### 3.1 富文本编辑
#### 3.1 富文本编辑 ✅ 已实现
**描述**: 增强文本编辑功能,支持多种格式
**功能点**:
- [ ] 粗体、斜体、下划线
- [ ] 删除线
- [ ] 标题层级 (H1-H6)
- [ ] 列表(无序、有序、检查列表)
- [ ] 引用块
- [ ] 代码块
- [ ] 链接
- [ ] 分割线
- [ ] 文本颜色
- [ ] 文本背景色
- ✅ 粗体、斜体、下划线
- ✅ 删除线
- ✅ 标题层级 (H1-H6)
- ✅ 列表(无序、有序)
- ✅ 引用块
- ✅ 代码块
- ✅ 链接插入
- ✅ 分割线
- ✅ 文本颜色
- ✅ 文本背景色
- ✅ 图片插入和缩放
**技术方案**:
- 集成富文本编辑库(如 RichEditor、SpannableStringBuilder
- 或使用 Markdown 渲染器(如 Markwon
- 实现格式工具栏
- 使用 RichTextHelper 工具类
- 基于 SpannableString 实现富文本
- 实现格式工具栏rich_text_selector
- 扩展 NoteEditText.java 支持富文本
**优先级**:
**工作量**: 4-5
**优先级**: 已完成
**工作量**: 0
#### 3.2 图片附件
#### 3.2 图片附件 ⚠️ 部分实现
**描述**: 支持在笔记中插入图片
**功能点**:
- [ ] 从相册选择图片
**当前状态**:
- ✅ 从相册选择图片
- ✅ 图片插入到笔记
- ✅ 图片缩放(双指缩放)
- ✅ 图片大小调整SeekBar对话框
- ❌ 拍照插入
- ❌ 图片裁剪
- ❌ 图片预览(全屏查看)
- ❌ 图片旋转
**待实现功能点**:
- [ ] 拍照插入
- [ ] 图片裁剪
- [ ] 图片压缩
- [ ] 图片预览(全屏查看)
- [ ] 图片删除
- [ ] 图片大小调整
- [ ] 图片旋转
- [ ] 图片备注
@ -312,7 +334,7 @@
- 创建图片查看器 Activity
**优先级**: 高
**工作量**: 4-5
**工作量**: 3-4
### P2 - 可以实现
@ -369,27 +391,22 @@
**优先级**: 中
**工作量**: 4-5天
#### 3.5 任务清单
**描述**: 在笔记中创建待办事项清单
#### 3.5 任务清单 ✅ 已实现
**描述**: 独立的待办任务管理功能
**功能点**:
- [ ] 添加任务项(- [ ] 语法)
- [ ] 标记完成/未完成
- [ ] 任务优先级(高/中/低)
- [ ] 任务截止日期
- [ ] 任务提醒
- [ ] 任务统计(完成率)
- [ ] 过滤已完成任务
- [ ] 任务拖拽排序
**当前状态**:
- ✅ TaskListActivity/TaskEditActivity 独立页面
- ✅ 任务创建、编辑、删除
- ✅ 任务状态管理(待办/已完成)
- ✅ 任务提醒设置
**技术方案**:
- 扩展 data 表支持任务类型mime_type = "text/x-todo"
- 实现任务 UI 组件TodoItemView
- 扩展 NoteEditText 解析任务语法
- 使用 TYPE_TASK 笔记类型存储任务
- 实现 TaskListAdapter 任务列表适配器
- 集成现有提醒功能
**优先级**:
**工作量**: 3-4
**优先级**: 已完成
**工作量**: 0
## 长期扩展 (Phase 4 - 6个月+)
@ -401,12 +418,13 @@
**用户需求**: 自定义壁纸
**当前状态**:
- ✅ 笔记背景颜色(5种
- ✅ 笔记背景颜色(10种预设
- ✅ 字体大小4种
- ✅ 夜间主题values-night
- ✅ 自定义颜色选择器
- ✅ 自定义壁纸(从相册选择)
- ❌ 完整主题系统
- ❌ 自定义主题颜色
- ❌ 自定义壁纸
- ❌ 预设壁纸库
**待实现功能点**:
- [ ] 多种预设主题
@ -506,14 +524,14 @@
## 功能实现时间线(精简版)
### Month 1-2: 核心功能增强
- [ ] Week 1: 搜索功能增强 (2.1) - 搜索历史、高级筛选
- Week 1: 搜索功能增强 (2.1) - 搜索历史、高级筛选
- [ ] Week 2: 导入导出功能增强 (2.2) - 便签图片导出、Markdown/TXT
- [ ] Week 3: 撤回功能 (2.3) - 撤回/重做
- Week 3: 撤回功能 (2.3) - 撤回/重做
- [ ] Week 4: 标签系统 (2.4) - 标签分类和筛选
### Month 3-4: 用户体验提升
- [ ] Week 5: 笔记模板 (2.6) - 模板管理
- [ ] Week 6-8: 富文本编辑 (3.1) - 完整格式支持
- Week 6-8: 富文本编辑 (3.1) - 完整格式支持
### Month 5-6: 功能扩展
- [ ] Week 9-10: 图片附件 (3.2) - 图片管理和预览
@ -521,7 +539,7 @@
### Month 7-8: 高级功能
- [ ] Week 13-14: 链接笔记 (3.4) - 笔图谱
- [ ] Week 15-16: 任务清单 (3.5) - 任务管理
- Week 15-16: 任务清单 (3.5) - 任务管理
### Month 9+: 智能化和生态
- [ ] Week 17-20: 云同步和账号系统 (4.3) - 注册登录、云同步
@ -743,15 +761,19 @@
6. 稳健的架构设计(可扩展、可测试)
**下一步行动**
1. 实施 Phase 2 P0 功能(搜索增强、便签导出、撤回功能
2. 技术债务清理(ViewBinding、Kotlin 迁移)
1. 实施 Phase 2 P0 功能(便签图片导出、Markdown/TXT导出
2. 技术债务清理Kotlin 迁移考虑
3. 建立自动化测试体系(单元测试、集成测试)
4. 设置监控和分析(崩溃率、使用率)
5. 规划服务端开发(云同步、账号系统)
---
**文档版本**: v3.0(精简版)
**更新日期**: 2026-01-21
**文档版本**: v4.0(精简版)
**更新日期**: 2026-01-28
**维护者**: Sisyphus AI Agent
**更新说明**: 根据用户反馈精简功能列表,移除不必要的生物识别和复杂版本历史,强调便签图片导出、智能识别和云同步功能
**更新说明**:
- 更新项目状态:富文本编辑、撤销/重做、待办任务、搜索历史等功能已完成
- 更新项目统计55个Java源文件、176个资源文件
- 更新ViewBinding迁移状态100%完成
- 更新标题管理统一使用TITLE字段支持编辑界面和左滑重命名

@ -13,3 +13,8 @@
.externalNativeBuild
.cxx
local.properties
build.gradle.kts
gradle.properties
gradlew
gradlew.bat
settings.gradle.kts

@ -15,7 +15,7 @@ android {
defaultConfig {
applicationId = "net.micode.notes"
minSdk = 24
targetSdk = 31
targetSdk = 33
versionCode = 1
versionName = "1.0"
@ -53,7 +53,15 @@ dependencies {
// RecyclerView依赖
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.cursoradapter:cursoradapter:1.0.0")
// Preference依赖
implementation("androidx.preference:preference:1.2.1")
// Palette依赖
implementation("androidx.palette:palette:1.0.0")
testImplementation(libs.junit)
testImplementation("org.mockito:mockito-core:5.7.0")
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)
}

@ -25,6 +25,7 @@
<!-- ==================== 应用配置 ==================== -->
<application
android:name=".NotesApplication"
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
@ -91,6 +92,28 @@
android:resource="@xml/searchable" />
</activity>
<!-- ==================== 搜索活动 ==================== -->
<activity
android:name=".ui.NoteSearchActivity"
android:label="@string/search"
android:launchMode="singleTop"
android:theme="@style/Theme.Notesmaster"
android:windowSoftInputMode="stateVisible|adjustResize"
android:exported="false" />
<!-- ==================== 待办任务列表活动 ==================== -->
<activity
android:name=".ui.TaskListActivity"
android:label="Tasks"
android:theme="@style/Theme.Notesmaster" />
<!-- ==================== 待办任务编辑活动 ==================== -->
<activity
android:name=".ui.TaskEditActivity"
android:label="Edit Task"
android:theme="@style/Theme.Notesmaster"
android:windowSoftInputMode="stateVisible|adjustResize" />
<!-- ==================== 内容提供者 ==================== -->
<!-- 提供笔记数据的访问接口,允许其他应用访问笔记数据 -->
<provider
@ -162,7 +185,7 @@
android:name=".ui.AlarmAlertActivity"
android:label="@string/app_name"
android:launchMode="singleInstance"
android:theme="@android:style/Theme.Holo.Wallpaper.NoTitleBar" >
android:theme="@style/Theme.Notesmaster" >
</activity>
<!-- ==================== 设置活动 ==================== -->
@ -171,14 +194,14 @@
android:name="net.micode.notes.ui.NotesPreferenceActivity"
android:label="@string/preferences_title"
android:launchMode="singleTop"
android:theme="@android:style/Theme.Holo.Light" >
android:theme="@style/Theme.Notesmaster" >
</activity>
<!-- ==================== 密码设置/验证活动 ==================== -->
<activity
android:name=".ui.PasswordActivity"
android:label="Password"
android:theme="@android:style/Theme.Holo.Light.NoActionBar"
android:theme="@style/Theme.Notesmaster"
android:windowSoftInputMode="stateVisible|adjustResize">
</activity>

@ -125,6 +125,13 @@ public class MainActivity extends AppCompatActivity implements SidebarFragment.O
// TODO: 实现导出功能
}
@Override
public void onTemplateSelected() {
Log.d(TAG, "Template selected");
// 跳转到模板列表(在 NotesListActivity 中处理)
closeSidebar();
}
@Override
public void onSettingsSelected() {
Log.d(TAG, "Settings selected");

@ -0,0 +1,18 @@
package net.micode.notes;
import android.app.Application;
import net.micode.notes.data.ThemeRepository;
import com.google.android.material.color.DynamicColors;
public class NotesApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Apply Dynamic Colors (Material You) if available
DynamicColors.applyToActivitiesIfAvailable(this);
// Apply saved theme preference
ThemeRepository repository = new ThemeRepository(this);
ThemeRepository.applyTheme(repository.getThemeMode());
}
}

@ -0,0 +1,56 @@
package net.micode.notes.data;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.widget.TextView;
import androidx.preference.PreferenceManager;
public class FontManager {
public static final String PREF_FONT_FAMILY = "pref_font_family";
private final SharedPreferences mPrefs;
private static FontManager sInstance;
private FontManager(Context context) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public static synchronized FontManager getInstance(Context context) {
if (sInstance == null) {
sInstance = new FontManager(context.getApplicationContext());
}
return sInstance;
}
public void applyFont(TextView textView) {
String fontValue = mPrefs.getString(PREF_FONT_FAMILY, "default");
Typeface typeface = getTypeface(fontValue);
if (typeface != null) {
textView.setTypeface(typeface);
} else {
textView.setTypeface(Typeface.DEFAULT);
}
}
private Typeface getTypeface(String fontValue) {
switch (fontValue) {
case "serif":
return Typeface.SERIF;
case "sans-serif":
return Typeface.SANS_SERIF;
case "monospace":
return Typeface.MONOSPACE;
case "cursive":
// Android doesn't have a built-in cursive typeface constant,
// but we can try to load sans-serif-light or similar as a placeholder,
// or load from assets if we had custom fonts.
// For now, let's map it to serif-italic style if possible or just serif.
return Typeface.create(Typeface.SERIF, Typeface.ITALIC);
case "default":
default:
return Typeface.DEFAULT;
}
}
}

@ -57,6 +57,10 @@ public class Notes {
*
*/
public static final int TYPE_SYSTEM = 2;
/**
*
*/
public static final int TYPE_TASK = 3;
/**
* ID
@ -77,6 +81,10 @@ public class Notes {
* ID
*/
public static final int ID_TRASH_FOLER = -3;
/**
* Template folder ID
*/
public static final int ID_TEMPLATE_FOLDER = -4;
/**
* Intent Extra
@ -253,6 +261,30 @@ public class Notes {
* <P> Type : TEXT </P>
*/
public static final String TITLE = "title";
/**
* Task Priority: 0=Low/None, 1=Medium, 2=High
* <P> Type : INTEGER </P>
*/
public static final String GTASK_PRIORITY = "gtask_priority";
/**
* Task Due Date (Timestamp)
* <P> Type : INTEGER (long) </P>
*/
public static final String GTASK_DUE_DATE = "gtask_due_date";
/**
* Task Status: 0=Active, 1=Completed
* <P> Type : INTEGER </P>
*/
public static final String GTASK_STATUS = "gtask_status";
/**
* Task Finished Time (Timestamp)
* <P> Type : INTEGER (long) </P>
*/
public static final String GTASK_FINISHED_TIME = "gtask_finished_time";
}
public interface DataColumns {

@ -70,7 +70,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
* onUpgrade
* </p>
*/
private static final int DB_VERSION = 8;
private static final int DB_VERSION = 10;
/**
*
@ -427,6 +427,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
values.put(NoteColumns.ID, Notes.ID_TRASH_FOLER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
/**
* create template folder
*/
// 创建模板文件夹
createTemplateFolder(db);
}
/**
@ -493,6 +499,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
public void onCreate(SQLiteDatabase db) {
createNoteTable(db);
createDataTable(db);
createPresetTemplates(db);
}
/**
@ -559,6 +566,18 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion++;
}
// 从V8升级到V9
if (oldVersion == 8) {
upgradeToV9(db);
oldVersion++;
}
// 从V9升级到V10
if (oldVersion == 9) {
upgradeToV10(db);
oldVersion++;
}
// 如果需要,重新创建触发器
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
@ -630,6 +649,30 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
+ " TEXT NOT NULL DEFAULT ''");
Log.i(TAG, "Fixed: Added missing TITLE column in onOpen");
}
// Check for missing GTASK columns
boolean hasGTaskColumns = false;
if (cursor != null) {
if (cursor.getColumnIndex(NoteColumns.GTASK_PRIORITY) != -1) {
hasGTaskColumns = true;
}
}
if (!hasGTaskColumns) {
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_PRIORITY
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_DUE_DATE
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_STATUS
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_FINISHED_TIME
+ " INTEGER NOT NULL DEFAULT 0");
Log.i(TAG, "Fixed: Added missing GTASK columns in onOpen");
} catch (Exception e) {
Log.e(TAG, "Failed to add GTASK columns in onOpen", e);
}
}
if (cursor != null) {
cursor.close();
@ -760,4 +803,111 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
Log.e(TAG, "Failed to add TITLE column in V8 upgrade (it probably already exists)", e);
}
}
/**
* V9
* <p>
* GTASK
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV9(SQLiteDatabase db) {
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_PRIORITY
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_DUE_DATE
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_STATUS
+ " INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.GTASK_FINISHED_TIME
+ " INTEGER NOT NULL DEFAULT 0");
} catch (Exception e) {
Log.e(TAG, "Failed to add GTASK columns in V9 upgrade", e);
}
}
/**
* V10
* <p>
*
* </p>
*
* @param db SQLiteDatabase
*/
private void upgradeToV10(SQLiteDatabase db) {
createTemplateFolder(db);
createPresetTemplates(db);
}
/**
*
*
* @param db SQLiteDatabase
*/
private void createTemplateFolder(SQLiteDatabase db) {
ContentValues values = new ContentValues();
values.put(NoteColumns.ID, Notes.ID_TEMPLATE_FOLDER);
values.put(NoteColumns.TYPE, Notes.TYPE_SYSTEM);
db.insert(TABLE.NOTE, null, values);
}
/**
*
*
* @param db SQLiteDatabase
*/
private void createPresetTemplates(SQLiteDatabase db) {
// 工作模板
long workFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "工作");
if (workFolderId > 0) {
insertNote(db, workFolderId, "会议记录", "会议主题:\n时间\n地点\n参会人\n\n会议内容\n\n行动项\n");
insertNote(db, workFolderId, "周报", "本周工作总结:\n1. \n2. \n\n下周工作计划\n1. \n2. \n\n需要协调的问题\n");
}
// 生活模板
long lifeFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "生活");
if (lifeFolderId > 0) {
insertNote(db, lifeFolderId, "日记", "日期:\n天气\n心情\n\n正文\n");
insertNote(db, lifeFolderId, "购物清单", "1. \n2. \n3. \n");
}
// 学习模板
long studyFolderId = insertFolder(db, Notes.ID_TEMPLATE_FOLDER, "学习");
if (studyFolderId > 0) {
insertNote(db, studyFolderId, "读书笔记", "书名:\n作者\n\n核心观点\n\n精彩摘录\n\n读后感\n");
}
}
private long insertFolder(SQLiteDatabase db, long parentId, String name) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, parentId);
values.put(NoteColumns.TYPE, Notes.TYPE_FOLDER);
values.put(NoteColumns.SNIPPET, name);
values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis());
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
values.put(NoteColumns.NOTES_COUNT, 0);
return db.insert(TABLE.NOTE, null, values);
}
private void insertNote(SQLiteDatabase db, long parentId, String title, String content) {
ContentValues values = new ContentValues();
values.put(NoteColumns.PARENT_ID, parentId);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis());
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
values.put(NoteColumns.SNIPPET, content); // SNIPPET acts as content preview or full content for simple notes
values.put(NoteColumns.TITLE, title); // Assuming V8+ has TITLE
long noteId = db.insert(TABLE.NOTE, null, values);
if (noteId > 0) {
ContentValues dataValues = new ContentValues();
dataValues.put(DataColumns.NOTE_ID, noteId);
dataValues.put(DataColumns.MIME_TYPE, DataConstants.NOTE);
dataValues.put(DataColumns.CONTENT, content);
dataValues.put(NoteColumns.CREATED_DATE, System.currentTimeMillis());
dataValues.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
db.insert(TABLE.DATA, null, dataValues);
}
}
}

@ -240,13 +240,13 @@ public class NotesRepository {
String[] selectionArgs;
if (folderId == Notes.ID_ROOT_FOLDER) {
// 根文件夹:显示所有文件夹和便签
selection = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.PARENT_ID + "=?) OR (" +
// 根文件夹:显示所有文件夹和便签 (排除系统文件夹和待办任务)
selection = "(" + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.TYPE + "<>" + Notes.TYPE_TASK + " AND " + NoteColumns.PARENT_ID + "=?) OR (" +
NoteColumns.ID + "=" + Notes.ID_CALL_RECORD_FOLDER + " AND " + NoteColumns.NOTES_COUNT + ">0)";
selectionArgs = new String[]{String.valueOf(Notes.ID_ROOT_FOLDER)};
} else {
// 子文件夹:显示该文件夹下的文件夹和便签
selection = NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM;
// 子文件夹:显示该文件夹下的文件夹和便签 (排除系统文件夹和待办任务)
selection = NoteColumns.PARENT_ID + "=? AND " + NoteColumns.TYPE + "<>" + Notes.TYPE_SYSTEM + " AND " + NoteColumns.TYPE + "<>" + Notes.TYPE_TASK;
selectionArgs = new String[]{String.valueOf(folderId)};
}
@ -314,6 +314,15 @@ public class NotesRepository {
return root;
}
if (folderId == Notes.ID_TEMPLATE_FOLDER) {
NoteInfo root = new NoteInfo();
root.id = Notes.ID_TEMPLATE_FOLDER;
root.title = "笔记模板";
root.snippet = "笔记模板";
root.type = Notes.TYPE_FOLDER; // Treat as folder for UI
return root;
}
String selection = NoteColumns.ID + "=?";
String[] selectionArgs = new String[]{String.valueOf(folderId)};
@ -748,13 +757,15 @@ public class NotesRepository {
return;
}
String selection = "(" + NoteColumns.TYPE + " = ?) AND (" +
String selection = "(" + NoteColumns.TYPE + " <> ?) AND (" +
NoteColumns.TITLE + " LIKE ? OR " +
NoteColumns.SNIPPET + " LIKE ? OR " +
NoteColumns.ID + " IN (SELECT " + DataColumns.NOTE_ID +
" FROM data WHERE " + DataColumns.CONTENT + " LIKE ?))";
String[] selectionArgs = new String[]{
String.valueOf(Notes.TYPE_NOTE),
String.valueOf(Notes.TYPE_SYSTEM),
"%" + keyword + "%",
"%" + keyword + "%",
"%" + keyword + "%"
};
@ -1083,6 +1094,8 @@ public class NotesRepository {
}
ContentValues values = new ContentValues();
// 同时更新 TITLE 和 SNIPPET保持一致性
values.put(NoteColumns.TITLE, newName);
values.put(NoteColumns.SNIPPET, newName);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
@ -1197,4 +1210,152 @@ public class NotesRepository {
Log.d(TAG, "Executor shutdown");
}
}
/**
*
*
* @param templateId ID
* @param targetFolderId ID
* @param callback
*/
public void applyTemplate(long templateId, long targetFolderId, Callback<Long> callback) {
executor.execute(() -> {
try {
// 1. 获取模板内容
String content = getNoteContent(templateId);
String title = getNoteTitle(templateId);
// 2. 创建新笔记
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.PARENT_ID, targetFolderId);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.CREATED_DATE, currentTime);
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.SNIPPET, extractSnippet(content));
values.put(NoteColumns.TITLE, title); // Copy title (or maybe empty?)
Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values);
Long newNoteId = 0L;
if (uri != null) {
newNoteId = ContentUris.parseId(uri);
}
if (newNoteId > 0) {
// 3. 插入内容
ContentValues dataValues = new ContentValues();
dataValues.put(DataColumns.NOTE_ID, newNoteId);
dataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
dataValues.put(DataColumns.CONTENT, content);
dataValues.put(NoteColumns.CREATED_DATE, currentTime);
dataValues.put(NoteColumns.MODIFIED_DATE, currentTime);
contentResolver.insert(Notes.CONTENT_DATA_URI, dataValues);
callback.onSuccess(newNoteId);
} else {
callback.onError(new RuntimeException("Failed to create note from template"));
}
} catch (Exception e) {
callback.onError(e);
}
});
}
/**
*
*
* @param sourceNoteId ID
* @param categoryId ID
* @param templateName
* @param callback
*/
public void createTemplate(long sourceNoteId, long categoryId, String templateName, Callback<Long> callback) {
executor.execute(() -> {
try {
// 1. 获取源内容
String content = getNoteContent(sourceNoteId);
// 2. 创建模板笔记
ContentValues values = new ContentValues();
long currentTime = System.currentTimeMillis();
values.put(NoteColumns.PARENT_ID, categoryId);
values.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
values.put(NoteColumns.CREATED_DATE, currentTime);
values.put(NoteColumns.MODIFIED_DATE, currentTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
values.put(NoteColumns.SNIPPET, extractSnippet(content));
values.put(NoteColumns.TITLE, templateName);
Uri uri = contentResolver.insert(Notes.CONTENT_NOTE_URI, values);
Long newNoteId = 0L;
if (uri != null) {
newNoteId = ContentUris.parseId(uri);
}
if (newNoteId > 0) {
// 3. 插入内容
ContentValues dataValues = new ContentValues();
dataValues.put(DataColumns.NOTE_ID, newNoteId);
dataValues.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
dataValues.put(DataColumns.CONTENT, content);
dataValues.put(NoteColumns.CREATED_DATE, currentTime);
dataValues.put(NoteColumns.MODIFIED_DATE, currentTime);
contentResolver.insert(Notes.CONTENT_DATA_URI, dataValues);
callback.onSuccess(newNoteId);
} else {
callback.onError(new RuntimeException("Failed to create template"));
}
} catch (Exception e) {
callback.onError(e);
}
});
}
private String getNoteContent(long noteId) {
String content = "";
Cursor cursor = contentResolver.query(
Notes.CONTENT_DATA_URI,
new String[]{DataColumns.CONTENT},
DataColumns.NOTE_ID + " = ? AND " + DataColumns.MIME_TYPE + " = ?",
new String[]{String.valueOf(noteId), TextNote.CONTENT_ITEM_TYPE},
null
);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
content = cursor.getString(0);
}
} finally {
cursor.close();
}
}
return content;
}
private String getNoteTitle(long noteId) {
String title = "";
Cursor cursor = contentResolver.query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.TITLE},
NoteColumns.ID + " = ?",
new String[]{String.valueOf(noteId)},
null
);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
title = cursor.getString(0);
}
} finally {
cursor.close();
}
}
return title;
}
}

@ -0,0 +1,43 @@
package net.micode.notes.data;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.preference.PreferenceManager;
public class ThemeRepository {
private static final String PREF_THEME_MODE = "pref_theme_mode";
public static final String THEME_MODE_SYSTEM = "system";
public static final String THEME_MODE_LIGHT = "light";
public static final String THEME_MODE_DARK = "dark";
private final SharedPreferences mPrefs;
public ThemeRepository(Context context) {
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
}
public String getThemeMode() {
return mPrefs.getString(PREF_THEME_MODE, THEME_MODE_SYSTEM);
}
public void setThemeMode(String mode) {
mPrefs.edit().putString(PREF_THEME_MODE, mode).apply();
applyTheme(mode);
}
public static void applyTheme(String mode) {
switch (mode) {
case THEME_MODE_LIGHT:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
break;
case THEME_MODE_DARK:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
break;
case THEME_MODE_SYSTEM:
default:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
break;
}
}
}

@ -0,0 +1,6 @@
package net.micode.notes.model;
public interface Command {
void execute();
void undo();
}

@ -0,0 +1,40 @@
package net.micode.notes.model;
import android.text.Editable;
import android.widget.EditText;
public class NoteCommand implements Command {
private final EditText mEditor;
private final int mStart;
private final CharSequence mBefore;
private final CharSequence mAfter;
public NoteCommand(EditText editor, int start, CharSequence before, CharSequence after) {
mEditor = editor;
mStart = start;
mBefore = before.toString();
mAfter = after.toString();
}
@Override
public void execute() {
// Redo: replace 'before' with 'after'
Editable text = mEditor.getText();
int end = mStart + mBefore.length();
if (end <= text.length()) {
text.replace(mStart, end, mAfter);
mEditor.setSelection(mStart + mAfter.length());
}
}
@Override
public void undo() {
// Undo: replace 'after' with 'before'
Editable text = mEditor.getText();
int end = mStart + mAfter.length();
if (end <= text.length()) {
text.replace(mStart, end, mBefore);
mEditor.setSelection(mStart + mBefore.length());
}
}
}

@ -0,0 +1,137 @@
package net.micode.notes.model;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.DataColumns;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.data.Notes.TextNote;
public class Task {
private static final String TAG = "Task";
public long id;
public String snippet; // Content
public long createdDate;
public long modifiedDate;
public int priority; // 0=Low, 1=Mid, 2=High
public long dueDate;
public int status; // 0=Active, 1=Completed
public long finishedTime;
public long alertDate;
public static final int PRIORITY_LOW = 0;
public static final int PRIORITY_NORMAL = 1;
public static final int PRIORITY_HIGH = 2;
public static final int STATUS_ACTIVE = 0;
public static final int STATUS_COMPLETED = 1;
public Task() {
id = 0;
snippet = "";
createdDate = System.currentTimeMillis();
modifiedDate = System.currentTimeMillis();
priority = PRIORITY_LOW;
dueDate = 0;
status = STATUS_ACTIVE;
finishedTime = 0;
alertDate = 0;
}
public static Task fromCursor(Cursor cursor) {
Task task = new Task();
// Use getColumnIndex instead of getColumnIndexOrThrow for safety
int idxId = cursor.getColumnIndex(NoteColumns.ID);
if (idxId != -1) task.id = cursor.getLong(idxId);
int idxSnippet = cursor.getColumnIndex(NoteColumns.SNIPPET);
if (idxSnippet != -1) task.snippet = cursor.getString(idxSnippet);
int idxCreated = cursor.getColumnIndex(NoteColumns.CREATED_DATE);
if (idxCreated != -1) task.createdDate = cursor.getLong(idxCreated);
int idxModified = cursor.getColumnIndex(NoteColumns.MODIFIED_DATE);
if (idxModified != -1) task.modifiedDate = cursor.getLong(idxModified);
int idxAlert = cursor.getColumnIndex(NoteColumns.ALERTED_DATE);
if (idxAlert != -1) task.alertDate = cursor.getLong(idxAlert);
int idxPriority = cursor.getColumnIndex(NoteColumns.GTASK_PRIORITY);
if (idxPriority != -1) task.priority = cursor.getInt(idxPriority);
int idxDueDate = cursor.getColumnIndex(NoteColumns.GTASK_DUE_DATE);
if (idxDueDate != -1) task.dueDate = cursor.getLong(idxDueDate);
int idxStatus = cursor.getColumnIndex(NoteColumns.GTASK_STATUS);
if (idxStatus != -1) task.status = cursor.getInt(idxStatus);
int idxFinished = cursor.getColumnIndex(NoteColumns.GTASK_FINISHED_TIME);
if (idxFinished != -1) task.finishedTime = cursor.getLong(idxFinished);
return task;
}
public Uri save(Context context) {
ContentValues values = new ContentValues();
values.put(NoteColumns.TYPE, Notes.TYPE_TASK);
values.put(NoteColumns.MODIFIED_DATE, System.currentTimeMillis());
values.put(NoteColumns.ALERTED_DATE, alertDate);
values.put(NoteColumns.GTASK_PRIORITY, priority);
values.put(NoteColumns.GTASK_DUE_DATE, dueDate);
values.put(NoteColumns.GTASK_STATUS, status);
values.put(NoteColumns.GTASK_FINISHED_TIME, finishedTime);
values.put(NoteColumns.LOCAL_MODIFIED, 1);
// Ensure snippet is updated in note table too, though trigger might handle it,
// explicit update is safer if trigger fails or data table logic changes.
values.put(NoteColumns.SNIPPET, snippet);
if (id == 0) {
values.put(NoteColumns.CREATED_DATE, System.currentTimeMillis());
values.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
Uri uri = context.getContentResolver().insert(Notes.CONTENT_NOTE_URI, values);
if (uri != null) {
id = ContentUris.parseId(uri);
updateData(context);
}
return uri;
} else {
context.getContentResolver().update(
ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id),
values, null, null
);
updateData(context);
return ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, id);
}
}
private void updateData(Context context) {
ContentValues values = new ContentValues();
values.put(DataColumns.NOTE_ID, id);
values.put(DataColumns.MIME_TYPE, TextNote.CONTENT_ITEM_TYPE);
values.put(DataColumns.CONTENT, snippet);
values.put(DataColumns.MODIFIED_DATE, System.currentTimeMillis());
Cursor c = context.getContentResolver().query(Notes.CONTENT_DATA_URI, new String[]{DataColumns.ID},
DataColumns.NOTE_ID + "=?", new String[]{String.valueOf(id)}, null);
if (c != null) {
if (c.moveToFirst()) {
long dataId = c.getLong(0);
context.getContentResolver().update(ContentUris.withAppendedId(Notes.CONTENT_DATA_URI, dataId), values, null, null);
} else {
context.getContentResolver().insert(Notes.CONTENT_DATA_URI, values);
}
c.close();
} else {
context.getContentResolver().insert(Notes.CONTENT_DATA_URI, values);
}
}
}

@ -0,0 +1,49 @@
package net.micode.notes.model;
import java.util.Stack;
public class UndoRedoManager {
private static final int MAX_STACK_SIZE = 20;
private final Stack<Command> mUndoStack = new Stack<>();
private final Stack<Command> mRedoStack = new Stack<>();
public void addCommand(Command command) {
mUndoStack.push(command);
if (mUndoStack.size() > MAX_STACK_SIZE) {
mUndoStack.remove(0);
}
mRedoStack.clear();
}
public void undo() {
if (!mUndoStack.isEmpty()) {
Command command = mUndoStack.pop();
command.undo();
mRedoStack.push(command);
}
}
public void redo() {
if (!mRedoStack.isEmpty()) {
Command command = mRedoStack.pop();
command.execute();
mUndoStack.push(command);
if (mUndoStack.size() > MAX_STACK_SIZE) {
mUndoStack.remove(0);
}
}
}
public boolean canUndo() {
return !mUndoStack.isEmpty();
}
public boolean canRedo() {
return !mRedoStack.isEmpty();
}
public void clear() {
mUndoStack.clear();
mRedoStack.clear();
}
}

@ -377,6 +377,7 @@ public class WorkingNote {
*/
public void setTitle(String title) {
mTitle = title;
mNote.setNoteValue(NoteColumns.TITLE, mTitle);
}
public String getTitle() {
@ -427,6 +428,27 @@ public class WorkingNote {
}
}
// Wallpaper Support
private String mWallpaperPath;
public void setWallpaper(String path) {
mWallpaperPath = path;
// Ideally we should save this to DB, but for now we might use shared prefs or a separate table
// Or reuse bg_color_id with a special flag if we want to stick to existing schema strictly?
// Better: store in a new column or reuse a data column if possible.
// Given existing schema, let's use DataColumns.DATA5 if available? No DATA5.
// Let's use a SharedPreference for mapping noteId -> wallpaperPath for now to avoid schema migration complexity in this step.
// Or just use a special negative color ID range for wallpapers?
// Actually, let's use a separate storage for wallpapers map: note_id -> uri string
if (mNoteSettingStatusListener != null) {
mNoteSettingStatusListener.onBackgroundColorChanged(); // Reuse this to trigger refresh
}
}
public String getWallpaperPath() {
return mWallpaperPath;
}
/**
*
* <p>

@ -1,19 +1,3 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.tool;
import android.content.ContentProviderOperation;
@ -414,6 +398,32 @@ public class DataUtils {
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
* ID
*
* @param resolver ContentResolver
* @param noteId ID
* @return
* @throws IllegalArgumentException
*/
public static int getNoteTypeById(ContentResolver resolver, long noteId) {
Cursor cursor = resolver.query(Notes.CONTENT_NOTE_URI,
new String [] { NoteColumns.TYPE },
NoteColumns.ID + "=?",
new String [] { String.valueOf(noteId)},
null);
if (cursor != null) {
int type = -1;
if (cursor.moveToFirst()) {
type = cursor.getInt(0);
}
cursor.close();
return type;
}
throw new IllegalArgumentException("Note is not found with id: " + noteId);
}
/**
*

@ -45,10 +45,40 @@ public class ResourceParser {
/** 红色背景 */
public static final int RED = 4;
// New Presets
public static final int MIDNIGHT_BLACK = 5;
public static final int EYE_CARE_GREEN = 6;
public static final int WARM = 7;
public static final int COOL = 8;
/** 自定义颜色按钮 ID (用于 UI 显示) */
public static final int CUSTOM_COLOR_BUTTON_ID = -100;
/** 壁纸按钮 ID (用于 UI 显示) */
public static final int WALLPAPER_BUTTON_ID = -101;
/** 默认背景颜色 */
public static final int BG_DEFAULT_COLOR = YELLOW;
public static int getNoteBgColor(Context context, int id) {
if (id < 0) {
return id; // Custom color (ARGB)
}
switch (id) {
case YELLOW: return context.getColor(R.color.bg_yellow);
case BLUE: return context.getColor(R.color.bg_blue);
case WHITE: return context.getColor(R.color.bg_white);
case GREEN: return context.getColor(R.color.bg_green);
case RED: return context.getColor(R.color.bg_red);
case MIDNIGHT_BLACK: return context.getColor(R.color.bg_midnight_black);
case EYE_CARE_GREEN: return context.getColor(R.color.bg_eye_care_green);
case WARM: return context.getColor(R.color.bg_warm);
case COOL: return context.getColor(R.color.bg_cool);
default: return context.getColor(R.color.bg_white);
}
}
/** 小号字体 */
public static final int TEXT_SMALL = 0;
@ -97,6 +127,9 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgResource(int id) {
if (id >= BG_EDIT_RESOURCES.length || id < 0) {
return R.drawable.edit_white;
}
return BG_EDIT_RESOURCES[id];
}
@ -107,6 +140,9 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteTitleBgResource(int id) {
if (id >= BG_EDIT_TITLE_RESOURCES.length || id < 0) {
return R.drawable.edit_title_white;
}
return BG_EDIT_TITLE_RESOURCES[id];
}
}
@ -182,6 +218,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgFirstRes(int id) {
if (id >= BG_FIRST_RESOURCES.length || id < 0) return R.drawable.list_white_up;
return BG_FIRST_RESOURCES[id];
}
@ -192,6 +229,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgLastRes(int id) {
if (id >= BG_LAST_RESOURCES.length || id < 0) return R.drawable.list_white_down;
return BG_LAST_RESOURCES[id];
}
@ -202,6 +240,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgSingleRes(int id) {
if (id >= BG_SINGLE_RESOURCES.length || id < 0) return R.drawable.list_white_single;
return BG_SINGLE_RESOURCES[id];
}
@ -212,6 +251,7 @@ public class ResourceParser {
* @return ID
*/
public static int getNoteBgNormalRes(int id) {
if (id >= BG_NORMAL_RESOURCES.length || id < 0) return R.drawable.list_white_middle;
return BG_NORMAL_RESOURCES[id];
}

@ -0,0 +1,416 @@
package net.micode.notes.tool;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Typeface;
import android.text.Editable;
import android.text.Html;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.text.style.BulletSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.QuoteSpan;
import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
import android.text.style.URLSpan;
import android.text.style.UnderlineSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
public class RichTextHelper {
private static final String TAG = "RichTextHelper";
public static class NoteImageGetter implements Html.ImageGetter {
private Context mContext;
public NoteImageGetter(Context context) {
mContext = context;
}
@Override
public Drawable getDrawable(String source) {
if (TextUtils.isEmpty(source)) {
return null;
}
try {
Uri uri = Uri.parse(source);
String path = uri.getPath();
if (path == null) {
return null;
}
// Parse dimensions from fragment
int targetWidth = -1;
int targetHeight = -1;
String fragment = uri.getFragment();
if (fragment != null) {
String[] params = fragment.split("&");
for (String param : params) {
String[] pair = param.split("=");
if (pair.length == 2) {
if ("w".equals(pair[0])) {
targetWidth = Integer.parseInt(pair[1]);
} else if ("h".equals(pair[0])) {
targetHeight = Integer.parseInt(pair[1]);
}
}
}
}
// Check if it's a content URI or file path
// For simplicity in this project, we assume we saved it as file:// path
// But source might come as /data/user/0/...
// Decode bitmap with resizing
// Calculate max width (e.g., screen width - padding)
// For simplicity, let's assume a fixed max width or display metrics
int maxWidth = mContext.getResources().getDisplayMetrics().widthPixels - 40;
Bitmap bitmap = decodeSampledBitmapFromFile(path, maxWidth, maxWidth);
if (bitmap != null) {
BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);
if (targetWidth > 0 && targetHeight > 0) {
// Use saved dimensions
drawable.setBounds(0, 0, targetWidth, targetHeight);
} else {
// Use default intrinsic dimensions
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
}
return drawable;
}
} catch (Exception e) {
Log.e(TAG, "Failed to load image: " + source, e);
}
return null;
}
private Bitmap decodeSampledBitmapFromFile(String pathName, int reqWidth, int reqHeight) {
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(pathName, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(pathName, options);
}
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
public static void applyBold(EditText editText) {
applyStyleSpan(editText, Typeface.BOLD);
}
public static void applyItalic(EditText editText) {
applyStyleSpan(editText, Typeface.ITALIC);
}
public static void applyUnderline(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
UnderlineSpan[] spans = editable.getSpans(start, end, UnderlineSpan.class);
if (spans != null && spans.length > 0) {
for (UnderlineSpan span : spans) {
editable.removeSpan(span);
}
} else {
editable.setSpan(new UnderlineSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyStrikethrough(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
StrikethroughSpan[] spans = editable.getSpans(start, end, StrikethroughSpan.class);
if (spans != null && spans.length > 0) {
for (StrikethroughSpan span : spans) {
editable.removeSpan(span);
}
} else {
editable.setSpan(new StrikethroughSpan(), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
private static void applyStyleSpan(EditText editText, int style) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
StyleSpan[] spans = editable.getSpans(start, end, StyleSpan.class);
boolean exists = false;
for (StyleSpan span : spans) {
if (span.getStyle() == style) {
editable.removeSpan(span);
exists = true;
}
}
if (!exists) {
editable.setSpan(new StyleSpan(style), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyHeading(EditText editText, int level) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
// Expand to full line
Editable text = editText.getText();
String string = text.toString();
// Find line start and end
int lineStart = string.lastIndexOf('\n', start - 1) + 1;
if (lineStart < 0) lineStart = 0;
int lineEnd = string.indexOf('\n', end);
if (lineEnd < 0) lineEnd = string.length();
// Remove existing heading spans
RelativeSizeSpan[] sizeSpans = text.getSpans(lineStart, lineEnd, RelativeSizeSpan.class);
for (RelativeSizeSpan span : sizeSpans) {
text.removeSpan(span);
}
StyleSpan[] styleSpans = text.getSpans(lineStart, lineEnd, StyleSpan.class);
for (StyleSpan span : styleSpans) {
if (span.getStyle() == Typeface.BOLD) {
text.removeSpan(span);
}
}
if (level > 0) {
float scale = 1.0f;
switch (level) {
case 1: scale = 2.0f; break;
case 2: scale = 1.5f; break;
case 3: scale = 1.25f; break;
case 4: scale = 1.1f; break;
case 5: scale = 1.0f; break;
case 6: scale = 0.8f; break;
}
text.setSpan(new RelativeSizeSpan(scale), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
text.setSpan(new StyleSpan(Typeface.BOLD), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyBullet(EditText editText) {
// Simple bullet implementation for now
int start = editText.getSelectionStart();
Editable text = editText.getText();
String string = text.toString();
int lineStart = string.lastIndexOf('\n', start - 1) + 1;
if (lineStart < 0) lineStart = 0;
// Check if already bulleted
// Note: BulletSpan covers a paragraph.
int lineEnd = string.indexOf('\n', start);
if (lineEnd < 0) lineEnd = string.length();
BulletSpan[] spans = text.getSpans(lineStart, lineEnd, BulletSpan.class);
if (spans != null && spans.length > 0) {
for (BulletSpan span : spans) {
text.removeSpan(span);
}
} else {
text.setSpan(new BulletSpan(20, Color.BLACK), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyQuote(EditText editText) {
int start = editText.getSelectionStart();
Editable text = editText.getText();
String string = text.toString();
int lineStart = string.lastIndexOf('\n', start - 1) + 1;
if (lineStart < 0) lineStart = 0;
int lineEnd = string.indexOf('\n', start);
if (lineEnd < 0) lineEnd = string.length();
QuoteSpan[] spans = text.getSpans(lineStart, lineEnd, QuoteSpan.class);
if (spans != null && spans.length > 0) {
for (QuoteSpan span : spans) {
text.removeSpan(span);
}
} else {
text.setSpan(new QuoteSpan(Color.GRAY), lineStart, lineEnd, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void applyCode(EditText editText) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
TypefaceSpan[] spans = editable.getSpans(start, end, TypefaceSpan.class);
boolean exists = false;
for (TypefaceSpan span : spans) {
if ("monospace".equals(span.getFamily())) {
editable.removeSpan(span);
exists = true;
}
}
// Also toggle background color for code block look
BackgroundColorSpan[] bgSpans = editable.getSpans(start, end, BackgroundColorSpan.class);
for (BackgroundColorSpan span : bgSpans) {
if (span.getBackgroundColor() == 0xFFEEEEEE) {
editable.removeSpan(span);
}
}
if (!exists) {
editable.setSpan(new TypefaceSpan("monospace"), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
editable.setSpan(new BackgroundColorSpan(0xFFEEEEEE), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void insertLink(Context context, final EditText editText) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle("Insert Link");
final EditText input = new EditText(context);
input.setHint("http://example.com");
builder.setView(input);
builder.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
String url = input.getText().toString();
if (!TextUtils.isEmpty(url)) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start == end) {
// Insert url as text
editText.getText().insert(start, url);
end = start + url.length();
}
editText.getText().setSpan(new URLSpan(url), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
});
builder.setNegativeButton("Cancel", null);
builder.show();
}
public static void applyColor(EditText editText, int color, boolean isBackground) {
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Editable editable = editText.getText();
if (isBackground) {
BackgroundColorSpan[] spans = editable.getSpans(start, end, BackgroundColorSpan.class);
for (BackgroundColorSpan span : spans) editable.removeSpan(span);
editable.setSpan(new BackgroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
ForegroundColorSpan[] spans = editable.getSpans(start, end, ForegroundColorSpan.class);
for (ForegroundColorSpan span : spans) editable.removeSpan(span);
editable.setSpan(new ForegroundColorSpan(color), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static void insertDivider(EditText editText) {
int start = editText.getSelectionStart();
editText.getText().insert(start, "\n-------------------\n");
}
public static void insertImage(EditText editText, String imagePath) {
// Remove existing fragment if any
if (imagePath.contains("#")) {
imagePath = imagePath.substring(0, imagePath.indexOf("#"));
}
// Default: no size specified, use intrinsic
String html = "<img src=\"" + imagePath + "\">";
int start = editText.getSelectionStart();
int end = editText.getSelectionEnd();
if (start > end) { int temp = start; start = end; end = temp; }
Spanned spanned = Html.fromHtml(html, new NoteImageGetter(editText.getContext()), null);
editText.getText().replace(start, end, spanned);
// Insert a newline after image for easier typing
editText.getText().insert(start + spanned.length(), "\n");
}
public static void updateImageSpanSize(EditText editText, android.text.style.ImageSpan span, int width, int height) {
Editable editable = editText.getText();
int start = editable.getSpanStart(span);
int end = editable.getSpanEnd(span);
if (start < 0 || end < 0) return; // Span not found
String source = span.getSource();
if (source == null) return;
// Remove old fragment
if (source.contains("#")) {
source = source.substring(0, source.indexOf("#"));
}
// Append new dimensions
String newSource = source + "#w=" + width + "&h=" + height;
String html = "<img src=\"" + newSource + "\">";
// Create new span with updated source
Spanned newSpanned = Html.fromHtml(html, new NoteImageGetter(editText.getContext()), null);
// We only want the ImageSpan, not the whole Spanned (which might contain newline if insertImage added it, but fromHtml for img usually just one char)
// Actually fromHtml returns a Spanned with ImageSpan on a special character.
// We can just replace the old span range with new one.
// Careful: replacing text might reset other spans or move cursor.
// Better: get the new ImageSpan from newSpanned and set it on the existing range, removing the old one.
android.text.style.ImageSpan[] newSpans = newSpanned.getSpans(0, newSpanned.length(), android.text.style.ImageSpan.class);
if (newSpans.length > 0) {
editable.removeSpan(span);
editable.setSpan(newSpans[0], start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static String toHtml(Spanned text) {
return Html.toHtml(text);
}
public static Spanned fromHtml(String html, Context context) {
return Html.fromHtml(html, new NoteImageGetter(context), null);
}
public static Spanned fromHtml(String html) {
return Html.fromHtml(html);
}
}

@ -0,0 +1,67 @@
package net.micode.notes.tool;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.List;
public class SearchHistoryManager {
private static final String PREF_NAME = "search_history";
private static final String KEY_HISTORY = "history_list";
private static final int MAX_HISTORY_SIZE = 10;
private final SharedPreferences mPrefs;
public SearchHistoryManager(Context context) {
mPrefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
}
public List<String> getHistory() {
String json = mPrefs.getString(KEY_HISTORY, "");
List<String> list = new ArrayList<>();
if (TextUtils.isEmpty(json)) {
return list;
}
try {
JSONArray array = new JSONArray(json);
for (int i = 0; i < array.length(); i++) {
list.add(array.getString(i));
}
} catch (JSONException e) {
e.printStackTrace();
}
return list;
}
public void addHistory(String keyword) {
if (TextUtils.isEmpty(keyword)) return;
List<String> history = getHistory();
// Remove existing to move to top
history.remove(keyword);
history.add(0, keyword);
// Limit size
if (history.size() > MAX_HISTORY_SIZE) {
history = history.subList(0, MAX_HISTORY_SIZE);
}
saveHistory(history);
}
public void removeHistory(String keyword) {
List<String> history = getHistory();
if (history.remove(keyword)) {
saveHistory(history);
}
}
public void clearHistory() {
mPrefs.edit().remove(KEY_HISTORY).apply();
}
private void saveHistory(List<String> history) {
JSONArray array = new JSONArray(history);
mPrefs.edit().putString(KEY_HISTORY, array.toString()).apply();
}
}

@ -63,6 +63,8 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
private static final int SNIPPET_PREW_MAX_LEN = 60;
// 媒体播放器,用于播放闹钟声音
MediaPlayer mPlayer;
// 笔记类型
private int mNoteType;
/**
*
@ -102,6 +104,8 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
mNoteId = Long.valueOf(intent.getData().getPathSegments().get(1));
// 通过笔记ID获取笔记内容摘要
mSnippet = DataUtils.getSnippetById(this.getContentResolver(), mNoteId);
// 获取笔记类型
mNoteType = DataUtils.getNoteTypeById(this.getContentResolver(), mNoteId);
// 如果摘要超过最大长度,截取并添加省略号
mSnippet = mSnippet.length() > SNIPPET_PREW_MAX_LEN ? mSnippet.substring(0,
SNIPPET_PREW_MAX_LEN) + getResources().getString(R.string.notelist_string_info)
@ -114,7 +118,7 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
// 初始化媒体播放器
mPlayer = new MediaPlayer();
// 检查笔记是否在数据库中存在且可见
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, Notes.TYPE_NOTE)) {
if (DataUtils.visibleInNoteDatabase(getContentResolver(), mNoteId, mNoteType)) {
// 显示操作对话框
showActionDialog();
// 播放闹钟声音
@ -212,13 +216,20 @@ public class AlarmAlertActivity extends Activity implements OnClickListener, OnD
switch (which) {
// 如果点击了"查看笔记"按钮(负按钮)
case DialogInterface.BUTTON_NEGATIVE:
// 创建跳转到笔记编辑活动的Intent
Intent intent = new Intent(this, NoteEditActivity.class);
// 设置动作为查看
intent.setAction(Intent.ACTION_VIEW);
// 传递笔记ID
intent.putExtra(Intent.EXTRA_UID, mNoteId);
// 启动笔记编辑活动
Intent intent;
if (mNoteType == Notes.TYPE_TASK) {
// 如果是待办任务,跳转到笔记编辑活动(任务功能已合并)
intent = new Intent(this, NoteEditActivity.class);
intent.putExtra(Intent.EXTRA_UID, mNoteId);
} else {
// 创建跳转到笔记编辑活动的Intent
intent = new Intent(this, NoteEditActivity.class);
// 设置动作为查看
intent.setAction(Intent.ACTION_VIEW);
// 传递笔记ID
intent.putExtra(Intent.EXTRA_UID, mNoteId);
}
// 启动活动
startActivity(intent);
break;
// 默认情况(点击"确定"按钮)

@ -88,7 +88,7 @@ public class AlarmInitReceiver extends BroadcastReceiver {
sender.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, c.getLong(COLUMN_ID)));
// 创建PendingIntent它封装了上述Intent可以在指定时间触发
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, 0);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, sender, PendingIntent.FLAG_IMMUTABLE);
// 获取系统闹钟服务
AlarmManager alermManager = (AlarmManager) context

@ -0,0 +1,111 @@
package net.micode.notes.ui;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.tool.ResourceParser;
import java.util.List;
public class NoteColorAdapter extends RecyclerView.Adapter<NoteColorAdapter.ViewHolder> {
public interface OnColorClickListener {
void onColorClick(int colorId);
}
private List<Integer> mColorIds;
private int mSelectedColorId;
private OnColorClickListener mListener;
public NoteColorAdapter(List<Integer> colorIds, int selectedColorId, OnColorClickListener listener) {
mColorIds = colorIds;
mSelectedColorId = selectedColorId;
mListener = listener;
}
public void setSelectedColor(int colorId) {
mSelectedColorId = colorId;
notifyDataSetChanged();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.note_color_item, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
int colorId = mColorIds.get(position);
if (colorId == ResourceParser.CUSTOM_COLOR_BUTTON_ID) {
holder.colorView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
int padding = (int) (12 * holder.itemView.getContext().getResources().getDisplayMetrics().density);
holder.colorView.setPadding(padding, padding, padding, padding);
holder.colorView.setImageResource(R.drawable.ic_palette);
// Apply a dark tint to ensure visibility
holder.colorView.setColorFilter(android.graphics.Color.DKGRAY, android.graphics.PorterDuff.Mode.SRC_IN);
// Optional: Set a background for the icon
holder.colorView.setBackgroundResource(R.drawable.bg_color_btn_mask);
} else if (colorId == ResourceParser.WALLPAPER_BUTTON_ID) {
holder.colorView.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
int padding = (int) (12 * holder.itemView.getContext().getResources().getDisplayMetrics().density);
holder.colorView.setPadding(padding, padding, padding, padding);
holder.colorView.setImageResource(R.drawable.ic_image);
// Apply a dark tint to ensure visibility
holder.colorView.setColorFilter(android.graphics.Color.DKGRAY, android.graphics.PorterDuff.Mode.SRC_IN);
// Optional: Set a background for the icon
holder.colorView.setBackgroundResource(R.drawable.bg_color_btn_mask);
} else {
holder.colorView.setScaleType(ImageView.ScaleType.CENTER_CROP);
holder.colorView.setPadding(0, 0, 0, 0);
// 使用ResourceParser获取背景资源
int bgRes = ResourceParser.NoteBgResources.getNoteBgResource(colorId);
holder.colorView.setImageResource(bgRes);
holder.colorView.setBackground(null); // Clear background if reused
if (colorId >= ResourceParser.MIDNIGHT_BLACK || colorId < 0) {
int color = ResourceParser.getNoteBgColor(holder.itemView.getContext(), colorId);
holder.colorView.setColorFilter(color, android.graphics.PorterDuff.Mode.MULTIPLY);
} else {
holder.colorView.clearColorFilter();
}
}
if (colorId == mSelectedColorId && colorId != ResourceParser.CUSTOM_COLOR_BUTTON_ID) {
holder.checkView.setVisibility(View.VISIBLE);
} else {
holder.checkView.setVisibility(View.GONE);
}
holder.itemView.setOnClickListener(v -> {
if (mListener != null) {
mListener.onColorClick(colorId);
}
});
}
@Override
public int getItemCount() {
return mColorIds.size();
}
static class ViewHolder extends RecyclerView.ViewHolder {
ImageView colorView;
ImageView checkView;
public ViewHolder(@NonNull View itemView) {
super(itemView);
colorView = itemView.findViewById(R.id.color_view);
checkView = itemView.findViewById(R.id.check_view);
}
}
}

@ -57,6 +57,8 @@ import android.widget.Toast;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.TextNote;
import net.micode.notes.model.NoteCommand;
import net.micode.notes.model.UndoRedoManager;
import net.micode.notes.model.WorkingNote;
import net.micode.notes.model.WorkingNote.NoteSettingChangedListener;
import net.micode.notes.tool.DataUtils;
@ -77,8 +79,11 @@ import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.appbar.MaterialToolbar;
import net.micode.notes.databinding.NoteEditBinding;
import net.micode.notes.tool.RichTextHelper;
import net.micode.notes.data.FontManager;
public class NoteEditActivity extends AppCompatActivity implements OnClickListener,
NoteSettingChangedListener, OnTextViewChangeListener {
/**
@ -101,24 +106,6 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
public EditText etTitle;
}
private static final Map<Integer, Integer> sBgSelectorBtnsMap = new HashMap<Integer, Integer>();
static {
sBgSelectorBtnsMap.put(R.id.iv_bg_yellow, ResourceParser.YELLOW);
sBgSelectorBtnsMap.put(R.id.iv_bg_red, ResourceParser.RED);
sBgSelectorBtnsMap.put(R.id.iv_bg_blue, ResourceParser.BLUE);
sBgSelectorBtnsMap.put(R.id.iv_bg_green, ResourceParser.GREEN);
sBgSelectorBtnsMap.put(R.id.iv_bg_white, ResourceParser.WHITE);
}
private static final Map<Integer, Integer> sBgSelectorSelectionMap = new HashMap<Integer, Integer>();
static {
sBgSelectorSelectionMap.put(ResourceParser.YELLOW, R.id.iv_bg_yellow_select);
sBgSelectorSelectionMap.put(ResourceParser.RED, R.id.iv_bg_red_select);
sBgSelectorSelectionMap.put(ResourceParser.BLUE, R.id.iv_bg_blue_select);
sBgSelectorSelectionMap.put(ResourceParser.GREEN, R.id.iv_bg_green_select);
sBgSelectorSelectionMap.put(ResourceParser.WHITE, R.id.iv_bg_white_select);
}
private static final Map<Integer, Integer> sFontSizeBtnsMap = new HashMap<Integer, Integer>();
static {
sFontSizeBtnsMap.put(R.id.ll_font_large, ResourceParser.TEXT_LARGE);
@ -145,6 +132,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private View mFontSizeSelector;
private View mRichTextSelector;
private EditText mNoteEditor;
private View mNoteEditorPanel;
@ -170,25 +159,11 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private NoteEditBinding binding;
/**
* ID
*/
private View getBgSelectorView(int viewId) {
switch (viewId) {
case R.id.iv_bg_yellow_select:
return binding.ivBgYellowSelect;
case R.id.iv_bg_red_select:
return binding.ivBgRedSelect;
case R.id.iv_bg_blue_select:
return binding.ivBgBlueSelect;
case R.id.iv_bg_green_select:
return binding.ivBgGreenSelect;
case R.id.iv_bg_white_select:
return binding.ivBgWhiteSelect;
default:
throw new IllegalArgumentException("Unknown view ID: " + viewId);
}
}
private UndoRedoManager mUndoRedoManager;
private boolean mInUndoRedo = false;
private androidx.recyclerview.widget.RecyclerView mColorSelectorRv;
private NoteColorAdapter mColorAdapter;
/**
* ID
@ -215,6 +190,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
// 使用ViewBinding设置布局
binding = NoteEditBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
mUndoRedoManager = new UndoRedoManager();
// 初始化Toolbar使用MaterialToolbar与列表页面一致
setSupportActionBar(binding.toolbar);
@ -368,10 +344,16 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
mNoteEditor = binding.noteEditView;
mNoteEditorPanel = binding.svNoteEdit;
mNoteBgColorSelector = binding.noteBgColorSelector;
mColorSelectorRv = binding.rvBgColorSelector;
mNoteEditor.addTextChangedListener(new TextWatcher() {
private CharSequence mBeforeText;
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (!mInUndoRedo) {
mBeforeText = s.subSequence(start, start + count).toString();
}
}
@Override
@ -379,6 +361,13 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
if (mNoteHeaderHolder != null && mNoteHeaderHolder.tvCharCount != null) {
mNoteHeaderHolder.tvCharCount.setText(String.valueOf(s.length()) + " 字");
}
if (!mInUndoRedo) {
CharSequence afterText = s.subSequence(start, start + count).toString();
if (!TextUtils.equals(mBeforeText, afterText)) {
mUndoRedoManager.addCommand(new NoteCommand(mNoteEditor, start, mBeforeText, afterText));
invalidateOptionsMenu();
}
}
}
@Override
@ -406,30 +395,34 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
}
});
// 设置背景颜色选择器的点击事件
for (int id : sBgSelectorBtnsMap.keySet()) {
ImageView iv;
switch (id) {
case R.id.iv_bg_yellow:
iv = binding.ivBgYellow;
break;
case R.id.iv_bg_red:
iv = binding.ivBgRed;
break;
case R.id.iv_bg_blue:
iv = binding.ivBgBlue;
break;
case R.id.iv_bg_green:
iv = binding.ivBgGreen;
break;
case R.id.iv_bg_white:
iv = binding.ivBgWhite;
break;
default:
throw new IllegalArgumentException("Unknown view ID: " + id);
// Initialize Color Adapter
java.util.List<Integer> colors = java.util.Arrays.asList(
ResourceParser.YELLOW,
ResourceParser.BLUE,
ResourceParser.WHITE,
ResourceParser.GREEN,
ResourceParser.RED,
ResourceParser.MIDNIGHT_BLACK,
ResourceParser.EYE_CARE_GREEN,
ResourceParser.WARM,
ResourceParser.COOL,
ResourceParser.CUSTOM_COLOR_BUTTON_ID,
ResourceParser.WALLPAPER_BUTTON_ID
);
mColorAdapter = new NoteColorAdapter(colors, ResourceParser.YELLOW, new NoteColorAdapter.OnColorClickListener() {
@Override
public void onColorClick(int colorId) {
if (colorId == ResourceParser.CUSTOM_COLOR_BUTTON_ID) {
showColorPickerDialog();
} else if (colorId == ResourceParser.WALLPAPER_BUTTON_ID) {
pickWallpaper();
} else {
mWorkingNote.setBgColorId(colorId);
mNoteBgColorSelector.setVisibility(View.GONE);
}
}
iv.setOnClickListener(this);
}
});
mColorSelectorRv.setAdapter(mColorAdapter);
mFontSizeSelector = binding.fontSizeSelector;
for (int id : sFontSizeBtnsMap.keySet()) {
@ -464,6 +457,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
mFontSizeId = ResourceParser.BG_DEFAULT_FONT_SIZE;
}
mEditTextList = binding.noteEditList;
initRichTextToolbar();
}
@Override
@ -472,6 +466,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
initNoteScreen();
}
private static final int REQUEST_CODE_PICK_IMAGE = 106;
/**
*
* <p>
@ -487,21 +483,28 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private void initNoteScreen() {
mNoteEditor.setTextAppearance(this, TextAppearanceResources
.getTexAppearanceResource(mFontSizeId));
// Apply custom font
FontManager.getInstance(this).applyFont(mNoteEditor);
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
switchToListMode(mWorkingNote.getContent());
} else {
mNoteEditor.setText(getHighlightQueryResult(mWorkingNote.getContent(), mUserQuery));
String content = mWorkingNote.getContent();
if (content.contains("<") && content.contains(">")) {
mNoteEditor.setText(RichTextHelper.fromHtml(content, this));
} else {
mNoteEditor.setText(getHighlightQueryResult(content, mUserQuery));
}
mNoteEditor.setSelection(mNoteEditor.getText().length());
}
mNoteHeaderHolder.etTitle.setText(mWorkingNote.getTitle());
for (Integer id : sBgSelectorSelectionMap.keySet()) {
View view = getBgSelectorView(sBgSelectorSelectionMap.get(id));
if (view != null) {
view.setVisibility(View.GONE);
}
// Update Color Adapter selection
if (mColorAdapter != null) {
mColorAdapter.setSelectedColor(mWorkingNote.getBgColorId());
}
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
updateNoteBackgrounds();
mNoteHeaderHolder.tvModified.setText(DateUtils.formatDateTime(this,
mWorkingNote.getModifiedDate(), DateUtils.FORMAT_SHOW_DATE
@ -678,17 +681,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
int id = v.getId();
if (id == R.id.btn_set_bg_color) {
mNoteBgColorSelector.setVisibility(View.VISIBLE);
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.VISIBLE);
}
} else if (sBgSelectorBtnsMap.containsKey(id)) {
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.GONE);
}
mWorkingNote.setBgColorId(sBgSelectorBtnsMap.get(id));
mNoteBgColorSelector.setVisibility(View.GONE);
// Note: Adapter selection is already set in onBackgroundColorChanged or init
} else if (sFontSizeBtnsMap.containsKey(id)) {
View fontView = getFontSelectorView(sFontSelectorSelectionMap.get(mFontSizeId));
if (fontView != null) {
@ -706,6 +699,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
} else {
mNoteEditor.setTextAppearance(this,
TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
// Apply custom font again as setTextAppearance might reset it
FontManager.getInstance(this).applyFont(mNoteEditor);
}
mFontSizeSelector.setVisibility(View.GONE);
}
@ -758,12 +753,143 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
* </p>
*/
public void onBackgroundColorChanged() {
View bgView = getBgSelectorView(sBgSelectorSelectionMap.get(mWorkingNote.getBgColorId()));
if (bgView != null) {
bgView.setVisibility(View.VISIBLE);
if (mColorAdapter != null) {
mColorAdapter.setSelectedColor(mWorkingNote.getBgColorId());
}
updateNoteBackgrounds();
}
private void updateNoteBackgrounds() {
int colorId = mWorkingNote.getBgColorId();
String wallpaperPath = mWorkingNote.getWallpaperPath();
if (wallpaperPath != null) {
// Load wallpaper
android.net.Uri uri = android.net.Uri.parse(wallpaperPath);
try {
java.io.InputStream inputStream = getContentResolver().openInputStream(uri);
android.graphics.Bitmap bitmap = android.graphics.BitmapFactory.decodeStream(inputStream);
android.graphics.drawable.BitmapDrawable drawable = new android.graphics.drawable.BitmapDrawable(getResources(), bitmap);
// Tiling mode (can be configurable later)
drawable.setTileModeXY(android.graphics.Shader.TileMode.REPEAT, android.graphics.Shader.TileMode.REPEAT);
// Add Blur Effect for Android 12+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) {
mNoteEditorPanel.setBackground(drawable);
mNoteEditorPanel.setRenderEffect(android.graphics.RenderEffect.createBlurEffect(
20f, 20f, android.graphics.Shader.TileMode.CLAMP));
} else {
mNoteEditorPanel.setBackground(drawable);
}
// Header always uses original wallpaper (or maybe slightly darker?)
mHeadViewPanel.setBackground(drawable.getConstantState().newDrawable());
// Dynamic Coloring with Palette
androidx.palette.graphics.Palette.from(bitmap).generate(palette -> {
if (palette != null) {
applyPaletteColors(palette);
}
});
} catch (Exception e) {
Log.e(TAG, "Failed to load wallpaper", e);
// Fallback to color
applyColorBackground(colorId);
}
} else {
applyColorBackground(colorId);
// Reset toolbar colors to default/theme
resetToolbarColors();
}
updateTextColor(colorId);
}
private void applyPaletteColors(androidx.palette.graphics.Palette palette) {
int primaryColor = palette.getDominantColor(getResources().getColor(R.color.primary_color));
int onPrimaryColor = getResources().getColor(R.color.on_primary_color);
// Ensure contrast for onPrimaryColor
if (androidx.core.graphics.ColorUtils.calculateContrast(onPrimaryColor, primaryColor) < 3.0) {
onPrimaryColor = android.graphics.Color.WHITE;
}
binding.toolbar.setBackgroundColor(primaryColor);
binding.toolbar.setTitleTextColor(onPrimaryColor);
if (binding.toolbar.getNavigationIcon() != null) {
binding.toolbar.getNavigationIcon().setTint(onPrimaryColor);
}
getWindow().setStatusBarColor(primaryColor);
}
private void resetToolbarColors() {
int primaryColor = getResources().getColor(R.color.primary_color);
int onPrimaryColor = getResources().getColor(R.color.on_primary_color);
binding.toolbar.setBackgroundColor(primaryColor);
binding.toolbar.setTitleTextColor(onPrimaryColor);
if (binding.toolbar.getNavigationIcon() != null) {
binding.toolbar.getNavigationIcon().setTint(onPrimaryColor);
}
getWindow().setStatusBarColor(primaryColor);
}
private void updateTextColor(int colorId) {
// Default to black for light backgrounds
int textColor = android.graphics.Color.BLACK;
if (colorId == ResourceParser.MIDNIGHT_BLACK) {
textColor = android.graphics.Color.WHITE;
} else if (colorId < 0) {
// Custom color: Calculate luminance
// colorId is the ARGB value for custom colors
if (isColorDark(colorId)) {
textColor = android.graphics.Color.WHITE;
}
}
// For wallpaper, we might want to check palette, but for now default to black or keep current
// If wallpaper is set, this method is called with the underlying colorId.
// We should probably rely on the underlying color or default to white/black.
mNoteEditor.setTextColor(textColor);
// Also update title color if needed
if (mNoteHeaderHolder != null && mNoteHeaderHolder.etTitle != null) {
mNoteHeaderHolder.etTitle.setTextColor(textColor);
}
}
private boolean isColorDark(int color) {
double darkness = 1 - (0.299 * android.graphics.Color.red(color) +
0.587 * android.graphics.Color.green(color) +
0.114 * android.graphics.Color.blue(color)) / 255;
return darkness >= 0.5;
}
private void applyColorBackground(int colorId) {
mNoteEditorPanel.setBackgroundResource(mWorkingNote.getBgColorResId());
mHeadViewPanel.setBackgroundResource(mWorkingNote.getTitleBgResId());
if (colorId >= ResourceParser.MIDNIGHT_BLACK || colorId < 0) {
int color = ResourceParser.getNoteBgColor(this, colorId);
if (mNoteEditorPanel.getBackground() != null) {
mNoteEditorPanel.getBackground().setTint(color);
mNoteEditorPanel.getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY);
}
if (mHeadViewPanel.getBackground() != null) {
mHeadViewPanel.getBackground().setTint(color);
mHeadViewPanel.getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY);
}
} else {
// Clear tint for legacy resources
if (mNoteEditorPanel.getBackground() != null) {
mNoteEditorPanel.getBackground().clearColorFilter();
}
if (mHeadViewPanel.getBackground() != null) {
mHeadViewPanel.getBackground().clearColorFilter();
}
}
}
/**
@ -790,6 +916,18 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
getMenuInflater().inflate(R.menu.call_note_edit, menu);
} else {
getMenuInflater().inflate(R.menu.note_edit, menu);
MenuItem undoItem = menu.findItem(R.id.menu_undo);
MenuItem redoItem = menu.findItem(R.id.menu_redo);
MenuItem clearItem = menu.findItem(R.id.menu_clear_history);
if (undoItem != null) {
undoItem.setEnabled(mUndoRedoManager.canUndo());
}
if (redoItem != null) {
redoItem.setEnabled(mUndoRedoManager.canRedo());
}
if (clearItem != null) {
clearItem.setEnabled(mUndoRedoManager.canUndo() || mUndoRedoManager.canRedo());
}
}
if (mWorkingNote.getCheckListMode() == TextNote.MODE_CHECK_LIST) {
menu.findItem(R.id.menu_list_mode).setTitle(R.string.menu_normal_mode);
@ -825,9 +963,42 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_rich_text:
if (mRichTextSelector.getVisibility() == View.VISIBLE) {
mRichTextSelector.setVisibility(View.GONE);
} else {
mRichTextSelector.setVisibility(View.VISIBLE);
mFontSizeSelector.setVisibility(View.GONE);
}
break;
case R.id.menu_undo:
mInUndoRedo = true;
mUndoRedoManager.undo();
mInUndoRedo = false;
invalidateOptionsMenu();
showToast(R.string.undo_success);
break;
case R.id.menu_redo:
mInUndoRedo = true;
mUndoRedoManager.redo();
mInUndoRedo = false;
invalidateOptionsMenu();
showToast(R.string.redo_success);
break;
case R.id.menu_clear_history:
mUndoRedoManager.clear();
invalidateOptionsMenu();
showToast(R.string.menu_clear_history);
break;
case R.id.menu_new_note:
createNewNote();
break;
case R.id.menu_save_as_template:
saveAsTemplate();
break;
case R.id.menu_picture:
pickImage();
break;
case R.id.menu_delete:
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.alert_title_delete));
@ -985,7 +1156,7 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
if (mWorkingNote.getNoteId() > 0) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, mWorkingNote.getNoteId()));
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, 0);
PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
AlarmManager alarmManager = ((AlarmManager) getSystemService(ALARM_SERVICE));
showAlertHeader();
if(!set) {
@ -1143,6 +1314,9 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
View view = LayoutInflater.from(this).inflate(R.layout.note_edit_list_item, null);
final NoteEditText edit = (NoteEditText) view.findViewById(R.id.et_edit_text);
edit.setTextAppearance(this, TextAppearanceResources.getTexAppearanceResource(mFontSizeId));
// Apply custom font
FontManager.getInstance(this).applyFont(edit);
CheckBox cb = ((CheckBox) view.findViewById(R.id.cb_edit_item));
cb.setOnCheckedChangeListener(new OnCheckedChangeListener() {
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
@ -1202,6 +1376,8 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
* @param newMode
*/
public void onCheckListModeChanged(int oldMode, int newMode) {
mUndoRedoManager.clear();
invalidateOptionsMenu();
if (newMode == TextNote.MODE_CHECK_LIST) {
switchToListMode(mNoteEditor.getText().toString());
} else {
@ -1336,6 +1512,142 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
SHORTCUT_ICON_TITLE_MAX_LEN) : content;
}
private void showColorPickerDialog() {
final View dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_color_picker, null);
final View colorPreview = dialogView.findViewById(R.id.view_color_preview);
android.widget.SeekBar sbRed = dialogView.findViewById(R.id.sb_red);
android.widget.SeekBar sbGreen = dialogView.findViewById(R.id.sb_green);
android.widget.SeekBar sbBlue = dialogView.findViewById(R.id.sb_blue);
int currentColor = android.graphics.Color.WHITE;
if (mWorkingNote.getBgColorId() < 0) {
currentColor = mWorkingNote.getBgColorId();
}
final int[] rgb = new int[]{
android.graphics.Color.red(currentColor),
android.graphics.Color.green(currentColor),
android.graphics.Color.blue(currentColor)
};
colorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]));
sbRed.setProgress(rgb[0]);
sbGreen.setProgress(rgb[1]);
sbBlue.setProgress(rgb[2]);
android.widget.SeekBar.OnSeekBarChangeListener listener = new android.widget.SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(android.widget.SeekBar seekBar, int progress, boolean fromUser) {
if (seekBar.getId() == R.id.sb_red) rgb[0] = progress;
else if (seekBar.getId() == R.id.sb_green) rgb[1] = progress;
else if (seekBar.getId() == R.id.sb_blue) rgb[2] = progress;
colorPreview.setBackgroundColor(android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]));
}
@Override
public void onStartTrackingTouch(android.widget.SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(android.widget.SeekBar seekBar) {}
};
sbRed.setOnSeekBarChangeListener(listener);
sbGreen.setOnSeekBarChangeListener(listener);
sbBlue.setOnSeekBarChangeListener(listener);
new AlertDialog.Builder(this)
.setTitle("Custom Color")
.setView(dialogView)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int newColor = android.graphics.Color.rgb(rgb[0], rgb[1], rgb[2]);
// Use negative integer for custom color. Ensure it's negative.
// ARGB color with alpha 255 is negative in Java int.
// If alpha is 0, it might be positive. We assume full opacity.
newColor |= 0xFF000000;
mWorkingNote.setBgColorId(newColor);
mNoteBgColorSelector.setVisibility(View.GONE);
})
.setNegativeButton(android.R.string.cancel, null)
.show();
}
private static final int REQUEST_CODE_PICK_WALLPAPER = 105;
private void pickWallpaper() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_PICK_WALLPAPER);
}
private void pickImage() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE);
}
private void saveImageToPrivateStorage(android.net.Uri uri) {
new Thread(() -> {
try {
java.io.InputStream is = getContentResolver().openInputStream(uri);
if (is == null) return;
// Create images directory if not exists
java.io.File imagesDir = new java.io.File(getFilesDir(), "images");
if (!imagesDir.exists()) {
imagesDir.mkdirs();
}
// Create a unique file name
String fileName = "img_" + System.currentTimeMillis() + ".jpg";
java.io.File destFile = new java.io.File(imagesDir, fileName);
java.io.FileOutputStream fos = new java.io.FileOutputStream(destFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = is.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
fos.close();
is.close();
final String filePath = "file://" + destFile.getAbsolutePath();
runOnUiThread(() -> {
RichTextHelper.insertImage(mNoteEditor, filePath);
});
} catch (Exception e) {
Log.e(TAG, "Failed to copy image", e);
runOnUiThread(() -> {
showToast(R.string.failed_sdcard_export); // Use generic failure message or add new one
});
}
}).start();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_WALLPAPER && resultCode == RESULT_OK && data != null) {
android.net.Uri uri = data.getData();
if (uri != null) {
// Take persistent permissions
try {
getContentResolver().takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
} catch (SecurityException e) {
Log.e(TAG, "Failed to take persistable uri permission", e);
}
mWorkingNote.setWallpaper(uri.toString());
mNoteBgColorSelector.setVisibility(View.GONE);
}
} else if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK && data != null) {
android.net.Uri uri = data.getData();
if (uri != null) {
saveImageToPrivateStorage(uri);
}
}
}
/**
* Toast
* <p>
@ -1358,4 +1670,174 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
private void showToast(int resId, int duration) {
Toast.makeText(this, resId, duration).show();
}
private void initRichTextToolbar() {
mRichTextSelector = findViewById(R.id.rich_text_selector);
findViewById(R.id.btn_bold).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyBold(mNoteEditor); }
});
findViewById(R.id.btn_italic).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyItalic(mNoteEditor); }
});
findViewById(R.id.btn_underline).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyUnderline(mNoteEditor); }
});
findViewById(R.id.btn_strikethrough).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyStrikethrough(mNoteEditor); }
});
findViewById(R.id.btn_header).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final CharSequence[] items = {"H1 (Largest)", "H2", "H3", "H4", "H5", "H6 (Smallest)", "Normal"};
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("Header Level");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
// item index maps to level: 0->1, 1->2, ..., 5->6, 6->0 (Normal)
int level = (item == 6) ? 0 : (item + 1);
RichTextHelper.applyHeading(mNoteEditor, level);
}
});
builder.show();
}
});
findViewById(R.id.btn_list).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyBullet(mNoteEditor); }
});
findViewById(R.id.btn_quote).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyQuote(mNoteEditor); }
});
findViewById(R.id.btn_code).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.applyCode(mNoteEditor); }
});
findViewById(R.id.btn_link).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.insertLink(NoteEditActivity.this, mNoteEditor); }
});
findViewById(R.id.btn_divider).setOnClickListener(new OnClickListener() {
public void onClick(View v) { RichTextHelper.insertDivider(mNoteEditor); }
});
findViewById(R.id.btn_color_text).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final CharSequence[] items = {"Black", "Red", "Blue"};
final int[] colors = {android.graphics.Color.BLACK, android.graphics.Color.RED, android.graphics.Color.BLUE};
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("Text Color");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
RichTextHelper.applyColor(mNoteEditor, colors[item], false);
}
});
builder.show();
}
});
findViewById(R.id.btn_color_fill).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
final CharSequence[] items = {"None", "Yellow", "Green", "Cyan"};
final int[] colors = {android.graphics.Color.TRANSPARENT, android.graphics.Color.YELLOW, android.graphics.Color.GREEN, android.graphics.Color.CYAN};
AlertDialog.Builder builder = new AlertDialog.Builder(NoteEditActivity.this);
builder.setTitle("Background Color");
builder.setItems(items, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
RichTextHelper.applyColor(mNoteEditor, colors[item], true);
}
});
builder.show();
}
});
}
private void saveAsTemplate() {
if (!mWorkingNote.existInDatabase()) {
saveNote();
}
final net.micode.notes.data.NotesRepository repository = new net.micode.notes.data.NotesRepository(getContentResolver());
new Thread(() -> {
android.database.Cursor cursor = getContentResolver().query(Notes.CONTENT_NOTE_URI,
new String[]{net.micode.notes.data.Notes.NoteColumns.ID, net.micode.notes.data.Notes.NoteColumns.SNIPPET},
net.micode.notes.data.Notes.NoteColumns.PARENT_ID + "=? AND " + net.micode.notes.data.Notes.NoteColumns.TYPE + "=?",
new String[]{String.valueOf(Notes.ID_TEMPLATE_FOLDER), String.valueOf(Notes.TYPE_FOLDER)},
null);
final java.util.List<String> folderNames = new java.util.ArrayList<>();
final java.util.List<Long> folderIds = new java.util.ArrayList<>();
if (cursor != null) {
while(cursor.moveToNext()) {
folderIds.add(cursor.getLong(0));
folderNames.add(cursor.getString(1));
}
cursor.close();
}
runOnUiThread(() -> {
if (folderNames.isEmpty()) {
Toast.makeText(this, "No template categories found", Toast.LENGTH_SHORT).show();
repository.shutdown();
return;
}
showSaveTemplateDialog(repository, folderNames, folderIds);
});
}).start();
}
private void showSaveTemplateDialog(final net.micode.notes.data.NotesRepository repository,
final java.util.List<String> folderNames,
final java.util.List<Long> folderIds) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Save as Template");
LinearLayout layout = new LinearLayout(this);
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(50, 40, 50, 40);
final EditText input = new EditText(this);
input.setHint("Template Name");
input.setText(mWorkingNote.getTitle());
layout.addView(input);
final TextView label = new TextView(this);
label.setText("Select Category:");
label.setPadding(0, 20, 0, 10);
layout.addView(label);
final android.widget.Spinner spinner = new android.widget.Spinner(this);
android.widget.ArrayAdapter<String> adapter = new android.widget.ArrayAdapter<>(this,
android.R.layout.simple_spinner_item, folderNames);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinner.setAdapter(adapter);
layout.addView(spinner);
builder.setView(layout);
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
String name = input.getText().toString();
int position = spinner.getSelectedItemPosition();
if (position >= 0 && position < folderIds.size()) {
long categoryId = folderIds.get(position);
repository.createTemplate(mWorkingNote.getNoteId(), categoryId, name, new net.micode.notes.data.NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long result) {
runOnUiThread(() -> {
Toast.makeText(NoteEditActivity.this, "Template Saved", Toast.LENGTH_SHORT).show();
repository.shutdown();
});
}
@Override
public void onError(Exception e) {
runOnUiThread(() -> {
Toast.makeText(NoteEditActivity.this, "Failed: " + e.getMessage(), Toast.LENGTH_SHORT).show();
repository.shutdown();
});
}
});
}
});
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> repository.shutdown());
builder.setOnCancelListener(dialog -> repository.shutdown());
builder.show();
}
}

@ -55,7 +55,17 @@ import java.util.Map;
*
* @see NoteEditActivity
*/
public class NoteEditText extends EditText {
import android.view.ScaleGestureDetector;
import android.view.GestureDetector;
import android.text.style.ImageSpan;
import net.micode.notes.tool.RichTextHelper;
import android.app.AlertDialog;
import android.widget.SeekBar;
import android.widget.TextView;
import android.widget.LinearLayout;
import android.content.DialogInterface;
public class NoteEditText extends EditText implements ScaleGestureDetector.OnScaleGestureListener {
// 日志标签
private static final String TAG = "NoteEditText";
// 当前EditText的索引
@ -63,6 +73,13 @@ public class NoteEditText extends EditText {
// 删除前的光标位置
private int mSelectionStartBeforeDelete;
// Scale Gesture Detector
private ScaleGestureDetector mScaleDetector;
private GestureDetector mGestureDetector;
private ImageSpan mSelectedImageSpan;
private int mInitialWidth;
private int mInitialHeight;
// 电话号码URI方案
private static final String SCHEME_TEL = "tel:" ;
// HTTP URI方案
@ -78,6 +95,74 @@ public class NoteEditText extends EditText {
sSchemaActionResMap.put(SCHEME_EMAIL, R.string.note_link_email);
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (mSelectedImageSpan != null) {
float scaleFactor = detector.getScaleFactor();
int newWidth = (int) (mInitialWidth * scaleFactor);
int newHeight = (int) (mInitialHeight * scaleFactor);
// Constrain size
int maxWidth = getResources().getDisplayMetrics().widthPixels;
if (newWidth > maxWidth) {
newWidth = maxWidth;
newHeight = (int) (mInitialHeight * (maxWidth / (float) mInitialWidth));
}
if (newWidth < 100) newWidth = 100;
if (newHeight < 100) newHeight = 100;
if (mSelectedImageSpan.getDrawable() != null) {
mSelectedImageSpan.getDrawable().setBounds(0, 0, newWidth, newHeight);
// Force layout update
invalidate();
requestLayout();
}
return true;
}
return false;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
float x = detector.getFocusX();
float y = detector.getFocusY();
x += getScrollX();
y += getScrollY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
Layout layout = getLayout();
if (layout != null) {
int line = layout.getLineForVertical((int) y);
int offset = layout.getOffsetForHorizontal(line, x);
if (getText() instanceof Spanned) {
Spanned spanned = (Spanned) getText();
ImageSpan[] spans = spanned.getSpans(offset, offset, ImageSpan.class);
if (spans.length > 0) {
mSelectedImageSpan = spans[0];
if (mSelectedImageSpan.getDrawable() != null) {
Rect bounds = mSelectedImageSpan.getDrawable().getBounds();
mInitialWidth = bounds.width();
mInitialHeight = bounds.height();
return true;
}
}
}
}
return false;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if (mSelectedImageSpan != null && mSelectedImageSpan.getDrawable() != null) {
Rect bounds = mSelectedImageSpan.getDrawable().getBounds();
RichTextHelper.updateImageSpanSize(this, mSelectedImageSpan, bounds.width(), bounds.height());
mSelectedImageSpan = null;
}
}
/**
*
* <p>
@ -123,6 +208,111 @@ public class NoteEditText extends EditText {
public NoteEditText(Context context) {
super(context, null);
mIndex = 0;
init(context);
}
private void init(Context context) {
mScaleDetector = new ScaleGestureDetector(context, this);
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
float x = e.getX();
float y = e.getY();
x += getScrollX();
y += getScrollY();
x -= getTotalPaddingLeft();
y -= getTotalPaddingTop();
Layout layout = getLayout();
if (layout != null) {
int line = layout.getLineForVertical((int) y);
int offset = layout.getOffsetForHorizontal(line, x);
if (getText() instanceof Spanned) {
Spanned spanned = (Spanned) getText();
ImageSpan[] spans = spanned.getSpans(offset, offset, ImageSpan.class);
if (spans.length > 0) {
showResizeDialog(spans[0]);
return true;
}
}
}
return super.onDoubleTap(e);
}
});
}
private void showResizeDialog(final ImageSpan imageSpan) {
if (imageSpan.getDrawable() == null) return;
final Rect bounds = imageSpan.getDrawable().getBounds();
final int originalWidth = bounds.width();
final int originalHeight = bounds.height();
final float aspectRatio = (float) originalHeight / originalWidth;
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("Resize Image");
LinearLayout layout = new LinearLayout(getContext());
layout.setOrientation(LinearLayout.VERTICAL);
layout.setPadding(50, 20, 50, 20);
final TextView label = new TextView(getContext());
label.setText("Scale: 100%");
layout.addView(label);
final SeekBar seekBar = new SeekBar(getContext());
seekBar.setMax(200); // 0 to 200%
seekBar.setProgress(100);
layout.addView(seekBar);
builder.setView(layout);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
// Minimum 10%
if (progress < 10) progress = 10;
float scale = progress / 100f;
int newWidth = (int) (originalWidth * scale);
int newHeight = (int) (newWidth * aspectRatio);
label.setText("Scale: " + progress + "%");
// Live preview
imageSpan.getDrawable().setBounds(0, 0, newWidth, newHeight);
invalidate();
requestLayout();
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
});
builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Rect finalBounds = imageSpan.getDrawable().getBounds();
RichTextHelper.updateImageSpanSize(NoteEditText.this, imageSpan, finalBounds.width(), finalBounds.height());
}
});
builder.setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Revert
imageSpan.getDrawable().setBounds(0, 0, originalWidth, originalHeight);
invalidate();
requestLayout();
}
});
builder.show();
}
/**
@ -151,6 +341,7 @@ public class NoteEditText extends EditText {
*/
public NoteEditText(Context context, AttributeSet attrs) {
super(context, attrs, android.R.attr.editTextStyle);
init(context);
}
/**
@ -162,6 +353,7 @@ public class NoteEditText extends EditText {
*/
public NoteEditText(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
/**
@ -174,6 +366,17 @@ public class NoteEditText extends EditText {
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mScaleDetector != null) {
mScaleDetector.onTouchEvent(event);
if (mScaleDetector.isInProgress()) {
return true;
}
}
if (mGestureDetector != null) {
mGestureDetector.onTouchEvent(event);
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 获取触摸坐标

@ -0,0 +1,188 @@
package net.micode.notes.ui;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SearchView;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.SearchHistoryManager;
import java.util.ArrayList;
import java.util.List;
public class NoteSearchActivity extends AppCompatActivity implements SearchView.OnQueryTextListener, NoteSearchAdapter.OnItemClickListener {
private SearchView mSearchView;
private RecyclerView mRecyclerView;
private TextView mTvNoResult;
private NoteSearchAdapter mAdapter;
private NotesRepository mRepository;
private SearchHistoryManager mHistoryManager;
private TextView mBtnShowHistory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_note_search);
mRepository = new NotesRepository(getContentResolver());
mHistoryManager = new SearchHistoryManager(this);
initViews();
// Initial state: search is empty, show history button if there is history, or just show list
// Requirement: "history option below search bar"
showHistoryOption();
}
private void initViews() {
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
toolbar.setNavigationOnClickListener(v -> finish());
mSearchView = findViewById(R.id.search_view);
mSearchView.setOnQueryTextListener(this);
mSearchView.setFocusable(true);
mSearchView.setIconified(false);
mSearchView.requestFocusFromTouch();
mBtnShowHistory = findViewById(R.id.btn_show_history);
mBtnShowHistory.setOnClickListener(v -> showHistoryList());
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mAdapter = new NoteSearchAdapter(this, this);
mRecyclerView.setAdapter(mAdapter);
mTvNoResult = findViewById(R.id.tv_no_result);
}
private void showHistoryOption() {
// Show the "History" button, hide the list
mBtnShowHistory.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
mTvNoResult.setVisibility(View.GONE);
}
private void showHistoryList() {
List<String> history = mHistoryManager.getHistory();
if (history.isEmpty()) {
// If no history, maybe show a toast or empty state?
// But for now, let's just show the empty list which is fine
}
List<Object> data = new ArrayList<>(history);
mAdapter.setData(data, null);
mBtnShowHistory.setVisibility(View.GONE); // Hide button when showing list
mTvNoResult.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
private void performSearch(String query) {
if (TextUtils.isEmpty(query)) {
showHistoryOption();
return;
}
// Hide history button when searching
mBtnShowHistory.setVisibility(View.GONE);
mRepository.searchNotes(query, new NotesRepository.Callback<List<NotesRepository.NoteInfo>>() {
@Override
public void onSuccess(List<NotesRepository.NoteInfo> result) {
runOnUiThread(() -> {
List<Object> data = new ArrayList<>(result);
mAdapter.setData(data, query);
if (data.isEmpty()) {
mTvNoResult.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
mTvNoResult.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
});
}
@Override
public void onError(Exception error) {
runOnUiThread(() -> {
Toast.makeText(NoteSearchActivity.this, "Search failed: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
}
@Override
public boolean onQueryTextSubmit(String query) {
if (!TextUtils.isEmpty(query)) {
mHistoryManager.addHistory(query);
performSearch(query);
mSearchView.clearFocus(); // Hide keyboard
}
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
if (TextUtils.isEmpty(newText)) {
showHistoryOption();
} else {
performSearch(newText);
}
return true;
}
@Override
public void onNoteClick(NotesRepository.NoteInfo note) {
// Save history when user clicks a result
String query = mSearchView.getQuery().toString();
if (!TextUtils.isEmpty(query)) {
mHistoryManager.addHistory(query);
}
Intent intent = new Intent(this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, note.getId());
// Pass search keyword for highlighting in editor
// NoteEditActivity uses SearchManager.EXTRA_DATA_KEY for ID and USER_QUERY for keyword
intent.putExtra(android.app.SearchManager.EXTRA_DATA_KEY, String.valueOf(note.getId()));
intent.putExtra(android.app.SearchManager.USER_QUERY, mSearchView.getQuery().toString());
startActivity(intent);
}
@Override
public void onHistoryClick(String keyword) {
mSearchView.setQuery(keyword, true);
}
@Override
public void onHistoryDelete(String keyword) {
mHistoryManager.removeHistory(keyword);
// Refresh history view if we are currently showing history (search box is empty)
if (TextUtils.isEmpty(mSearchView.getQuery())) {
showHistoryList();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mRepository != null) {
mRepository.shutdown();
}
}
}

@ -0,0 +1,180 @@
package net.micode.notes.ui;
import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.BackgroundColorSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.NotesRepository;
import net.micode.notes.tool.DataUtils;
import net.micode.notes.tool.ResourceParser;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class NoteSearchAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int TYPE_HISTORY = 1;
private static final int TYPE_NOTE = 2;
private Context mContext;
private List<Object> mDataList;
private String mSearchKeyword;
private OnItemClickListener mListener;
public interface OnItemClickListener {
void onNoteClick(NotesRepository.NoteInfo note);
void onHistoryClick(String keyword);
void onHistoryDelete(String keyword);
}
public NoteSearchAdapter(Context context, OnItemClickListener listener) {
mContext = context;
mListener = listener;
mDataList = new ArrayList<>();
}
public void setData(List<Object> data, String keyword) {
mDataList = data;
mSearchKeyword = keyword;
notifyDataSetChanged();
}
@Override
public int getItemViewType(int position) {
Object item = mDataList.get(position);
if (item instanceof String) {
return TYPE_HISTORY;
} else if (item instanceof NotesRepository.NoteInfo) {
return TYPE_NOTE;
}
return super.getItemViewType(position);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == TYPE_HISTORY) {
View view = LayoutInflater.from(mContext).inflate(R.layout.search_history_item, parent, false);
return new HistoryViewHolder(view);
} else {
View view = LayoutInflater.from(mContext).inflate(R.layout.note_item, parent, false);
return new NoteViewHolder(view);
}
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
if (holder instanceof HistoryViewHolder) {
String keyword = (String) mDataList.get(position);
((HistoryViewHolder) holder).bind(keyword);
} else if (holder instanceof NoteViewHolder) {
NotesRepository.NoteInfo note = (NotesRepository.NoteInfo) mDataList.get(position);
((NoteViewHolder) holder).bind(note);
}
}
@Override
public int getItemCount() {
return mDataList.size();
}
class HistoryViewHolder extends RecyclerView.ViewHolder {
TextView tvKeyword;
ImageView ivDelete;
public HistoryViewHolder(View itemView) {
super(itemView);
tvKeyword = itemView.findViewById(R.id.tv_history_keyword);
ivDelete = itemView.findViewById(R.id.iv_delete_history);
}
public void bind(final String keyword) {
tvKeyword.setText(keyword);
itemView.setOnClickListener(v -> {
if (mListener != null) mListener.onHistoryClick(keyword);
});
ivDelete.setOnClickListener(v -> {
if (mListener != null) mListener.onHistoryDelete(keyword);
});
}
}
class NoteViewHolder extends RecyclerView.ViewHolder {
ImageView ivTypeIcon;
TextView tvTitle;
TextView tvTime;
TextView tvName;
ImageView ivAlertIcon;
CheckBox checkbox;
public NoteViewHolder(View itemView) {
super(itemView);
ivTypeIcon = itemView.findViewById(R.id.iv_type_icon);
tvTitle = itemView.findViewById(R.id.tv_title);
tvTime = itemView.findViewById(R.id.tv_time);
tvName = itemView.findViewById(R.id.tv_name);
ivAlertIcon = itemView.findViewById(R.id.iv_alert_icon);
checkbox = itemView.findViewById(android.R.id.checkbox);
}
public void bind(final NotesRepository.NoteInfo note) {
// 设置标题和高亮
// NoteInfo.title defaults to snippet if title is empty, so it's safe to use title
if (!TextUtils.isEmpty(mSearchKeyword)) {
tvTitle.setText(getHighlightText(note.title, mSearchKeyword));
} else {
tvTitle.setText(note.title);
}
// 设置时间
tvTime.setText(android.text.format.DateUtils.getRelativeTimeSpanString(note.modifiedDate));
// 设置背景(如果 NoteInfo 中有背景ID
// 注意NoteInfo 中 bgColorId 是整型ID需要转换为资源ID
// 这里为了简单,暂不设置复杂的背景,或者使用默认背景
// 点击事件
itemView.setOnClickListener(v -> {
if (mListener != null) mListener.onNoteClick(note);
});
// 隐藏不需要的视图
ivTypeIcon.setVisibility(View.GONE);
tvName.setVisibility(View.GONE);
checkbox.setVisibility(View.GONE);
ivAlertIcon.setVisibility(View.GONE);
}
}
private Spannable getHighlightText(String text, String keyword) {
if (text == null) text = "";
SpannableString spannable = new SpannableString(text);
if (!TextUtils.isEmpty(keyword)) {
Pattern pattern = Pattern.compile(Pattern.quote(keyword), Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
spannable.setSpan(
new BackgroundColorSpan(0x40FFFF00), // 半透明黄色
matcher.start(),
matcher.end(),
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
}
}
return spannable;
}
}

@ -463,6 +463,8 @@ public class NotesListActivity extends AppCompatActivity
if (viewModel.isTrashMode()) {
// 回收站模式:弹出恢复/删除对话框
showTrashItemDialog(note);
} else if (viewModel.isTemplateMode() && note.type == Notes.TYPE_NOTE) {
showApplyTemplateDialog(note);
} else if (note.type == Notes.TYPE_FOLDER) {
// 文件夹:进入该文件夹
// 检查隐私锁
@ -508,6 +510,45 @@ public class NotesListActivity extends AppCompatActivity
}
}
/**
*
*/
private void showApplyTemplateDialog(NotesRepository.NoteInfo note) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("应用模板");
builder.setMessage("使用模板 \"" + (TextUtils.isEmpty(note.title) ? "未命名" : note.title) + "\" 创建新笔记?");
builder.setPositiveButton("创建", (dialog, which) -> {
viewModel.applyTemplate(note.getId(), new NotesRepository.Callback<Long>() {
@Override
public void onSuccess(Long newNoteId) {
runOnUiThread(() -> {
Toast.makeText(NotesListActivity.this, "创建成功", Toast.LENGTH_SHORT).show();
// 跳转到新笔记编辑页
Intent intent = new Intent(NotesListActivity.this, NoteEditActivity.class);
intent.setAction(Intent.ACTION_VIEW);
intent.putExtra(Intent.EXTRA_UID, newNoteId);
startActivity(intent);
// 退出模板模式(返回根目录)
// viewModel.loadNotes(Notes.ID_ROOT_FOLDER);
});
}
@Override
public void onError(Exception error) {
runOnUiThread(() -> {
Toast.makeText(NotesListActivity.this, "创建失败: " + error.getMessage(), Toast.LENGTH_SHORT).show();
});
}
});
});
builder.setNegativeButton("取消", null);
builder.setNeutralButton("编辑模板", (dialog, which) -> {
// 打开编辑器编辑模板本身
openNoteEditor(note);
});
builder.show();
}
/**
*
*/
@ -647,6 +688,8 @@ public class NotesListActivity extends AppCompatActivity
// 设置标题
if (viewModel.isTrashMode()) {
binding.toolbar.setTitle(R.string.menu_trash);
} else if (viewModel.isTemplateMode()) {
binding.toolbar.setTitle(R.string.menu_templates);
} else {
binding.toolbar.setTitle(R.string.app_name);
// 添加普通模式菜单
@ -758,9 +801,13 @@ public class NotesListActivity extends AppCompatActivity
int itemId = item.getItemId();
switch (itemId) {
case R.id.menu_tasks:
startActivity(new Intent(this, TaskListActivity.class));
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left);
return true;
case R.id.menu_search:
// TODO: 打开搜索对话框
Toast.makeText(this, "搜索功能开发中", Toast.LENGTH_SHORT).show();
Intent searchIntent = new Intent(this, NoteSearchActivity.class);
startActivity(searchIntent);
return true;
case R.id.menu_new_folder:
// 创建新文件夹
@ -898,6 +945,16 @@ public class NotesListActivity extends AppCompatActivity
Toast.makeText(this, "导出功能待实现", Toast.LENGTH_SHORT).show();
}
@Override
public void onTemplateSelected() {
// 跳转到模板文件夹
viewModel.enterFolder(Notes.ID_TEMPLATE_FOLDER);
// 关闭侧栏
if (binding.drawerLayout != null) {
binding.drawerLayout.closeDrawer(sidebarFragment);
}
}
@Override
public void onSettingsSelected() {
// 打开设置页面

@ -28,6 +28,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.data.FontManager;
/**
@ -80,6 +81,7 @@ public class NotesListItem extends LinearLayout {
mCallName.setVisibility(View.GONE);
mAlert.setVisibility(View.VISIBLE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
FontManager.getInstance(context).applyFont(mTitle);
mTitle.setText(context.getString(R.string.call_record_folder_name)
+ context.getString(R.string.format_folder_files_count, data.getNotesCount()));
mAlert.setImageResource(R.drawable.call_record);
@ -87,6 +89,7 @@ public class NotesListItem extends LinearLayout {
mCallName.setVisibility(View.VISIBLE);
mCallName.setText(data.getCallName());
mTitle.setTextAppearance(context,R.style.TextAppearanceSecondaryItem);
FontManager.getInstance(context).applyFont(mTitle);
mTitle.setText(DataUtils.getFormattedSnippet(data.getSnippet()));
if (data.hasAlert()) {
mAlert.setImageResource(R.drawable.clock);
@ -97,6 +100,7 @@ public class NotesListItem extends LinearLayout {
} else {
mCallName.setVisibility(View.GONE);
mTitle.setTextAppearance(context, R.style.TextAppearancePrimaryItem);
FontManager.getInstance(context).applyFont(mTitle);
if (data.getType() == Notes.TYPE_FOLDER) {
mTitle.setText(data.getSnippet()
@ -131,18 +135,35 @@ public class NotesListItem extends LinearLayout {
*/
private void setBackground(NoteItemData data) {
int id = data.getBgColorId();
int resId;
if (data.getType() == Notes.TYPE_NOTE) {
if (data.isSingle() || data.isOneFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgSingleRes(id));
resId = NoteItemBgResources.getNoteBgSingleRes(id);
} else if (data.isLast()) {
setBackgroundResource(NoteItemBgResources.getNoteBgLastRes(id));
resId = NoteItemBgResources.getNoteBgLastRes(id);
} else if (data.isFirst() || data.isMultiFollowingFolder()) {
setBackgroundResource(NoteItemBgResources.getNoteBgFirstRes(id));
resId = NoteItemBgResources.getNoteBgFirstRes(id);
} else {
setBackgroundResource(NoteItemBgResources.getNoteBgNormalRes(id));
resId = NoteItemBgResources.getNoteBgNormalRes(id);
}
} else {
setBackgroundResource(NoteItemBgResources.getFolderBgRes());
resId = NoteItemBgResources.getFolderBgRes();
}
setBackgroundResource(resId);
// Apply tint for new colors
if (data.getType() == Notes.TYPE_NOTE && (id >= net.micode.notes.tool.ResourceParser.MIDNIGHT_BLACK || id < 0)) {
int color = net.micode.notes.tool.ResourceParser.getNoteBgColor(getContext(), id);
if (getBackground() != null) {
getBackground().setTint(color);
getBackground().setTintMode(android.graphics.PorterDuff.Mode.MULTIPLY);
}
} else {
// Ensure no tint for legacy colors (if view is recycled)
if (getBackground() != null) {
getBackground().clearColorFilter();
}
}
}

@ -1,659 +1,60 @@
/*
* Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.micode.notes.ui;
import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.databinding.SettingsHeaderBinding;
// Google Tasks同步功能已禁用
// import net.micode.notes.gtask.remote.GTaskSyncService;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.ui.PasswordActivity;
public class NotesPreferenceActivity extends AppCompatActivity {
/**
* Activity
* <p>
* Activity
* <ul>
* <li>Google Tasks</li>
* <li></li>
* <li></li>
* </ul>
* </p>
* <p>
* PreferenceActivity使SharedPreferences
* GTaskReceiver广
* </p>
*/
public class NotesPreferenceActivity extends PreferenceActivity {
/**
* SharedPreferences
*/
public static final String PREFERENCE_NAME = "notes_preferences";
/**
* SharedPreferences
*/
public static final String PREFERENCE_SYNC_ACCOUNT_NAME = "pref_key_account_name";
/**
* SharedPreferences
*/
public static final String PREFERENCE_LAST_SYNC_TIME = "pref_last_sync_time";
/**
* SharedPreferences
*/
public static final String PREFERENCE_SET_BG_COLOR_KEY = "pref_key_bg_random_appear";
public static final String PREFERENCE_SECURITY_KEY = "pref_key_security";
public static final int REQUEST_CODE_CHECK_PASSWORD = 104;
/**
* Preference
*/
private static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
/**
* Intent
*/
private static final String AUTHORITIES_FILTER_KEY = "authorities";
/**
* PreferenceCategory
*/
private PreferenceCategory mAccountCategory;
/**
* 广
*/
private GTaskReceiver mReceiver;
/**
*
*/
private SettingsHeaderBinding mHeaderBinding;
/**
*
*/
private Account[] mOriAccounts;
/**
*
*/
private boolean mHasAddedAccount;
/**
* Activity
* <p>
*
* <ul>
* <li>ActionBar</li>
* <li>preferences.xml</li>
* <li>广</li>
* <li></li>
* </ul>
* </p>
* @param icicle
*/
@Override
protected void onCreate(Bundle icicle) {
super.onCreate(icicle);
/* using the app icon for navigation */
getActionBar().setDisplayHomeAsUpEnabled(true);
addPreferencesFromResource(R.xml.preferences);
mAccountCategory = (PreferenceCategory) findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
// Google Tasks同步功能已禁用
// mReceiver = new GTaskReceiver();
// IntentFilter filter = new IntentFilter();
// filter.addAction(GTaskSyncService.GTASK_SERVICE_BROADCAST_NAME);
//registerReceiver(mReceiver, filter);
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU) {
// // Android 13 (API 33) 及以上版本需要指定导出标志
// registerReceiver(mReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
// } else {
// // Android 12 及以下版本使用旧方法
// registerReceiver(mReceiver, filter);
// }
mOriAccounts = null;
mHeaderBinding = SettingsHeaderBinding.inflate(getLayoutInflater());
getListView().addHeaderView(mHeaderBinding.getRoot(), null, true);
loadSecurityPreference();
}
/**
* Activity
* <p>
* Google
* UI
* </p>
*/
@Override
protected void onResume() {
super.onResume();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
// need to set sync account automatically if user has added a new
// account
if (mHasAddedAccount) {
Account[] accounts = getGoogleAccounts();
if (mOriAccounts != null && accounts.length > mOriAccounts.length) {
for (Account accountNew : accounts) {
boolean found = false;
for (Account accountOld : mOriAccounts) {
if (TextUtils.equals(accountOld.name, accountNew.name)) {
found = true;
break;
}
}
if (!found) {
setSyncAccount(accountNew.name);
break;
}
}
}
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.preferences_title);
}
refreshUI();
}
/**
* Activity
* <p>
* 广
* </p>
*/
@Override
protected void onDestroy() {
// Google Tasks同步功能已禁用
// if (mReceiver != null) {
// unregisterReceiver(mReceiver);
// }
mHeaderBinding = null;
super.onDestroy();
}
private void loadSecurityPreference() {
Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY);
if (securityPref != null) {
securityPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!SecurityManager.getInstance(NotesPreferenceActivity.this).isPasswordSet()) {
showSetPasswordDialog();
} else {
Intent intent = new Intent(NotesPreferenceActivity.this, PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD);
startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD);
}
return true;
}
});
}
}
private void showSetPasswordDialog() {
new AlertDialog.Builder(this)
.setTitle("设置密码")
.setItems(new String[]{"数字锁", "手势锁"}, (dialog, which) -> {
int type = (which == 0) ? SecurityManager.TYPE_PIN : SecurityManager.TYPE_PATTERN;
Intent intent = new Intent(this, PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_SETUP_PASSWORD);
intent.putExtra(PasswordActivity.EXTRA_PASSWORD_TYPE, type);
startActivity(intent);
})
.show();
}
private void showManagePasswordDialog() {
new AlertDialog.Builder(this)
.setTitle("管理密码")
.setItems(new String[]{"更改密码", "取消密码"}, (dialog, which) -> {
if (which == 0) { // Change
showSetPasswordDialog();
} else { // Remove
SecurityManager.getInstance(this).removePassword();
Toast.makeText(this, "密码已取消", Toast.LENGTH_SHORT).show();
}
})
.show();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHECK_PASSWORD && resultCode == RESULT_OK) {
showManagePasswordDialog();
if (savedInstanceState == null) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings_container, new SettingsFragment())
.commit();
}
}
/**
*
* <p>
* Preference
* Preference
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
private void loadAccountPreference() {
mAccountCategory.removeAll();
Preference accountPref = new Preference(this);
final String defaultAccount = getSyncAccountName(this);
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
// Google Tasks同步功能已禁用
// if (!GTaskSyncService.isSyncing()) {
// if (TextUtils.isEmpty(defaultAccount)) {
// // first time to set account
// showSelectAccountAlertDialog();
// } else {
// // if account has already been set, we need to promp
// // user about risk
// showChangeAccountConfirmAlertDialog();
// }
// } else {
// Toast.makeText(NotesPreferenceActivity.this,
// R.string.preferences_toast_cannot_change_account, Toast.LENGTH_SHORT)
// .show();
// }
Toast.makeText(NotesPreferenceActivity.this,
"Google Tasks同步功能已禁用", Toast.LENGTH_SHORT)
.show();
return true;
}
});
mAccountCategory.addPreference(accountPref);
loadSyncButton();
}
/**
*
* <p>
*
* <ul>
* <li>"取消同步"</li>
* <li>"立即同步"</li>
* </ul>
*
* </p>
*/
private void loadSyncButton() {
Button syncButton = mHeaderBinding.preferenceSyncButton;
TextView lastSyncTimeView = mHeaderBinding.prefenereceSyncStatusTextview;
Button syncButton = findViewById(R.id.preference_sync_button);
TextView lastSyncTimeView = findViewById(R.id.prefenerece_sync_status_textview);
// Google Tasks同步功能已禁用
// set button state
// if (GTaskSyncService.isSyncing()) {
// syncButton.setText(getString(R.string.preferences_button_sync_cancel));
// syncButton.setOnClickListener(new View.OnClickListener() {
// public void onClick(View v) {
// GTaskSyncService.cancelSync(NotesPreferenceActivity.this);
// }
// });
// } else {
// syncButton.setText(getString(R.string.preferences_button_sync_immediately));
// syncButton.setOnClickListener(new View.OnClickListener() {
// public void onClick(View v) {
// GTaskSyncService.startSync(NotesPreferenceActivity.this);
// }
// });
// }
// syncButton.setEnabled(!TextUtils.isEmpty(getSyncAccountName(this)));
// 禁用同步按钮
syncButton.setEnabled(false);
syncButton.setText("同步功能已禁用");
// set last sync time
// if (GTaskSyncService.isSyncing()) {
// lastSyncTimeView.setText(GTaskSyncService.getProgressString());
// lastSyncTimeView.setVisibility(View.VISIBLE);
// } else {
// long lastSyncTime = getLastSyncTime(this);
// if (lastSyncTime != 0) {
// lastSyncTimeView.setText(getString(R.string.preferences_last_sync_time,
// DateFormat.format(getString(R.string.preferences_last_sync_time_format),
// lastSyncTime)));
// lastSyncTimeView.setVisibility(View.VISIBLE);
// } else {
// lastSyncTimeView.setVisibility(View.GONE);
// }
// }
lastSyncTimeView.setText("Google Tasks同步功能已禁用");
lastSyncTimeView.setVisibility(View.VISIBLE);
}
/**
* UI
* <p>
*
* </p>
*/
private void refreshUI() {
loadAccountPreference();
loadSyncButton();
}
/**
*
* <p>
* Google
* "添加账户"
* </p>
*/
private void showSelectAccountAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_select_account_title));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_select_account_tips));
dialogBuilder.setCustomTitle(titleView);
dialogBuilder.setPositiveButton(null, null);
Account[] accounts = getGoogleAccounts();
String defAccount = getSyncAccountName(this);
mOriAccounts = accounts;
mHasAddedAccount = false;
if (accounts.length > 0) {
CharSequence[] items = new CharSequence[accounts.length];
final CharSequence[] itemMapping = items;
int checkedItem = -1;
int index = 0;
for (Account account : accounts) {
if (TextUtils.equals(account.name, defAccount)) {
checkedItem = index;
}
items[index++] = account.name;
}
dialogBuilder.setSingleChoiceItems(items, checkedItem,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
setSyncAccount(itemMapping[which].toString());
dialog.dismiss();
refreshUI();
}
});
}
View addAccountView = LayoutInflater.from(this).inflate(R.layout.add_account_text, null);
dialogBuilder.setView(addAccountView);
final AlertDialog dialog = dialogBuilder.show();
addAccountView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
mHasAddedAccount = true;
Intent intent = new Intent("android.settings.ADD_ACCOUNT_SETTINGS");
intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
"gmail-ls"
});
startActivityForResult(intent, -1);
dialog.dismiss();
}
});
}
/**
*
* <p>
*
* <ul>
* <li></li>
* <li></li>
* <li></li>
* </ul>
* </p>
*/
private void showChangeAccountConfirmAlertDialog() {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this);
View titleView = LayoutInflater.from(this).inflate(R.layout.account_dialog_title, null);
TextView titleTextView = (TextView) titleView.findViewById(R.id.account_dialog_title);
titleTextView.setText(getString(R.string.preferences_dialog_change_account_title,
getSyncAccountName(this)));
TextView subtitleTextView = (TextView) titleView.findViewById(R.id.account_dialog_subtitle);
subtitleTextView.setText(getString(R.string.preferences_dialog_change_account_warn_msg));
dialogBuilder.setCustomTitle(titleView);
CharSequence[] menuItemArray = new CharSequence[] {
getString(R.string.preferences_menu_change_account),
getString(R.string.preferences_menu_remove_account),
getString(R.string.preferences_menu_cancel)
};
dialogBuilder.setItems(menuItemArray, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
if (which == 0) {
showSelectAccountAlertDialog();
} else if (which == 1) {
removeSyncAccount();
refreshUI();
}
}
});
dialogBuilder.show();
}
/**
* Google
* <p>
* AccountManager"com.google"
* </p>
* @return Google
*/
private Account[] getGoogleAccounts() {
AccountManager accountManager = AccountManager.get(this);
return accountManager.getAccountsByType("com.google");
}
/**
*
* <p>
* SharedPreferences
* <ul>
* <li></li>
* <li>GTASK_IDSYNC_ID</li>
* </ul>
* </p>
* @param account
*/
private void setSyncAccount(String account) {
if (!getSyncAccountName(this).equals(account)) {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (account != null) {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, account);
} else {
editor.putString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
editor.commit();
// clean up last sync time
setLastSyncTime(this, 0);
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
Toast.makeText(NotesPreferenceActivity.this,
getString(R.string.preferences_toast_success_set_accout, account),
Toast.LENGTH_SHORT).show();
}
}
/**
*
* <p>
* SharedPreferences
* GTASK_IDSYNC_ID
* </p>
*/
private void removeSyncAccount() {
SharedPreferences settings = getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
if (settings.contains(PREFERENCE_SYNC_ACCOUNT_NAME)) {
editor.remove(PREFERENCE_SYNC_ACCOUNT_NAME);
}
if (settings.contains(PREFERENCE_LAST_SYNC_TIME)) {
editor.remove(PREFERENCE_LAST_SYNC_TIME);
}
editor.commit();
// clean up local gtask related info
new Thread(new Runnable() {
public void run() {
ContentValues values = new ContentValues();
values.put(NoteColumns.GTASK_ID, "");
values.put(NoteColumns.SYNC_ID, 0);
getContentResolver().update(Notes.CONTENT_NOTE_URI, values, null, null);
}
}).start();
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @return
*/
public static String getSyncAccountName(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
public static String getSyncAccountName(android.content.Context context) {
android.content.SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME, android.content.Context.MODE_PRIVATE);
return settings.getString(PREFERENCE_SYNC_ACCOUNT_NAME, "");
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @param time
*/
public static void setLastSyncTime(Context context, long time) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = settings.edit();
editor.putLong(PREFERENCE_LAST_SYNC_TIME, time);
editor.commit();
}
/**
*
* <p>
* SharedPreferences
* </p>
* @param context
* @return 0
*/
public static long getLastSyncTime(Context context) {
SharedPreferences settings = context.getSharedPreferences(PREFERENCE_NAME,
Context.MODE_PRIVATE);
return settings.getLong(PREFERENCE_LAST_SYNC_TIME, 0);
}
/**
* 广
* <p>
* GTaskSyncService广UI
* </p>
*/
private class GTaskReceiver extends BroadcastReceiver {
/**
* 广
* <p>
* 广UI
* </p>
* @param context
* @param intent 广Intent
*/
@Override
public void onReceive(Context context, Intent intent) {
refreshUI();
// Google Tasks同步功能已禁用
// if (intent.getBooleanExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_IS_SYNCING, false)) {
// TextView syncStatus = (TextView) findViewById(R.id.prefenerece_sync_status_textview);
// syncStatus.setText(intent
// .getStringExtra(GTaskSyncService.GTASK_SERVICE_BROADCAST_PROGRESS_MSG));
// }
}
}
/**
*
* <p>
* ActionBar
*
* </p>
* @param item
* @return truefalse
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:

@ -0,0 +1,117 @@
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import net.micode.notes.R;
import net.micode.notes.tool.SecurityManager;
import net.micode.notes.data.ThemeRepository;
import androidx.preference.ListPreference;
import static android.app.Activity.RESULT_OK;
public class SettingsFragment extends PreferenceFragmentCompat {
public static final String PREFERENCE_SYNC_ACCOUNT_KEY = "pref_sync_account_key";
public static final String PREFERENCE_SECURITY_KEY = "pref_key_security";
public static final String PREFERENCE_THEME_MODE = "pref_theme_mode";
public static final int REQUEST_CODE_CHECK_PASSWORD = 104;
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.preferences, rootKey);
loadThemePreference();
loadSecurityPreference();
loadAccountPreference();
}
private void loadThemePreference() {
ListPreference themePref = findPreference(PREFERENCE_THEME_MODE);
if (themePref != null) {
themePref.setOnPreferenceChangeListener((preference, newValue) -> {
ThemeRepository.applyTheme((String) newValue);
return true;
});
}
}
private void loadSecurityPreference() {
Preference securityPref = findPreference(PREFERENCE_SECURITY_KEY);
if (securityPref != null) {
securityPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
if (!SecurityManager.getInstance(getActivity()).isPasswordSet()) {
showSetPasswordDialog();
} else {
Intent intent = new Intent(getActivity(), PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD);
startActivityForResult(intent, REQUEST_CODE_CHECK_PASSWORD);
}
return true;
}
});
}
}
private void loadAccountPreference() {
androidx.preference.PreferenceCategory accountCategory = findPreference(PREFERENCE_SYNC_ACCOUNT_KEY);
if (accountCategory != null) {
accountCategory.removeAll();
Preference accountPref = new Preference(getContext());
accountPref.setTitle(getString(R.string.preferences_account_title));
accountPref.setSummary(getString(R.string.preferences_account_summary));
accountPref.setOnPreferenceClickListener(preference -> {
Toast.makeText(getActivity(), "Google Tasks同步功能已禁用", Toast.LENGTH_SHORT).show();
return true;
});
accountCategory.addPreference(accountPref);
}
}
private void showSetPasswordDialog() {
new AlertDialog.Builder(getActivity())
.setTitle("设置密码")
.setItems(new String[]{"数字锁", "手势锁"}, (dialog, which) -> {
int type = (which == 0) ? SecurityManager.TYPE_PIN : SecurityManager.TYPE_PATTERN;
Intent intent = new Intent(getActivity(), PasswordActivity.class);
intent.setAction(PasswordActivity.ACTION_SETUP_PASSWORD);
intent.putExtra(PasswordActivity.EXTRA_PASSWORD_TYPE, type);
startActivity(intent);
})
.show();
}
private void showManagePasswordDialog() {
new AlertDialog.Builder(getActivity())
.setTitle("管理密码")
.setItems(new String[]{"更改密码", "取消密码"}, (dialog, which) -> {
if (which == 0) { // Change
showSetPasswordDialog();
} else { // Remove
SecurityManager.getInstance(getActivity()).removePassword();
Toast.makeText(getActivity(), "密码已取消", Toast.LENGTH_SHORT).show();
}
})
.show();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_CHECK_PASSWORD && resultCode == RESULT_OK) {
showManagePasswordDialog();
}
}
}

@ -108,6 +108,11 @@ public class SidebarFragment extends Fragment {
*/
void onExportSelected();
/**
*
*/
void onTemplateSelected();
/**
*
*/
@ -231,6 +236,12 @@ public class SidebarFragment extends Fragment {
}
});
binding.menuTemplates.setOnClickListener(v -> {
if (listener != null) {
listener.onTemplateSelected();
}
});
binding.menuSettings.setOnClickListener(v -> {
if (listener != null) {
listener.onSettingsSelected();

@ -0,0 +1,205 @@
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.app.DatePickerDialog;
import android.app.TimePickerDialog;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.RadioGroup;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import net.micode.notes.model.Task;
import java.util.Calendar;
public class TaskEditActivity extends AppCompatActivity {
private EditText contentEdit;
private ImageView alarmBtn;
private ImageView tagBtn;
private Button doneBtn;
private Task task;
private long taskId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_edit);
contentEdit = findViewById(R.id.task_edit_content);
alarmBtn = findViewById(R.id.btn_alarm);
tagBtn = findViewById(R.id.btn_tag);
doneBtn = findViewById(R.id.btn_done);
Intent intent = getIntent();
taskId = intent.getLongExtra(Intent.EXTRA_UID, 0);
if (taskId > 0) {
loadTask();
} else {
task = new Task();
}
setupListeners();
}
private void loadTask() {
new Thread(() -> {
Cursor cursor = getContentResolver().query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.ID + "=?",
new String[]{String.valueOf(taskId)},
null
);
if (cursor != null) {
if (cursor.moveToFirst()) {
task = Task.fromCursor(cursor);
runOnUiThread(() -> {
contentEdit.setText(task.snippet);
contentEdit.setSelection(task.snippet.length());
});
} else {
task = new Task();
}
cursor.close();
} else {
task = new Task();
}
}).start();
}
private void setupListeners() {
doneBtn.setOnClickListener(v -> {
saveTask();
setResult(RESULT_OK);
finish();
});
alarmBtn.setOnClickListener(v -> {
showAlarmDialog();
});
tagBtn.setOnClickListener(v -> {
showTagDialog();
});
}
@Override
public void onBackPressed() {
if (saveTask()) {
setResult(RESULT_OK);
}
super.onBackPressed();
}
private boolean saveTask() {
String content = contentEdit.getText().toString();
if (content.trim().length() == 0) {
if (task.id == 0) {
return false;
}
}
task.snippet = content;
task.save(this);
// Register Alarm if needed.
if (task.alertDate > 0 && task.alertDate > System.currentTimeMillis()) {
Intent intent = new Intent(this, AlarmReceiver.class);
intent.setData(android.content.ContentUris.withAppendedId(Notes.CONTENT_NOTE_URI, task.id));
android.app.PendingIntent pendingIntent = android.app.PendingIntent.getBroadcast(this, 0, intent, android.app.PendingIntent.FLAG_UPDATE_CURRENT | android.app.PendingIntent.FLAG_IMMUTABLE);
android.app.AlarmManager alarmManager = (android.app.AlarmManager) getSystemService(ALARM_SERVICE);
alarmManager.set(android.app.AlarmManager.RTC_WAKEUP, task.alertDate, pendingIntent);
}
return true;
}
private void showAlarmDialog() {
final Calendar c = Calendar.getInstance();
if (task.alertDate > 0) {
c.setTimeInMillis(task.alertDate);
}
new DatePickerDialog(this, (view, year, month, dayOfMonth) -> {
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
new TimePickerDialog(this, (view1, hourOfDay, minute) -> {
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
c.set(Calendar.MINUTE, minute);
c.set(Calendar.SECOND, 0);
task.alertDate = c.getTimeInMillis();
Toast.makeText(this, "Alarm set", Toast.LENGTH_SHORT).show();
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
}
private void showTagDialog() {
View view = LayoutInflater.from(this).inflate(R.layout.dialog_task_tag, null);
RadioGroup priorityGroup = view.findViewById(R.id.priority_group);
TextView dateText = view.findViewById(R.id.date_text);
Button dateBtn = view.findViewById(R.id.btn_set_date);
if (task.priority == Task.PRIORITY_HIGH) priorityGroup.check(R.id.priority_high);
else if (task.priority == Task.PRIORITY_NORMAL) priorityGroup.check(R.id.priority_mid);
else priorityGroup.check(R.id.priority_low);
final Calendar c = Calendar.getInstance();
if (task.dueDate > 0) {
c.setTimeInMillis(task.dueDate);
dateText.setText(android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", c));
} else {
dateText.setText("No Due Date");
}
dateBtn.setOnClickListener(v -> {
new DatePickerDialog(this, (dView, year, month, dayOfMonth) -> {
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
new TimePickerDialog(this, (tView, hourOfDay, minute) -> {
c.set(Calendar.HOUR_OF_DAY, hourOfDay);
c.set(Calendar.MINUTE, minute);
task.dueDate = c.getTimeInMillis();
dateText.setText(android.text.format.DateFormat.format("yyyy-MM-dd HH:mm", c));
}, c.get(Calendar.HOUR_OF_DAY), c.get(Calendar.MINUTE), true).show();
}, c.get(Calendar.YEAR), c.get(Calendar.MONTH), c.get(Calendar.DAY_OF_MONTH)).show();
});
new AlertDialog.Builder(this)
.setTitle("Set Tag")
.setView(view)
.setPositiveButton("OK", (dialog, which) -> {
int id = priorityGroup.getCheckedRadioButtonId();
if (id == R.id.priority_high) task.priority = Task.PRIORITY_HIGH;
else if (id == R.id.priority_mid) task.priority = Task.PRIORITY_NORMAL;
else task.priority = Task.PRIORITY_LOW;
})
.setNegativeButton("Cancel", null)
.show();
}
}

@ -0,0 +1,156 @@
package net.micode.notes.ui;
import android.app.Activity;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.view.MenuItem;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
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.model.Task;
import java.util.ArrayList;
import java.util.List;
public class TaskListActivity extends AppCompatActivity implements TaskListAdapter.OnTaskItemClickListener {
private RecyclerView recyclerView;
private TaskListAdapter adapter;
private FloatingActionButton fab;
private static final int REQUEST_EDIT_TASK = 1001;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_task_list);
// Initialize Toolbar
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
// Remove default title
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayShowTitleEnabled(false);
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
// Setup custom "Notes" navigation
View notesTitle = findViewById(R.id.tv_toolbar_title_notes);
if (notesTitle != null) {
notesTitle.setOnClickListener(v -> {
finish();
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
});
}
// Setup Navigation Icon (Back Button) - REMOVED as per new requirement
// toolbar.setNavigationOnClickListener(v -> {
// finish();
// overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
// });
recyclerView = findViewById(R.id.task_list_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new TaskListAdapter(this, this);
recyclerView.setAdapter(adapter);
fab = findViewById(R.id.btn_new_task);
fab.setOnClickListener(v -> {
Intent intent = new Intent(TaskListActivity.this, TaskEditActivity.class);
startActivityForResult(intent, REQUEST_EDIT_TASK);
});
}
@Override
protected void onResume() {
super.onResume();
loadTasks();
}
private void loadTasks() {
new Thread(() -> {
Cursor cursor = getContentResolver().query(
Notes.CONTENT_NOTE_URI,
null,
NoteColumns.TYPE + "=?",
new String[]{String.valueOf(Notes.TYPE_TASK)},
null
);
List<Task> tasks = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
tasks.add(Task.fromCursor(cursor));
}
cursor.close();
}
android.util.Log.d("TaskListActivity", "Loaded tasks count: " + tasks.size());
runOnUiThread(() -> adapter.setTasks(tasks));
}).start();
}
@Override
public void onItemClick(Task task) {
Intent intent = new Intent(this, TaskEditActivity.class);
intent.putExtra(Intent.EXTRA_UID, task.id);
startActivityForResult(intent, REQUEST_EDIT_TASK);
}
@Override
public void onCheckBoxClick(Task task) {
task.status = (task.status == Task.STATUS_ACTIVE) ? Task.STATUS_COMPLETED : Task.STATUS_ACTIVE;
if (task.status == Task.STATUS_COMPLETED) {
task.finishedTime = System.currentTimeMillis();
} else {
task.finishedTime = 0;
}
new Thread(() -> {
task.save(this);
runOnUiThread(() -> loadTasks()); // Reload to sort
}).start();
}
@Override
public boolean onCreateOptionsMenu(android.view.Menu menu) {
// Removed menu_notes as per requirement, keeping empty or future menus
// getMenuInflater().inflate(R.menu.task_list, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
finish();
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
return true;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onBackPressed() {
super.onBackPressed();
overridePendingTransition(R.anim.slide_in_left, R.anim.slide_out_right);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_EDIT_TASK && resultCode == RESULT_OK) {
loadTasks();
}
}
}

@ -0,0 +1,168 @@
package net.micode.notes.ui;
import android.content.Context;
import android.graphics.Paint;
import android.text.format.DateFormat;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.model.Task;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
public class TaskListAdapter extends RecyclerView.Adapter<TaskListAdapter.TaskViewHolder> {
private List<Task> tasks = new ArrayList<>();
private Context context;
private OnTaskItemClickListener listener;
public interface OnTaskItemClickListener {
void onItemClick(Task task);
void onCheckBoxClick(Task task);
}
public TaskListAdapter(Context context, OnTaskItemClickListener listener) {
this.context = context;
this.listener = listener;
}
public void setTasks(List<Task> newTasks) {
this.tasks = new ArrayList<>(newTasks);
sortTasks();
notifyDataSetChanged();
}
private void sortTasks() {
Collections.sort(tasks, new Comparator<Task>() {
@Override
public int compare(Task t1, Task t2) {
// 1. Status: Active (0) < Completed (1)
if (t1.status != t2.status) {
return Integer.compare(t1.status, t2.status);
}
// 2. If both Active
if (t1.status == Task.STATUS_ACTIVE) {
// Priority: High (2) > Mid (1) > Low (0) -> DESC
if (t1.priority != t2.priority) {
return Integer.compare(t2.priority, t1.priority);
}
if (t1.dueDate != t2.dueDate) {
if (t1.dueDate == 0) return 1; // t1 no date -> bottom
if (t2.dueDate == 0) return -1; // t2 no date -> bottom
return Long.compare(t1.dueDate, t2.dueDate); // Early date first
}
// Creation Date (Fallback)
return Long.compare(t2.createdDate, t1.createdDate);
}
// 3. If both Completed
// DESC sort by finishedTime.
return Long.compare(t2.finishedTime, t1.finishedTime);
}
});
}
@NonNull
@Override
public TaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(context).inflate(R.layout.task_list_item, parent, false);
return new TaskViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull TaskViewHolder holder, int position) {
Task task = tasks.get(position);
holder.bind(task);
}
@Override
public int getItemCount() {
return tasks.size();
}
class TaskViewHolder extends RecyclerView.ViewHolder {
CheckBox checkBox;
TextView content;
TextView priority;
TextView date;
ImageView alarm;
public TaskViewHolder(@NonNull View itemView) {
super(itemView);
checkBox = itemView.findViewById(R.id.task_checkbox);
content = itemView.findViewById(R.id.task_content);
priority = itemView.findViewById(R.id.task_priority);
date = itemView.findViewById(R.id.task_date);
alarm = itemView.findViewById(R.id.task_alarm_icon);
itemView.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onItemClick(tasks.get(getAdapterPosition()));
}
});
checkBox.setOnClickListener(v -> {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onCheckBoxClick(tasks.get(getAdapterPosition()));
}
});
}
public void bind(Task task) {
content.setText(task.snippet);
checkBox.setChecked(task.status == Task.STATUS_COMPLETED);
if (task.status == Task.STATUS_COMPLETED) {
content.setPaintFlags(content.getPaintFlags() | Paint.STRIKE_THRU_TEXT_FLAG);
content.setAlpha(0.5f);
} else {
content.setPaintFlags(content.getPaintFlags() & (~Paint.STRIKE_THRU_TEXT_FLAG));
content.setAlpha(1.0f);
}
// Priority
if (task.priority == Task.PRIORITY_HIGH) {
priority.setVisibility(View.VISIBLE);
priority.setText("HIGH");
priority.setBackgroundColor(0xFFFFCDD2); // Light Red
priority.setTextColor(0xFFB71C1C); // Dark Red
} else if (task.priority == Task.PRIORITY_NORMAL) {
priority.setVisibility(View.VISIBLE);
priority.setText("MED");
priority.setBackgroundColor(0xFFFFF9C4); // Light Yellow
priority.setTextColor(0xFFF57F17); // Dark Yellow
} else {
priority.setVisibility(View.GONE);
}
// Due Date
if (task.dueDate > 0) {
date.setVisibility(View.VISIBLE);
date.setText(DateFormat.format("MM/dd HH:mm", task.dueDate));
} else {
date.setVisibility(View.GONE);
}
// Alarm
if (task.alertDate > 0) {
alarm.setVisibility(View.VISIBLE);
} else {
alarm.setVisibility(View.GONE);
}
}
}
}

@ -703,6 +703,36 @@ public class NotesListViewModel extends ViewModel {
return currentFolderId == Notes.ID_TRASH_FOLER;
}
/**
*
*
* @return true
*/
public boolean isTemplateMode() {
if (currentFolderId == Notes.ID_TEMPLATE_FOLDER) return true;
List<NotesRepository.NoteInfo> path = folderPathLiveData.getValue();
if (path != null) {
for (NotesRepository.NoteInfo info : path) {
if (info.getId() == Notes.ID_TEMPLATE_FOLDER) return true;
}
}
return false;
}
/**
*
*
* @param templateId ID
* @param callback
*/
public void applyTemplate(long templateId, NotesRepository.Callback<Long> callback) {
// 应用模板到根目录(或者让用户选择,这里简化为根目录)
// 实际上应该让用户选择,或者默认应用到当前上下文(如果是从新建笔记进入)
// 这里假设是从模板列表点击进入,则应用到根目录(或默认目录)
// 更好的逻辑是applyTemplate(templateId, Notes.ID_ROOT_FOLDER)
repository.applyTemplate(templateId, Notes.ID_ROOT_FOLDER, callback);
}
/**
*
* <p>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="-100%"
android:toXDelta="0%"
android:interpolator="@android:anim/decelerate_interpolator"/>
</set>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="100%"
android:toXDelta="0%"
android:interpolator="@android:anim/decelerate_interpolator"/>
</set>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:toXDelta="-100%"
android:interpolator="@android:anim/accelerate_interpolator"/>
</set>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate
android:duration="@android:integer/config_mediumAnimTime"
android:fromXDelta="0%"
android:toXDelta="100%"
android:interpolator="@android:anim/accelerate_interpolator"/>
</set>

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true" android:color="#88555555" />
<item android:state_selected="true" android:color="#ff999999" />
<item android:color="#ff000000" />
</selector>

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright (c) 2010-2011, The MiCode Open Source Community (www.micode.net)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="#50000000" />
</selector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M15.6,11.79c0.97,-0.67 1.65,-1.77 1.65,-2.79 0,-2.26 -1.75,-4 -4,-4H7v14h7c2.09,0 3.85,-1.75 3.85,-3.75 0,-1.58 -0.95,-2.9 -2.25,-3.46zM10,7.5h3c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5h-3v-3zM13.5,16.5h-3.5v-3h3.5c0.83,0 1.5,0.67 1.5,1.5s-0.67,1.5 -1.5,1.5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.4,16.6L4.8,12l4.6,-4.6L8,6l-6,6 6,6 1.4,-1.4zM14.6,16.6l4.6,-4.6 -4.6,-4.6L16,6l6,6 -6,6 -1.4,-1.4z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M16.56,8.94L7.62,0 6.21,1.41l2.38,2.38 -5.15,5.15c-0.59,0.59 -0.59,1.54 0,2.12l5.5,5.5c0.29,0.29 0.68,0.44 1.06,0.44s0.77,-0.15 1.06,-0.44l5.5,-5.5c0.59,-0.58 0.59,-1.53 0,-2.12zM5.21,10L10,5.21 14.79,10H5.21zM19,11.5s-2,2.17 -2,3.5c0,1.1 0.9,2 2,2s2,-0.9 2,-2c0,-1.33 -2,-3.5 -2,-3.5z"/>
<path
android:fillColor="#FF000000"
android:pathData="M0,20h24v4H0z"/>
</vector>

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M0,20h24v4H0z"/>
<path
android:fillColor="#FF000000"
android:pathData="M11,3L5.5,17h2.25l1.12,-3h6.25l1.12,3h2.25L13,3h-2zM10,11l2,-5.5 2,5.5h-4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M5,4v3h5.5v12h3V7H19V4z"/>
</vector>

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

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M4,10.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,4.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM4,16.5c-0.83,0 -1.5,0.67 -1.5,1.5s0.67,1.5 1.5,1.5 1.5,-0.67 1.5,-1.5 -0.67,-1.5 -1.5,-1.5zM8,19h12v-2H8v2zM8,13h12v-2H8v2zM8,5v2h12V5H8z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,17h3l2,-4V7H5v6h3l-2,4zM14,17h3l2,-4V7h-6v6h3l-2,4z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M10,19h4v-3h-4v3zM5,4v3h5v3h4V7h5V4H5zM3,14h18v-2H3v2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,17c3.31,0 6,-2.69 6,-6V3h-2.5v8c0,1.93 -1.57,3.5 -3.5,3.5S8.5,12.93 8.5,11V3H6v8c0,3.31 2.69,6 6,6zM5,19v2h14v-2H5z"/>
</vector>

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M19,13H5v-2h14v2z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4V7H7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9H7c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2H8v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4V17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M14,2H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6zM16,18H8v-2h8v2zM16,14H8v-2h8v2zM13,9V3.5L18.5,9H13z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M18.4,10.6C16.55,8.99 14.15,8 11.5,8c-4.65,0 -8.58,3.03 -9.96,7.22L3.9,16c1.05,-3.19 4.05,-5.5 7.6,-5.5 1.95,0 3.73,0.72 5.12,1.88L13,16h9V7l-3.6,3.6z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M2.5,4v3h5v12h3V7h5V4H2.5zM21.5,9h-9v3h3v7h3v-7h3V9z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,3h-4.18C14.4,1.84 13.3,1 12,1c-1.3,0 -2.4,0.84 -2.82,2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2zM12,3c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1zM14,17H7v-2h7v2zM17,13H7v-2h10v2zM17,9H7V7h10v2z"/>
</vector>

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

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#000000">
<path
android:fillColor="@android:color/white"
android:pathData="M12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9c0.83,0 1.5,-0.67 1.5,-1.5 0,-0.39 -0.15,-0.74 -0.39,-1.01 -0.23,-0.26 -0.38,-0.61 -0.38,-0.99 0,-0.83 0.67,-1.5 1.5,-1.5H16c2.76,0 5,-2.24 5,-5 0,-4.42 -4.03,-8 -9,-8zM6.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S5.67,9 6.5,9 8,9.67 8,10.5 7.33,12 6.5,12zM9.5,6c-0.83,0 -1.5,-0.67 -1.5,-1.5S8.67,3 9.5,3s1.5,0.67 1.5,1.5S10.33,6 9.5,6zM14.5,6c-0.83,0 -1.5,-0.67 -1.5,-1.5S13.67,3 14.5,3s1.5,0.67 1.5,1.5S15.33,6 14.5,6zM17.5,12c-0.83,0 -1.5,-0.67 -1.5,-1.5S16.67,9 17.5,9s1.5,0.67 1.5,1.5S18.33,12 17.5,12z"/>
</vector>

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/ThemeOverlay.Material3.Light">
<!-- 搜索框 -->
<androidx.appcompat.widget.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionSearch|flagNoExtractUi"
app:iconifiedByDefault="false"
app:queryHint="@string/search_hint" />
</androidx.appcompat.widget.Toolbar>
<!-- 历史记录按钮 -->
<TextView
android:id="@+id/btn_show_history"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:padding="16dp"
android:text="@string/search_history_title"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:drawablePadding="8dp"
android:visibility="gone"
app:drawableEndCompat="@android:drawable/arrow_down_float" />
</com.google.android.material.appbar.AppBarLayout>
<!-- 搜索结果列表 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:scrollbars="vertical" />
<!-- 无结果提示 -->
<TextView
android:id="@+id/tv_no_result"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/search_no_results"
android:visibility="gone"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/settings_header" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/settings_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
</LinearLayout>

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white">
<LinearLayout
android:id="@+id/bottom_bar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_alignParentBottom="true"
android:orientation="horizontal"
android:gravity="center_vertical"
android:background="#F0F0F0"
android:paddingStart="16dp"
android:paddingEnd="16dp">
<ImageView
android:id="@+id/btn_alarm"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_lock_idle_alarm"
android:layout_marginEnd="24dp"
android:clickable="true"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<ImageView
android:id="@+id/btn_tag"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@drawable/ic_menu_rich_text"
android:layout_marginEnd="24dp"
android:clickable="true"
android:background="?android:attr/selectableItemBackgroundBorderless"/>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<Button
android:id="@+id/btn_done"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Done" />
</LinearLayout>
<EditText
android:id="@+id/task_edit_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/bottom_bar"
android:gravity="top|start"
android:padding="16dp"
android:background="@null"
android:hint="Enter task..."
android:textSize="18sp" />
</RelativeLayout>

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_color">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.Material3.ActionBar"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp">
<TextView
android:id="@+id/tv_toolbar_title_notes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Notes"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_gravity="start|center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/task_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="80dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/btn_new_task"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:src="@drawable/ic_add"
app:backgroundTint="@color/fab_color"
app:tint="@color/text_color_primary"
android:contentDescription="New Task" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_marginBottom="24dp"
app:cardCornerRadius="12dp"
app:strokeWidth="1dp"
app:strokeColor="?attr/colorOutline"
xmlns:app="http://schemas.android.com/apk/res-auto">
<View
android:id="@+id/view_color_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white" />
</com.google.android.material.card.MaterialCardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Red"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/sb_red"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:progressTint="@android:color/holo_red_light"
android:thumbTint="@android:color/holo_red_dark"
android:max="255" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Green"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/sb_green"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:progressTint="@android:color/holo_green_light"
android:thumbTint="@android:color/holo_green_dark"
android:max="255" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Blue"
android:layout_marginBottom="4dp"/>
<SeekBar
android:id="@+id/sb_blue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:progressTint="@android:color/holo_blue_light"
android:thumbTint="@android:color/holo_blue_dark"
android:max="255" />
</LinearLayout>

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Priority"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<RadioGroup
android:id="@+id/priority_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<RadioButton
android:id="@+id/priority_low"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Low"
android:layout_weight="1"/>
<RadioButton
android:id="@+id/priority_mid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Mid"
android:layout_weight="1"/>
<RadioButton
android:id="@+id/priority_high"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="High"
android:layout_weight="1"/>
</RadioGroup>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Due Date"
android:textStyle="bold"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/date_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="No Due Date" />
<Button
android:id="@+id/btn_set_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Set" />
</LinearLayout>
</LinearLayout>

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_margin="4dp"
android:padding="2dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/color_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:shapeAppearanceOverlay="@style/ShapeAppearance.Material3.Corner.Full"
app:strokeWidth="1dp"
app:strokeColor="@color/secondary_text_dark" />
<ImageView
android:id="@+id/check_view"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="center"
android:src="@drawable/selected"
android:visibility="gone" />
</FrameLayout>

@ -170,112 +170,16 @@
android:layout_marginTop="30dp"
android:layout_marginRight="8dp"
android:layout_gravity="top|right"
android:visibility="gone">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_yellow"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_yellow_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_marginRight="5dp"
android:focusable="false"
android:visibility="gone"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_blue"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_blue_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_white"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_white_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="2dp"
android:src="@drawable/selected" />
</FrameLayout>
android:visibility="gone"
android:orientation="horizontal">
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_green"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/iv_bg_green_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="5dp"
android:src="@drawable/selected" />
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<ImageView
android:id="@+id/iv_bg_red"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_bg_color_selector"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
<ImageView
android:id="@+id/iv_bg_red_select"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:focusable="false"
android:visibility="gone"
android:layout_marginRight="3dp"
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<LinearLayout
@ -438,5 +342,155 @@
android:src="@drawable/selected" />
</FrameLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/rich_text_selector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/font_size_selector_bg"
android:layout_gravity="bottom"
android:visibility="gone"
android:orientation="vertical">
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp">
<ImageButton
android:id="@+id/btn_bold"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_bold"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Bold"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_italic"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_italic"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Italic"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_underline"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_underline"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Underline"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_strikethrough"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_strikethrough"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Strikethrough"
android:scaleType="centerInside" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:background="#CCCCCC"/>
<ImageButton
android:id="@+id/btn_header"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_header"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Header"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_list"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_list_bulleted"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="List"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_quote"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_quote"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Quote"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_code"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_code"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Code"
android:scaleType="centerInside" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:background="#CCCCCC"/>
<ImageButton
android:id="@+id/btn_link"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_insert_link"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Link"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_divider"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_insert_divider"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Divider"
android:scaleType="centerInside" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginHorizontal="4dp"
android:background="#CCCCCC"/>
<ImageButton
android:id="@+id/btn_color_text"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_color_text"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Text Color"
android:scaleType="centerInside" />
<ImageButton
android:id="@+id/btn_color_fill"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_format_color_fill"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="Background Color"
android:scaleType="centerInside" />
</LinearLayout>
</HorizontalScrollView>
</LinearLayout>
</LinearLayout>
</LinearLayout>

@ -22,7 +22,7 @@
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/list_background">
android:background="@color/background_color">
<!-- 主内容区域 -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
@ -89,6 +89,7 @@
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:contentDescription="@string/notelist_menu_new"
app:backgroundTint="@color/fab_color"
app:srcCompat="@android:drawable/ic_input_add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="16dp"
android:gravity="center_vertical"
android:background="?android:attr/selectableItemBackground">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@android:drawable/ic_menu_recent_history"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@null" />
<TextView
android:id="@+id/tv_history_keyword"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/iv_delete_history"
android:layout_width="24dp"
android:layout_height="24dp"
android:padding="4dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
android:tint="?android:attr/textColorSecondary"
android:contentDescription="@string/menu_delete" />
</LinearLayout>

@ -137,6 +137,20 @@
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
<!-- 模板 -->
<TextView
android:id="@+id/menu_templates"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:drawableStart="@android:drawable/ic_menu_edit"
android:drawablePadding="12dp"
android:text="@string/menu_templates"
android:textSize="16sp"
android:textColor="@android:color/black"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical" />
<!-- 设置 -->
<TextView
android:id="@+id/menu_settings"

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical"
android:background="@color/bg_white"
android:layout_marginBottom="1dp">
<CheckBox
android:id="@+id/task_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:paddingStart="8dp"
android:paddingEnd="8dp">
<TextView
android:id="@+id/task_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textColor="@color/text_color_primary"
android:ellipsize="end"
android:maxLines="2" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="4dp"
android:gravity="center_vertical">
<TextView
android:id="@+id/task_priority"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:layout_marginEnd="8dp"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:background="#FFCDD2"
android:textColor="#B71C1C"
android:text="HIGH"
android:visibility="gone"/>
<TextView
android:id="@+id/task_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="12sp"
android:textColor="@color/text_color_secondary" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/task_alarm_icon"
android:layout_width="16dp"
android:layout_height="16dp"
android:src="@android:drawable/ic_lock_idle_alarm"
android:tint="@color/text_color_secondary"
android:visibility="gone" />
</LinearLayout>

@ -16,12 +16,45 @@
-->
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_new_note"
android:title="@string/notelist_menu_new"/>
<item
android:id="@+id/menu_rich_text"
android:title="@string/menu_rich_text"
android:icon="@drawable/ic_menu_rich_text"
app:showAsAction="always"/>
<item
android:id="@+id/menu_picture"
android:title="@string/menu_picture"
android:icon="@android:drawable/ic_menu_gallery"
app:showAsAction="ifRoom" />
<item
android:id="@+id/menu_undo"
android:title="@string/menu_undo"
android:icon="@drawable/ic_menu_undo"
app:showAsAction="always"/>
<item
android:id="@+id/menu_redo"
android:title="@string/menu_redo"
android:icon="@drawable/ic_menu_redo"
app:showAsAction="always"/>
<item
android:id="@+id/menu_clear_history"
android:title="@string/menu_clear_history"/>
<item
android:id="@+id/menu_save_as_template"
android:title="@string/menu_save_as_template"/>
<item
android:id="@+id/menu_delete"
android:title="@string/menu_delete"/>

@ -15,8 +15,15 @@
limitations under the License.
-->
<menu
xmlns:android="http://schemas.android.com/apk/res/android">
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_tasks"
android:title="@string/menu_tasks"
android:icon="@drawable/ic_menu_tasks"
app:showAsAction="always" />
<item
android:id="@+id/menu_new_folder"
android:title="@string/menu_create_folder"/>

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="background_color">#121212</color>
<color name="text_color_primary">#DEFFFFFF</color>
<color name="text_color_secondary">#99FFFFFF</color>
<!-- In Night mode, keeping note backgrounds desaturated/dark is better,
but for now we keep them linked to original resources unless we want to override them.
We override bg_white to be dark grey for general UI usage (if any). -->
<color name="bg_white">#2C2C2C</color>
<!-- Overriding primary_text_dark to ensure legacy styles also switch if they are not updated -->
<color name="primary_text_dark">#DEFFFFFF</color>
<color name="secondary_text_dark">#99FFFFFF</color>
</resources>

@ -133,6 +133,13 @@
<string name="menu_trash">回收站</string>
<string name="create_folder_success">创建文件夹成功</string>
<string name="menu_undo">撤回</string>
<string name="menu_redo">重做</string>
<string name="menu_clear_history">清空撤回历史</string>
<string name="undo_success">撤回成功</string>
<string name="redo_success">重做成功</string>
<string name="undo_fail">无可撤回</string>
<string name="redo_fail">无可重做</string>
<!-- New translations for missing strings -->
<string name="menu_rename">重命名</string>
<string name="dialog_rename_folder_title">重命名文件夹</string>

@ -28,4 +28,16 @@
<item>Messaging</item>
<item>Email</item>
</string-array>
<string-array name="theme_entries">
<item>Follow System</item>
<item>Light</item>
<item>Dark</item>
</string-array>
<string-array name="theme_values">
<item>system</item>
<item>light</item>
<item>dark</item>
</string-array>
</resources>

@ -17,7 +17,29 @@
<resources>
<color name="user_query_highlight">#335b5b5b</color>
<color name="primary_color">#1976D2</color>
<color name="white">#FFFFFF</color>
<color name="black">#000000</color>
<color name="primary_color">#263238</color>
<color name="on_primary_color">#FFFFFF</color>
<color name="background_color">#FAFAFA</color>
<color name="background_color">#E8E8E8</color>
<color name="primary_text_dark">#000000</color>
<color name="secondary_text_dark">#808080</color>
<color name="fab_color">#FFC107</color>
<!-- Semantic Text Colors -->
<color name="text_color_primary">#000000</color>
<color name="text_color_secondary">#808080</color>
<!-- Note Background Colors -->
<color name="bg_yellow">#FFF9C4</color>
<color name="bg_blue">#B3E5FC</color>
<color name="bg_white">#FFFFFF</color>
<color name="bg_green">#C8E6C9</color>
<color name="bg_red">#FFCDD2</color>
<!-- New Presets -->
<color name="bg_midnight_black">#212121</color>
<color name="bg_eye_care_green">#C7EDCC</color>
<color name="bg_warm">#FFE0B2</color>
<color name="bg_cool">#E1BEE7</color>
</resources>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string-array name="font_family_entries">
<item>System Default</item>
<item>Serif</item>
<item>Sans Serif</item>
<item>Monospace</item>
<item>Cursive</item>
</string-array>
<string-array name="font_family_values">
<item>default</item>
<item>serif</item>
<item>sans-serif</item>
<item>monospace</item>
<item>cursive</item>
</string-array>
</resources>

@ -64,6 +64,7 @@
<string name="menu_folder_delete">Delete folder</string>
<string name="menu_folder_change_name">Change folder name</string>
<string name="folder_exist">The folder %1$s exist, please rename</string>
<string name="menu_tasks">Tasks</string>
<!-- Folder operation dialogs -->
<string name="dialog_rename_folder_title">Rename folder</string>
@ -148,6 +149,7 @@
<!-- Sidebar strings -->
<string name="menu_login">Login</string>
<string name="menu_export">Export</string>
<string name="menu_templates">Templates</string>
<string name="menu_settings">Settings</string>
<string name="menu_trash">Trash</string>
<string name="root_folder_name">My Notes</string>
@ -169,6 +171,23 @@
<string name="delete_confirmation">Are you sure you want to delete selected notes?</string>
<string name="menu_unpin">Unpin</string>
<string name="menu_unlock">Unlock</string>
<!-- Search related -->
<string name="search_no_results">No results found</string>
<string name="search_history_title">Search History</string>
<string name="search_history_clear">Clear</string>
<!-- Undo/Redo -->
<string name="menu_undo">Undo</string>
<string name="menu_redo">Redo</string>
<string name="menu_clear_history">Clear Undo History</string>
<string name="undo_success">Undo successful</string>
<string name="redo_success">Redo successful</string>
<string name="undo_fail">Nothing to undo</string>
<string name="redo_fail">Nothing to redo</string>
<string name="menu_save_as_template">Save as template</string>
<string name="menu_picture">Picture</string>
<string name="menu_rich_text">Rich Text</string>
<string name="menu_restore">Restore</string>
<string name="menu_permanent_delete">Delete Forever</string>
</resources>
</resources>

@ -35,27 +35,27 @@
<style name="TextAppearancePrimaryItem">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
<item name="android:textColor">@color/text_color_primary</item>
</style>
<style name="TextAppearanceSecondaryItem">
<item name="android:textSize">@dimen/text_font_size_small</item>
<item name="android:textColor">@color/secondary_text_dark</item>
<item name="android:textColor">@color/text_color_secondary</item>
</style>
<style name="TextAppearanceUnderMenuIcon">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@android:color/black</item>
<item name="android:textColor">@color/text_color_primary</item>
</style>
<style name="HighlightTextAppearancePrimary">
<item name="android:textSize">@dimen/text_font_size_normal</item>
<item name="android:textColor">@color/primary_text_dark</item>
<item name="android:textColor">@color/text_color_primary</item>
</style>
<style name="HighlightTextAppearanceSecondary">
<item name="android:textSize">@dimen/text_font_size_small</item>
<item name="android:textColor">@color/secondary_text_dark</item>
<item name="android:textColor">@color/text_color_secondary</item>
</style>
<style name="NoteTheme" parent="@android:style/Theme.Holo.Light">

@ -4,18 +4,26 @@
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/on_primary_color</item>
<item name="android:statusBarColor">@color/primary_color</item>
<!-- 强制背景为白色,防止深色模式下黑屏 -->
<item name="android:colorBackground">@color/background_color</item>
<item name="android:windowBackground">@color/background_color</item>
<!-- 透明导航栏支持edge-to-edge显示 -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<!-- 根据内容自动调整状态栏图标颜色(深色背景=浅色图标) -->
<item name="android:windowLightStatusBar">false</item>
<!-- Preference Theme for SettingsFragment -->
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
</style>
<style name="Theme.Notesmaster" parent="Base.Theme.Notesmaster" />
<!-- NoteEditActivity使用的主题统一使用Material3风格 -->
<style name="Theme.Notesmaster.Edit" parent="Theme.Material3.Light.NoActionBar">
<style name="Theme.Notesmaster.Edit" parent="Theme.Material3.DayNight.NoActionBar">
<item name="colorPrimary">@color/primary_color</item>
<item name="colorOnPrimary">@color/on_primary_color</item>
<item name="android:statusBarColor">@color/primary_color</item>
<!-- 确保背景色适配深色模式,虽然会被便签背景覆盖 -->
<item name="android:colorBackground">@color/background_color</item>
<item name="android:windowBackground">@color/background_color</item>
</style>
</resources>

@ -21,6 +21,24 @@
android:key="pref_sync_account_key">
</PreferenceCategory>
<PreferenceCategory android:title="Appearance">
<ListPreference
android:key="pref_theme_mode"
android:title="Theme"
android:summary="%s"
android:entries="@array/theme_entries"
android:entryValues="@array/theme_values"
android:defaultValue="system" />
<ListPreference
android:key="pref_font_family"
android:title="Font Family"
android:summary="%s"
android:entries="@array/font_family_entries"
android:entryValues="@array/font_family_values"
android:defaultValue="default" />
</PreferenceCategory>
<PreferenceCategory>
<CheckBoxPreference
android:key="pref_key_bg_random_appear"

@ -0,0 +1,757 @@
# Xiaomi Notes 功能扩展规划(精简版)
## 概述
本文档规划了小米笔记应用的潜在功能扩展,按优先级和时间线组织。核心功能优先,高级功能作为后续迭代。
## 项目当前状态2026-01-21
### 已实现的核心功能 ✅
**基础功能**
- ✅ 笔记创建和编辑
- ✅ 笔记列表显示
- ✅ 文件夹管理(树形结构、展开收起、面包屑导航)
- ✅ 笔记提醒(闹钟功能)
- ✅ 笔记背景颜色5种颜色
- ✅ 笔记字体样式4种大小
- ✅ 本地数据存储SQLite + ContentProvider
**高级功能**
- ✅ MVVM架构重构ViewModel + Repository Pattern
- ✅ Google Tasks云同步
- ✅ 密码保护(图案锁 + 密码验证)
- ✅ 笔记锁定功能
- ✅ 笔记置顶功能
- ✅ 数据备份和恢复
- ✅ 桌面小部件2x2, 4x4
- ✅ 搜索功能ContentProvider支持
- ✅ 回收站功能
- ✅ 多语言支持(简体中文、繁体中文、英文)
- ✅ 材料设计UIMaterial Design
**技术架构**
- ✅ MVVM架构模式
- ✅ Repository数据访问层
- ✅ LiveData响应式数据更新
- ✅ ContentProvider标准API
- ✅ SQLiteOpenHelper数据库管理
- ✅ ExecutorService异步操作
- ✅ 48个Java源文件
- ✅ 135个资源文件
- ✅ 数据库版本5含10个触发器
### 项目统计
| 类别 | 数量 | 说明 |
|------|-------|------|
| Java源文件 | 48个 | 包括data、ui、viewmodel、model、tool、widget、gtask |
| 资源文件 | 135个 | layout、values、drawable、menu、xml、raw |
| Android组件 | 14个 | 8个Activity、3个Receiver、1个Service、2个Widget |
| 测试文件 | 4个 | 1个单元测试、2个数据层测试、1个集成测试 |
| 数据库表 | 2个 | note表(21字段)、data表(11字段) |
| 系统文件夹 | 4个 | 根(0)、临时(-1)、通话记录(-2)、回收站(-3) |
## 功能分类
### 核心功能 (Phase 1 - 已完成)
- ✅ 笔记创建和编辑
- ✅ 笔记列表显示
- ✅ 文件夹管理(树形结构、面包屑导航)
- ✅ 笔记提醒(闹钟)
- ✅ 笔记背景颜色(黄/红/蓝/绿/白)
- ✅ 笔记字体样式(小/中/大/超大)
- ✅ 本地数据存储SQLite + ContentProvider
- ✅ 笔记锁定功能
- ✅ 笔记置顶功能
- ✅ 回收站功能
- ✅ Google Tasks同步
## 短期扩展 (Phase 2 - 1-2个月)
### P0 - 必须实现
#### 2.1 搜索功能增强 ⚠️ 部分实现
**描述**: 提供强大的搜索功能,支持全文搜索、筛选和排序
**当前状态**:
- ✅ 基础搜索功能ContentProvider支持search URI
- ✅ 搜索建议功能
- ✅ 搜索历史记录
- ✅ 高级筛选选项
- ✅ 搜索结果高亮
**待实现功能点**:
- ✅ 搜索历史记录(本地存储常用搜索词)
- ✅ 搜索结果高亮显示
- ✅ 搜索频率排序
**技术方案**:
- 使用 SharedPreferences 存储搜索历史
- 扩展 NotesRepository 搜索逻辑
- 实现搜索 UI 筛选面板
**优先级**: 高
**工作量**: 2-3天
#### 2.2 导入导出功能增强 ✅ 已实现基础版本
**描述**: 支持笔记的导入导出,便于数据迁移和分享
**用户需求**: 希望可以实现与没有应用的人分享也能有应用类似的便签显示效果
**当前状态**:
- ✅ 数据备份功能BackupUtils.java
- ✅ 数据恢复功能
- ❌ 导出为便签图片格式(便于分享给非用户)
- ❌ 导出为 Markdown/TXT/JSON
**待实现功能点**:
- [ ] 导出为便签图片格式(类似应用内笔记卡片样式)
- [ ] 支持自定义背景颜色
- [ ] 支持字体大小选择
- [ ] 支持水印(可选)
- [ ] 高分辨率导出(分享清晰)
- [ ] 导出为 Markdown 格式
- [ ] 导出为 TXT 格式
- [ ] 导出为 JSON 格式
- [ ] 批量导出选择界面
- [ ] 导入 JSON 格式
- [ ] 导入验证和冲突处理
**技术方案**:
- 使用现有的 BackupUtils 基础
- 使用 Canvas 绘制便签卡片并保存为图片
- 集成 Markdown 处理库
- 实现文件选择器Storage Access Framework
- 添加分享功能Intent.ACTION_SEND
**优先级**: 高
**工作量**: 3-4天
#### 2.3 撤回功能
**描述**: 支持简单的笔记撤回操作
**功能点**:
- ✅ 撤回上一次编辑
- ✅ 撤回历史栈可连续撤回10-20次
- ✅ 重做功能
- ✅ 撤回/重做状态提示
- ✅ 清空撤回历史
**技术方案**:
- 实现 UndoStack 数据结构
- 在 NoteEditText 中记录编辑历史
- 使用 Command Pattern 实现撤回逻辑
- 添加撤回/重做 UI 按钮
**优先级**: 中
**工作量**: 2-3天
### P1 - 应该实现
#### 2.4 标签系统
**描述**: 为笔记添加标签,便于分类和筛选
**用户疑问**: 是否与文件夹功能重叠?
**功能定位**:
- **文件夹**: 层级分类(父文件夹 → 子文件夹),一个笔记只能属于一个文件夹
- **标签**: 扁平分类,一个笔记可以有多个标签,支持交叉分类
**场景对比**:
| 功能 | 文件夹 | 标签 |
|------|-------|------|
| 结构 | 树形层级 | 扁平列表 |
| 归属关系 | 一个笔记 → 一个文件夹 | 一个笔记 ↔ 多个标签 |
| 查询 | 按文件夹筛选 | 按标签筛选、多标签组合 |
| 适用场景 | 主要分类、项目分组 | 辅助分类、主题标记、状态标记 |
| 示例 | "工作/项目A/会议记录" | "重要"、"待办"、"学习"、"会议" |
**功能点**:
- [ ] 创建标签
- [ ] 编辑标签(名称、颜色)
- [ ] 删除标签
- [ ] 为笔记添加/移除标签(多选支持)
- [ ] 按标签筛选笔记
- [ ] 按多标签组合筛选AND/OR 逻辑)
- [ ] 标签颜色自定义
- [ ] 标签统计(每个标签的笔记数)
- [ ] 标签云视图
**技术方案**:
- 设计 Tag 数据表id, name, color, created_date
- 设计 NoteTag 关联表note_id, tag_id
- 实现标签选择器 UI类似文件夹树
- 扩展 NotesRepository 标签查询逻辑
- 添加标签管理 ViewModel
**优先级**: 高
**工作量**: 3-4天
#### 2.5 笔记锁定 ✅ 已实现
**描述**: 为敏感笔记添加密码保护
**用户需求**: 只需要密码保护就行了
**当前状态**:
- ✅ 图案锁保护LockPatternView.java
- ✅ 密码保护PasswordActivity.java
- ✅ SecurityManager.java 加密工具
**优先级**: 已完成
**工作量**: 0天
#### 2.6 笔记模板
**描述**: 提供常用笔记模板,快速创建标准化笔记
**功能点**:
- [ ] 预置模板(会议记录、日记、待办事项、读书笔记等)
- [ ] 创建自定义模板
- [ ] 编辑模板
- [ ] 删除模板
- [ ] 应用模板到新笔记
- [ ] 模板分类(工作、生活、学习)
- [ ] 模板预览
**技术方案**:
- 设计 Template 数据表id, name, content, category, created_date
- 实现模板管理 UI
- 创建模板应用逻辑(复制模板内容到新笔记)
- 添加模板管理 Activity/Fragment
**优先级**: 中
**工作量**: 2-3天
### P2 - 可以实现
#### 2.7 笔记统计和分析
**描述**: 提供笔记使用情况的统计信息
**功能点**:
- [ ] 笔记数量统计(总数、本周新增、本月新增)
- [ ] 笔记创建频率图表
- [ ] 常用词分析
- [ ] 文件夹分布(饼图)
- [ ] 标签使用统计
- [ ] 笔记长度统计
- [ ] 活跃时间段分析
- [ ] 周/月/年报告
**技术方案**:
- 扩展 NotesRepository 统计查询
- 集成图表库MPAndroidChart 或 AnyChart
- 实现统计数据计算(基于现有表)
- 创建统计图表 UI Activity
**优先级**: 低
**工作量**: 3-4天
#### 2.8 快捷操作优化 ⚠️ 部分实现
**描述**: 添加快捷操作,提高效率
**当前状态**:
- ✅ 长按笔记快捷菜单(复制、分享、删除、移动、设置提醒)
- ✅ 桌面小组件2x2, 4x4
- ❌ 通知栏快捷操作
- ❌ 文本格式快捷工具栏
**待实现功能点**:
- [ ] 通知栏快捷操作(创建笔记、语音输入)
- [ ] 快捷方式Launcher Shortcuts API
- [ ] 快捷手势(左滑删除、右滑置顶)
**优先级**: 低
**工作量**: 2天
## 中期扩展 (Phase 3 - 3-4个月)
### P1 - 应该实现
#### 3.1 富文本编辑
**描述**: 增强文本编辑功能,支持多种格式
**功能点**:
- ✅ 粗体、斜体、下划线
- ✅ 删除线
- ✅ 标题层级 (H1-H6)
- ✅ 列表(无序、有序、检查列表)
- ✅ 引用块
- ✅ 代码块
- ✅ 链接
- ✅ 分割线
- ✅ 文本颜色
- ✅ 文本背景色
**技术方案**:
- 集成富文本编辑库(如 RichEditor、SpannableStringBuilder
- 或使用 Markdown 渲染器(如 Markwon
- 实现格式工具栏
- 扩展 NoteEditText.java 支持富文本
**优先级**: 高
**工作量**: 4-5天
#### 3.2 图片附件
**描述**: 支持在笔记中插入图片
**功能点**:
- [ ] 从相册选择图片
- [ ] 拍照插入
- [ ] 图片裁剪
- [ ] 图片压缩
- [ ] 图片预览(全屏查看)
- [ ] 图片删除
- [ ] 图片大小调整
- [ ] 图片旋转
- [ ] 图片备注
**技术方案**:
- 使用 ContentResolver 访问图片
- 集成图片裁剪库UCrop 或 Android Image Cropper
- 集成图片加载库Glide 或 Coil
- 扩展 data 表支持图片存储mime_type = "image/*"
- 创建图片查看器 Activity
**优先级**: 高
**工作量**: 4-5天
### P2 - 可以实现
#### 3.3 智能识别功能
**描述**: 智能识别笔记中的地址和时间,支持快速跳转
**用户需求**: 希望可以添加智能识别地址、时间功能,跳转导航和闹钟设置
**功能点**:
- [ ] 智能识别地址信息
- [ ] 自动检测地址模式(省市区、街道地址)
- [ ] 高亮显示识别的地址
- [ ] 点击地址跳转地图应用高德地图、百度地图、Google Maps
- [ ] 支持地址标注(在地址旁显示位置图标)
- [ ] 智能识别时间信息
- [ ] 自动检测日期时间模式今天14:30、明天上午9点、2025年1月21日
- [ ] 高亮显示识别的时间
- [ ] 点击时间快速设置闹钟/提醒
- [ ] 支持时间相对表达(下周三、后天)
- [ ] 智能识别电话号码
- [ ] 自动检测电话号码
- [ ] 点击电话号码直接拨号
- [ ] 智能识别URL链接
- [ ] 自动检测URL并使其可点击
- [ ] 长按URL显示预览或复制选项
**技术方案**:
- 使用正则表达式识别地址、时间、电话、URL模式
- 实现智能解析器SmartParser.java
- 扩展 NoteEditText 支持高亮显示
- 集成 Intent 跳转地图、电话、闹钟
- 使用 SpannableString 设置可点击区域
**优先级**: 中
**工作量**: 5-6天
#### 3.4 链接笔记
**描述**: 支持笔记之间的链接引用
**功能点**:
- [ ] 创建笔记链接([[笔记标题]]语法)
- [ ] 自动检测笔记标题链接
- [ ] 反向链接查看(哪些笔记引用了当前笔记)
- [ ] 链接预览(悬浮显示笔记摘要)
- [ ] 链接计数
- [ ] 图谱视图(可视化笔记关系)
**技术方案**:
- 设计 NoteLink 数据表id, source_note_id, target_note_id, position
- 实现链接语法解析(正则表达式)
- 创建链接管理 UI
- 集成图表库绘制笔记关系图谱
**优先级**: 中
**工作量**: 4-5天
#### 3.5 任务清单
**描述**: 在笔记中创建待办事项清单
**功能点**:
- ✅ 添加任务项(- [ ] 语法)
- ✅ 标记完成/未完成
- ✅ 任务优先级(高/中/低)
- ✅ 任务截止日期
- ✅ 任务提醒
- ❌ 任务统计(完成率)
- ✅ 过滤已完成任务
- ❌ 任务拖拽排序
**技术方案**:
- 扩展 data 表支持任务类型mime_type = "text/x-todo"
- 实现任务 UI 组件TodoItemView
- 扩展 NoteEditText 解析任务语法
- 集成现有提醒功能
**优先级**: 中
**工作量**: 3-4天
## 长期扩展 (Phase 4 - 6个月+)
### P2 - 可以实现
#### 4.1 主题和自定义 ⚠️ 部分实现
**描述**: 提供更多主题和界面自定义选项
**用户需求**: 自定义壁纸
**当前状态**:
- ✅ 笔记背景颜色5种
- ✅ 字体大小4种
- ✅ 夜间主题values-night
- ❌ 完整主题系统
- ❌ 自定义主题颜色
- ❌ 自定义壁纸
**待实现功能点**:
- [ ] 多种预设主题
- [ ] 简约白
- [ ] 深夜黑
- [ ] 护眼绿
- [ ] 暖色系
- [ ] 冷色系
- [ ] 自定义主题颜色
- [ ] 自定义应用壁纸
- [ ] 从相册选择壁纸
- [ ] 预设壁纸库
- [ ] 瓷片平铺/居中/拉伸
- [ ] 磨砂玻璃效果(可选)
- [ ] 自定义字体家族(使用 Google Fonts
- [ ] 自定义卡片样式
- [ ] 自定义图标包
- [ ] 动态取色(基于壁纸)
**技术方案**:
- 使用 Material You (Material 3) 动态颜色
- 实现主题管理 ViewModel
- 创建主题编辑器 Activity
- 集成 Google Fonts API
- 实现壁纸设置和管理
**优先级**: 低
**工作量**: 5-6天
#### 4.2 智能功能
**描述**: 利用 AI 提供智能辅助功能
**功能点**:
- [ ] 自动摘要AI生成笔记摘要
- [ ] 智能分类建议(基于内容推荐文件夹/标签)
- [ ] 关键词提取
- [ ] 相关笔记推荐(基于内容相似度)
- [ ] 语法检查
- [ ] 翻译功能
- [ ] 智能纠错
**技术方案**:
- 集成本地 NLP 模型TensorFlow Lite
- 或使用第三方 AI APIOpenAI、百度文心等
- 实现智能分析逻辑
- 创建智能功能 UI 组件
**优先级**: 低
**工作量**: 7-10天
#### 4.3 云同步和账号系统 ✅ 已实现基础版本
**描述**: 完善的数据备份和云同步机制
**用户需求**: 实现注册、登录和云同步
**当前状态**:
- ✅ Google Tasks 同步(已有账号系统)
- ✅ 手动备份BackupUtils.java
- ✅ 手动恢复
- ❌ 自定义账号系统(注册/登录)
- ❌ 云备份到自建服务器
- ❌ 多设备同步
**待实现功能点**:
- [ ] 自定义账号系统
- [ ] 用户注册(手机号/邮箱)
- [ ] 用户登录(密码 + 验证码)
- [ ] 密码找回
- [ ] 账号注销
- [ ] 账号安全设置(修改密码、绑定手机号)
- [ ] 云同步到自建服务器
- [ ] 自动同步(实时或定时)
- [ ] 手动同步
- [ ] 同步冲突解决
- [ ] 同步状态显示
- [ ] 多设备管理
- [ ] 查看已登录设备
- [ ] 远程登出设备
- [ ] 设备命名
- [ ] 备份文件加密
- [ ] 端到端加密
- [ ] 数据传输加密HTTPS + 证书固定)
**技术方案**:
- 设计服务端Node.js + MongoDB/PostgreSQL
- 实现 REST API用户认证、数据同步
- 客户端集成 Retrofit + OkHttp
- 使用 JWT 或 OAuth 进行认证
- 实现增量同步算法
- 扩展现有 BackupUtils.java
**优先级**: 中
**工作量**: 10-15天含服务端开发
## 功能实现时间线(精简版)
### Month 1-2: 核心功能增强
- ✅ Week 1: 搜索功能增强 (2.1) - 搜索历史、高级筛选
- [ ] Week 2: 导入导出功能增强 (2.2) - 便签图片导出、Markdown/TXT
- ✅ Week 3: 撤回功能 (2.3) - 撤回/重做
- [ ] Week 4: 标签系统 (2.4) - 标签分类和筛选
### Month 3-4: 用户体验提升
- [ ] Week 5: 笔记模板 (2.6) - 模板管理
- ✅ Week 6-8: 富文本编辑 (3.1) - 完整格式支持
### Month 5-6: 功能扩展
- [ ] Week 9-10: 图片附件 (3.2) - 图片管理和预览
- [ ] Week 11-12: 智能识别功能 (3.3) - 地址、时间识别
### Month 7-8: 高级功能
- [ ] Week 13-14: 链接笔记 (3.4) - 笔图谱
- ✅ Week 15-16: 任务清单 (3.5) - 任务管理
### Month 9+: 智能化和生态
- [ ] Week 17-20: 云同步和账号系统 (4.3) - 注册登录、云同步
- [ ] Week 21-22: 主题和自定义 (4.1) - Material 3 动态主题、自定义壁纸
- [ ] Week 23+: 智能功能 (4.2)、跨平台同步 (4.4) 等
## 技术栈规划(基于当前架构)
### 当前技术栈
- **UI 框架**: Material Design Components
- **架构**: MVVM + Repository Pattern
- **数据库**: SQLite + SQLiteOpenHelper
- **异步**: ExecutorService
- **视图绑定**: findViewById传统方式
- **依赖管理**: Gradle Version Catalog (libs.versions.toml)
- **网络**: Apache HttpClient 4.5.14
### 建议技术演进
#### 短期演进保持Java
- **UI 框架**: 升级到 Material Design 3
- **架构**: 保持 MVVM + Repository Pattern
- **数据库**: 保持 SQLiteOpenHelper考虑迁移到 Room
- **异步**: 保持 ExecutorService考虑迁移到 Kotlin Coroutines
- **依赖注入**: 考虑引入 Hilt可选
#### 中期演进可选Kotlin迁移
- **语言**: 逐步迁移到 Kotlin100% 互操作)
- **UI**: 迁移到 Jetpack Compose可选
- **异步**: 迁移到 Kotlin Coroutines + Flow
- **依赖注入**: Hilt
- **数据库**: Room + Coroutines
#### 可能引入的技术栈
- **富文本编辑**:
- RichEditor (Java)
- Markwon (Markdown渲染Kotlin优先)
- **图片处理**:
- Glide / Coil (图片加载)
- UCrop / Android Image Cropper (图片裁剪)
- **图表**: MPAndroidChart / AnyChart / ECharts
- **文件压缩**: Zip4j
- **加密**: Android Keystore + AES (已有 SecurityManager)
- **智能识别**:
- 正则表达式(地址、时间、电话识别)
- 第三方 API地址解析、时间解析
- **服务端开发**:
- Node.js + Express/Koa
- MongoDB / PostgreSQL
- Redis缓存
- Nginx反向代理
### 技术债务清理
#### 高优先级
- [ ] 迁移从 findViewById 到 ViewBinding / Jetpack Compose
- [ ] 迁移从 ExecutorService 到 Kotlin Coroutines / Flow
- [ ] 迁移从 SQLiteOpenHelper 到 Room
- [ ] 重构 Apache HttpClient 依赖(使用 Retrofit + OkHttp
#### 中优先级
- [ ] 引入依赖注入框架Hilt
- [ ] 升级到 Material Design 3
- [ ] 迁移到 Kotlin逐步
- [ ] 统一异常处理机制
#### 低优先级
- [ ] Jetpack Compose 迁移
- [ ] 单向数据流UDF架构优化
- [ ] 模块化架构Dynamic Feature Modules
## 性能优化
### 短期优化
- [ ] RecyclerView 性能优化DiffUtil、预加载
- [ ] 图片加载和缓存优化(使用 Glide/Coil
- [ ] 数据库查询优化(添加索引、避免 N+1 查询)
- [ ] 内存泄漏检测和修复LeakCanary
- [ ] 启动速度优化Application 初始化优化)
### 长期优化
- [ ] 启用 ProGuard/R8 混淆和优化
- [ ] APK 体积优化(资源压缩、动态下发)
- [ ] 启动速度优化(延迟加载、线程优化)
- [ ] 电池使用优化WorkManager 替代 AlarmManager
- [ ] 网络请求优化(缓存策略、请求合并)
## 安全性考虑
### 数据安全
- [x] 数据加密存储(已有 SecurityManager
- [ ] 敏感信息保护(日志脱敏)
- [x] 权限最小化原则(运行时权限请求)
- [ ] 安全的文件存储Scoped Storage
- [ ] 密码保护(已有)
### 隐私保护
- [x] 本地数据处理优先
- [ ] 明确的隐私政策
- [ ] 用户数据控制权(数据导出、删除)
- [ ] 安全的数据传输HTTPS + 证书固定)
### 代码安全
- [ ] 代码混淆ProGuard/R8
- [ ] 防止重打包(签名校验)
- [ ] 日志脱敏(不记录敏感信息)
- [ ] SQL 注入防护(使用参数化查询,已实现)
## 测试策略
### 单元测试
- [x] 数据库操作测试(已有 FolderDatabaseTest
- [x] 仓库层测试(已有 NotesRepositoryTest
- [ ] 业务逻辑测试ViewModel 单元测试)
- [ ] 工具类测试Utils 单元测试)
### 集成测试
- [ ] 主要功能流程测试(笔记创建、编辑、删除)
- [ ] 数据迁移测试(数据库版本升级)
- [ ] ContentProvider 接口测试
- [ ] 同步功能测试
### UI 测试
- [ ] Espresso UI 测试(关键流程)
- [ ] 用户交互测试(手势、快捷操作)
- [ ] 兼容性测试(不同 Android 版本、屏幕尺寸)
### 性能测试
- [ ] 大数据量测试1000+ 笔记)
- [ ] 内存使用测试(内存泄漏、峰值)
- [ ] 启动时间测试(冷启动、热启动)
- [ ] 电池使用测试(长时间运行)
## 用户反馈和迭代
### 收集反馈
- [ ] 应用内反馈功能(反馈表单)
- [ ] 应用商店评论监控
- [ ] 用户调研(问卷调查)
- [ ] 数据分析(崩溃率、使用频率、功能使用率)
- [ ] Beta 测试计划Play Console
### 迭代流程
1. 收集用户反馈(多渠道)
2. 分析需求优先级(影响力 vs 成本)
3. 制定开发计划Sprint Planning
4. 开发和测试Code Review + CI/CD
5. 发布新版本(灰度发布)
6. 收集新反馈(持续监控)
### 数据驱动决策
- [ ] 功能使用率统计
- [ ] 用户留存率分析
- [ ] 崩溃率监控
- [ ] 性能指标追踪
- [ ] A/B 测试(新功能)
## 风险评估
### 技术风险
- [ ] 第三方库兼容性(版本冲突、废弃)
- [ ] Android 版本兼容性(碎片化)
- [ ] 性能瓶颈(大数据量、复杂查询)
- [ ] 数据迁移风险(版本升级失败)
- [ ] Kotlin 迁移风险(互操作问题)
- [ ] 服务端开发风险(并发、安全、扩展性)
### 产品风险
- [ ] 功能过度复杂(用户体验下降)
- [ ] 用户学习成本高(功能过多)
- [ ] 功能使用率低(开发浪费)
- [ ] 维护成本高(技术债务积累)
- [ ] 竞品功能差距(市场竞争力)
### 缓解措施
- [ ] 渐进式功能发布Feature Flags
- [ ] 充分的测试覆盖(单元测试、集成测试)
- [ ] 用户教育文档(帮助中心、教程)
- [ ] 灵活的架构设计(解耦、可扩展)
- [ ] 持续重构(技术债管理)
- [ ] 可配置功能(用户可选)
## 成功指标
### 技术指标
- [ ] 崩溃率 < 0.5%
- [ ] ANR 率 < 0.1%
- [ ] 启动时间 < 2
- [ ] 代码覆盖率 > 60%
- [ ] 技术债务指数降低
### 产品指标
- [ ] 日活跃用户DAU增长
- [ ] 用户留存率7日、30日
- [ ] 功能使用率(每个功能的用户比例)
- [ ] 应用商店评分 > 4.5
- [ ] 用户反馈响应时间 < 24h
## 总结
本规划基于当前项目实际状态MVVM架构、Repository Pattern、48个Java文件、135个资源文件并根据用户反馈精简了功能列表提供了从核心功能增强到高级功能扩展的完整路线图。
**项目优势**
1. ✅ 完整的 MVVM 架构ViewModel + Repository
2. ✅ 响应式数据更新LiveData
3. ✅ 标准化数据访问ContentProvider
4. ✅ 稳定的数据库设计SQLite + 10个触发器
5. ✅ 多语言支持(中英文)
6. ✅ Google Tasks 云同步
7. ✅ 密码保护和安全功能
8. ✅ 备份和恢复功能
**关键成功因素**
1. 优先实现用户最需要的功能(搜索、便签导出、智能识别)
2. 保持代码质量和可维护性(持续重构)
3. 持续收集用户反馈并迭代(数据驱动)
4. 平衡功能丰富度和简洁性(用户体验)
5. 注重性能和用户体验(响应速度、流畅度)
6. 稳健的架构设计(可扩展、可测试)
**下一步行动**
1. 实施 Phase 2 P0 功能(搜索增强、便签导出、撤回功能)
2. 技术债务清理ViewBinding、Kotlin 迁移)
3. 建立自动化测试体系(单元测试、集成测试)
4. 设置监控和分析(崩溃率、使用率)
5. 规划服务端开发(云同步、账号系统)
---
**文档版本**: v3.0(精简版)
**更新日期**: 2026-01-21
**维护者**: Sisyphus AI Agent
**更新说明**: 根据用户反馈精简功能列表,移除不必要的生物识别和复杂版本历史,强调便签图片导出、智能识别和云同步功能
Loading…
Cancel
Save