diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/.gitignore b/src/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/src/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/src/.idea/misc.xml b/src/.idea/misc.xml new file mode 100644 index 0000000..639900d --- /dev/null +++ b/src/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/.idea/modules.xml b/src/.idea/modules.xml new file mode 100644 index 0000000..f669a0e --- /dev/null +++ b/src/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/src/.idea/vcs.xml b/src/.idea/vcs.xml new file mode 100644 index 0000000..6c0b863 --- /dev/null +++ b/src/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Notesmaster/.gitignore b/src/Notesmaster/.gitignore index 7f909be..aa724b7 100644 --- a/src/Notesmaster/.gitignore +++ b/src/Notesmaster/.gitignore @@ -13,8 +13,3 @@ .externalNativeBuild .cxx local.properties -build.gradle.kts -gradle.properties -gradlew -gradlew.bat -settings.gradle.kts \ No newline at end of file diff --git a/src/Notesmaster/.idea/deploymentTargetSelector.xml b/src/Notesmaster/.idea/deploymentTargetSelector.xml index b268ef3..0be8744 100644 --- a/src/Notesmaster/.idea/deploymentTargetSelector.xml +++ b/src/Notesmaster/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/src/Notesmaster/PROJECT_ANALYSIS_AND_PLAN.md b/src/Notesmaster/PROJECT_ANALYSIS_AND_PLAN.md new file mode 100644 index 0000000..67a4290 --- /dev/null +++ b/src/Notesmaster/PROJECT_ANALYSIS_AND_PLAN.md @@ -0,0 +1,117 @@ +# 小米便签 (MiNotes) 项目深度分析与拓展计划书 + +## 1. 项目概览与架构分析 (Project Overview & Architecture) + +### 1.1 总体架构 +本项目采用了 **混合架构 (Hybrid Architecture)**,处于从传统的 Android 开发模式向现代 MVVM 模式过渡的阶段。 +* **Legacy 部分**: 核心编辑页面 `NoteEditActivity` 及其对应的逻辑类 `WorkingNote` 采用了一种类似于 MVP/MVC 的变体,其中 `WorkingNote` 充当了“上帝类”的角色,承担了数据加载、状态管理、UI 回调等过多职责,导致耦合度较高。 +* **Modern 部分**: 列表页面 `NotesListActivity` 引入了 `ViewModel` (`NotesListViewModel`) 和 `Repository` (`NotesRepository`),利用 `LiveData` 进行数据驱动的 UI 更新,体现了较好的分层设计思想。 + +### 1.2 数据流向 +* **核心数据源**: `NotesProvider` (继承自 `ContentProvider`) 是应用的数据中枢。它不仅为应用内部提供数据(通过 `ContentResolver`),还设计用于跨进程/跨应用的数据共享。 +* **数据访问**: + * **读操作**: 大部分通过 `NotesRepository` -> `ContentResolver` -> `NotesProvider` -> `SQLiteDatabase` 的链路。 + * **写操作**: 编辑页直接通过 `WorkingNote` 操作 `ContentResolver`,绕过了 Repository 层。 +* **同步机制**: `GTaskManager` 负责与 Google Tasks 的同步,它直接操作本地数据库并进行网络请求。 + +### 1.3 关键组件 +* **WorkingNote**: 负责单条笔记的生命周期管理(加载、保存、转换模式)。 +* **NotesDatabaseHelper**: 管理 SQLite 数据库创建、升级及复杂的触发器(Trigger)逻辑。 +* **GTaskSyncService**: 后台同步服务。 + +--- + +## 2. 现有功能分析 (Feature Analysis) + +### 2.1 核心功能 +* **笔记编辑**: 支持普通文本模式和清单模式(Checklist)。 +* **文件夹管理**: 支持多层级文件夹(虽然 UI 上主要体现为单层),包含系统预置文件夹(通话记录、回收站)。 +* **通话记录笔记**: 特色功能,能够根据来电号码自动关联或创建笔记。 + +### 2.2 辅助功能 +* **桌面小部件 (Widgets)**: 提供 2x2 和 4x4 两种规格,通过 `RemoteViews` 更新。 +* **定时提醒**: 基于 `AlarmManager` 实现。 +* **云同步**: 仅支持 Google Tasks 同步。 + +--- + +## 3. 可扩展性与兼容性评估 (Scalability & Compatibility) + +### 3.1 可扩展性 (Scalability) +* **优势**: + * **ContentProvider**: 数据结构标准,易于被外部模块访问。 + * **MVVM 雏形**: 新增模块若遵循 MVVM 模式,将具有良好的测试性和维护性。 +* **劣势 (瓶颈)**: + * **数据库耦合**: 大量硬编码的 SQL 语句和触发器逻辑,难以迁移到 Room 等现代 ORM 框架。 + * **WorkingNote 臃肿**: 修改编辑逻辑牵一发而动全身。 + * **同步接口缺失**: 缺乏通用的同步接口抽象,难以扩展其他云服务(如 WebDAV)。 + +### 3.2 兼容性 (Compatibility) +当前代码库存在显著的兼容性风险,尤其是在 Android 10+ 设备上: +* **AsyncTask**: `GTaskASyncTask` 使用了已废弃的 API,可能导致内存泄漏或崩溃。 +* **存储权限**: 未适配 Android 10+ 的分区存储 (Scoped Storage),直接文件路径访问将失效。 +* **精确闹钟**: Android 12+ 需要 `SCHEDULE_EXACT_ALARM` 权限。 +* **通知渠道**: Android 8.0+ 必须适配 Notification Channels。 + +--- + +## 4. 项目拓展计划 (Extension Plan) + +本计划旨在将小米便签打造为一款现代化、高效率、高颜值的笔记应用。 + +### Phase 1: 现代化重构与基石搭建 (Modernization) +**目标**: 消除技术债务,确保在 Android 14/15 上稳定运行。 + +1. **并发模型升级**: + * 废弃 `AsyncTask`。 + * 引入 **Kotlin Coroutines** (协程) 处理异步任务(数据库读写、网络请求)。 +2. **系统适配**: + * **权限**: 适配动态权限申请,特别是闹钟和通知权限。 + * **存储**: 使用 `MediaStore` API 或 SAF (Storage Access Framework) 替代直接文件操作。 + * **服务**: 将后台同步服务适配为 WorkManager 或 Foreground Service。 +3. **代码解耦**: + * 重构 `NoteEditActivity`,引入 `NoteEditViewModel`。 + * 拆分 `WorkingNote`,将其纯化为数据模型,逻辑移入 ViewModel/UseCase。 + +### Phase 2: 核心体验升级 (Core Experience) +**目标**: 提升笔记的编辑与组织能力。 + +1. **富文本支持 (Markdown)**: + * 数据库 `data` 表新增 `mime_type` 类型支持。 + * 集成 Markdown 解析器(如 Markwon),支持标题、列表、代码块渲染。 +2. **多媒体附件**: + * 新增 `attachments` 数据表。 + * 支持插入图片(压缩存储或引用 URI)、音频文件。 +3. **标签系统 (Tagging)**: + * 实现笔记与标签的多对多关系。 + * UI 上支持按标签筛选、颜色标记。 + +### Phase 3: 云服务与互联 (Cloud & Connectivity) +**目标**: 打破数据孤岛,提供数据安全保障。 + +1. **同步架构抽象**: + * 定义 `ISyncProvider` 接口。 + * 将 `GTaskManager` 降级为一种实现。 +2. **WebDAV 支持**: + * 实现通用的 WebDAV 协议,允许用户连接坚果云、Nextcloud、NAS 等私有云。 +3. **本地备份**: + * 支持导出为 JSON/Zip 格式。 + * 实现自动定时本地备份。 + +### Phase 4: UI/UX 焕新 (Design Refresh) +**目标**: 拥抱 Material Design 3 (Material You)。 + +1. **动态取色**: 支持 Android 12+ 的动态主题色。 +2. **深色模式**: 完善的 Dark Mode 资源适配。 +3. **现代交互**: + * 列表页支持侧滑删除/归档。 + * 笔记拖拽排序。 + * 沉浸式状态栏与导航栏。 + +## 5. 关键决策与建议 +* **开发语言**: 建议新功能完全使用 **Kotlin** 开发,旧 Java 代码按需迁移。 +* **数据库**: 虽然迁移到 Room 工作量大,但建议在 Phase 2 开始尝试引入 Room,与现有 Helper 共存,逐步替换。 +* **同步策略**: 鉴于网络环境,建议将 **WebDAV** 作为首推的同步方式,Google Tasks 作为可选插件。 + +--- +*Generated by Trae AI Assistant* diff --git a/src/Notesmaster/PROJECT_EXPANSION_PLAN.md b/src/Notesmaster/PROJECT_EXPANSION_PLAN.md new file mode 100644 index 0000000..9944480 --- /dev/null +++ b/src/Notesmaster/PROJECT_EXPANSION_PLAN.md @@ -0,0 +1,122 @@ +# 小米便签 (MiNotes) 功能拓展详细规划书 (v3.0 - 全面增强版) + +本规划书在 v2.0 离线优先的基础上,进一步汲取了市场主流笔记应用(Notion, Obsidian, Flomo 等)的精华,旨在打造一款**全能型、离线优先、极具差异化**的笔记应用。我们从效率、知识管理、创意、安全四个维度进行了深度发散和规划。 + +## 1. 效率与工作流 (Productivity & Workflow) +*定位:让笔记不仅是记录,更是行动的开始。* + +### 1.1 专注模式 (Focus Mode / Pomodoro) +* **功能描述**: 在编辑笔记时,提供一个沉浸式的“专注模式”。开启后,隐藏所有无关 UI(状态栏、工具栏),并可叠加一个倒计时番茄钟。 +* **用户价值**: 帮助用户进入心流状态,适合深度写作、思考或复习。 +* **离线适配**: 纯本地逻辑。 +* **开发难度**: 低。 + +### 1.2 悬浮窗速记 (Floating Quick Note) +* **功能描述**: 在其他应用之上显示一个半透明悬浮球。点击后展开一个小窗口,支持快速输入文字、语音或粘贴剪贴板内容,无需切换应用。 +* **用户价值**: 随时随地捕捉灵感,阅读/看视频时的绝佳伴侣。 +* **离线适配**: 本地 WindowManager 实现。 +* **开发难度**: 中(需处理悬浮窗权限适配)。 + +### 1.3 每日回顾 (Daily Review / Journaling) +* **功能描述**: + * **自动日志**: 每天自动创建一个以“日期”命名的笔记,聚合当天的所有操作(新建笔记、完成待办、通话记录)。 + * **那年今日**: 首页顶部展示往年今天的笔记回顾。 +* **用户价值**: 自动化的生活记录与复盘,无需手动整理。 +* **开发难度**: 中。 + +### 1.4 模板中心 (Template Center) +* **功能描述**: 提供预设模板库(如康奈尔笔记法、SWOT 分析、会议纪要、周报),并允许用户将现有笔记保存为自定义模板。 +* **用户价值**: 快速开始标准化记录,降低启动门槛。 +* **开发难度**: 低。 + +--- + +## 2. 知识管理与可视化 (Knowledge & Visualization) +*定位:将碎片化信息重组为结构化知识。* + +### 2.1 看板视图 (Kanban View) +* **功能描述**: 将文件夹内的笔记或待办事项以卡片形式展示在“未开始”、“进行中”、“已完成”等列中,支持拖拽流转状态。 +* **用户价值**: 轻量级的项目管理工具,直观掌控任务进度。 +* **开发难度**: 高。 + +### 2.2 日历视图 (Calendar View) +* **功能描述**: 在日历上以圆点或标题形式展示笔记(按创建/修改时间或提醒时间分布)。支持点击日期直接创建笔记。 +* **用户价值**: 从时间维度回顾历史,规划未来。 +* **开发难度**: 中。 + +### 2.3 知识图谱 (Knowledge Graph) +* **功能描述**: 基于双向链接(`[[Link]]`),用节点和连线可视化展示笔记之间的引用关系。支持缩放、拖拽节点,点击节点跳转。 +* **用户价值**: 上帝视角俯瞰知识结构,发现知识盲区与隐性联系。 +* **开发难度**: 极高(需引入图形渲染库如 MPAndroidChart 修改版或自定义 View)。 + +### 2.4 标签墙与嵌套标签 (Tag Hierarchy) +* **功能描述**: 支持多级标签系统(如 `#工作/项目A/会议`),并提供一个动态的标签云或标签墙视图进行筛选。 +* **用户价值**: 突破文件夹的单一维度限制,实现多维度灵活管理。 +* **开发难度**: 中。 + +--- + +## 3. 创意与多媒体 (Creativity & Media) +*定位:释放表达欲,记录不设限。* + +### 3.1 手写与绘图 (Handwriting & Sketch) +* **功能描述**: 提供画笔工具,支持在笔记中插入手写区域或涂鸦。支持压感(如果设备支持),支持橡皮擦、套索工具。 +* **用户价值**: 还原纸笔体验,适合绘制草图、数学公式、手写签名。 +* **开发难度**: 高。 + +### 3.2 语音笔记与时间戳 (Audio Note with Timestamp) +* **功能描述**: 录音的同时可以打字记录。回放录音时,点击文字可跳转到对应的录音进度(类似 Notability 的功能)。 +* **用户价值**: 会议记录、课堂录音的神器,不再错过任何细节。 +* **开发难度**: 高。 + +### 3.3 闪念胶囊 (Flash Card / Flomo-like) +* **功能描述**: 一个类似于聊天界面的输入框,发送即保存为一张卡片。支持随机漫游回顾,支持标签分类。 +* **用户价值**: 极速捕捉转瞬即逝的灵感,无压力记录。 +* **开发难度**: 低。 + +--- + +## 4. 隐私与安全 (Privacy & Security) +*定位:数据主权,绝对安全。* + +### 4.1 生物识别锁 (Biometric Lock) +* **功能描述**: 支持指纹或面部解锁进入应用,或锁定特定私密文件夹/笔记。 +* **用户价值**: 保护日记、账号密码等敏感信息,防止他人窥探。 +* **开发难度**: 低(使用 Android BiometricPrompt API)。 + +### 4.2 本地加密存储 (Local Encryption) +* **功能描述**: 使用 AES-256 算法对数据库文件和附件进行加密存储。即使手机丢失,导出文件也无法被暴力破解。 +* **用户价值**: 满足极高安全需求场景(如商业机密、个人隐私)。 +* **开发难度**: 中(需引入 SQLCipher)。 + +### 4.3 隐身模式 (Stealth Mode) +* **功能描述**: 在多任务界面模糊显示应用内容;提供一个“伪装密码”,输入后进入一个空的或预设的伪装空间。 +* **用户价值**: 极致的隐私保护。 +* **开发难度**: 中。 + +--- + +## 5. 推荐实施路线 (Roadmap v3.0) - 离线优先策略 + +### Phase 1: 基础体验增强 (Foundation) +* **目标**: 夯实基础,提升易用性和安全性。 +* **任务**: + 1. **回收站** (现有计划) + 2. **深色模式** (现有计划) + 3. **生物识别锁**: 增加安全感,实现简单,价值高。 + 4. **模板功能**: 提升记录效率。 + +### Phase 2: 核心生产力 (Core Productivity) +* **目标**: 丰富记录形式,提升编辑效率。 +* **任务**: + 1. **本地备份与导出** (现有计划) + 2. **Markdown 支持** (现有计划) + 3. **悬浮窗速记**: 扩展记录场景。 + 4. **日历视图**: 提供新的时间维度视角。 + +### Phase 3: 高级与创新 (Innovation) +* **目标**: 打造差异化壁垒。 +* **任务**: + 1. **离线 OCR** (现有计划) + 2. **看板视图**: 引入项目管理能力。 + 3. **双向链接 & 知识图谱**: 打造知识库神器的核心。 diff --git a/src/Notesmaster/app/.project b/src/Notesmaster/app/.project index 4b50465..33c0cbe 100644 --- a/src/Notesmaster/app/.project +++ b/src/Notesmaster/app/.project @@ -5,6 +5,11 @@ + + org.eclipse.jdt.core.javabuilder + + + org.eclipse.buildship.core.gradleprojectbuilder @@ -12,6 +17,7 @@ + org.eclipse.jdt.core.javanature org.eclipse.buildship.core.gradleprojectnature diff --git a/src/Notesmaster/app/build.gradle.kts b/src/Notesmaster/app/build.gradle.kts index f0dba31..362d5b4 100644 --- a/src/Notesmaster/app/build.gradle.kts +++ b/src/Notesmaster/app/build.gradle.kts @@ -53,6 +53,8 @@ dependencies { // RecyclerView依赖 implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.cursoradapter:cursoradapter:1.0.0") + implementation("androidx.preference:preference:1.2.1") + implementation("androidx.palette:palette-ktx:1.0.0") testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/capsule/CapsuleService.java b/src/Notesmaster/app/src/main/java/net/micode/notes/capsule/CapsuleService.java new file mode 100644 index 0000000..d75bb44 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/capsule/CapsuleService.java @@ -0,0 +1,339 @@ +package net.micode.notes.capsule; + +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.graphics.PixelFormat; +import android.os.Build; +import android.os.IBinder; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import android.os.Handler; +import android.util.Log; +import android.view.DragEvent; +import android.content.ClipData; +import android.content.ClipDescription; + +import net.micode.notes.R; +import net.micode.notes.model.Note; +import net.micode.notes.data.Notes; + +public class CapsuleService extends Service { + + private static final String TAG = "CapsuleService"; + private WindowManager mWindowManager; + private View mCollapsedView; + private View mExpandedView; + private WindowManager.LayoutParams mCollapsedParams; + private WindowManager.LayoutParams mExpandedParams; + + private Handler mHandler = new Handler(); + public static String currentSourcePackage = ""; + + private static final String CHANNEL_ID = "CapsuleServiceChannel"; + + public static final String ACTION_SAVE_SUCCESS = "net.micode.notes.capsule.ACTION_SAVE_SUCCESS"; + + private final android.content.BroadcastReceiver mSaveReceiver = new android.content.BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (ACTION_SAVE_SUCCESS.equals(intent.getAction())) { + highlightCapsule(); + } + } + }; + + public static void setCurrentSourcePackage(String pkg) { + currentSourcePackage = pkg; + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + @Override + public void onCreate() { + super.onCreate(); + mWindowManager = (WindowManager) getSystemService(WINDOW_SERVICE); + createNotificationChannel(); + startForeground(1, createNotification()); + + initViews(); + } + + private void initViews() { + // Collapsed View + mCollapsedView = LayoutInflater.from(this).inflate(R.layout.layout_capsule_collapsed, null); + + int layoutFlag; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + layoutFlag = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; + } else { + layoutFlag = WindowManager.LayoutParams.TYPE_PHONE; + } + + mCollapsedParams = new WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + layoutFlag, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT); + + mCollapsedParams.gravity = Gravity.TOP | Gravity.START; + mCollapsedParams.x = 0; + mCollapsedParams.y = 100; + + // Expanded View + mExpandedView = LayoutInflater.from(this).inflate(R.layout.layout_capsule_expanded, null); + + mExpandedParams = new WindowManager.LayoutParams( + dp2px(300), + dp2px(400), + layoutFlag, + WindowManager.LayoutParams.FLAG_DIM_BEHIND, // Allow focus for EditText + PixelFormat.TRANSLUCENT); + mExpandedParams.dimAmount = 0.5f; + mExpandedParams.gravity = Gravity.CENTER; + + // Setup Listeners + setupCollapsedListener(); + setupExpandedListener(); + + // Add Collapsed View initially + try { + mWindowManager.addView(mCollapsedView, mCollapsedParams); + Log.d(TAG, "initViews: Collapsed view added"); + } catch (Exception e) { + Log.e(TAG, "initViews: Failed to add collapsed view", e); + } + } + + private void setupCollapsedListener() { + mCollapsedView.setOnTouchListener(new View.OnTouchListener() { + private int initialX; + private int initialY; + private float initialTouchX; + private float initialTouchY; + + @Override + public boolean onTouch(View v, MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + initialX = mCollapsedParams.x; + initialY = mCollapsedParams.y; + initialTouchX = event.getRawX(); + initialTouchY = event.getRawY(); + Log.d(TAG, "onTouch: ACTION_DOWN at " + initialTouchX + ", " + initialTouchY); + return true; + case MotionEvent.ACTION_UP: + int Xdiff = (int) (event.getRawX() - initialTouchX); + int Ydiff = (int) (event.getRawY() - initialTouchY); + Log.d(TAG, "onTouch: ACTION_UP, diff: " + Xdiff + ", " + Ydiff); + // If click (small movement) + if (Math.abs(Xdiff) < 10 && Math.abs(Ydiff) < 10) { + Log.d(TAG, "onTouch: Click detected, showing expanded view"); + showExpandedView(); + } + return true; + case MotionEvent.ACTION_MOVE: + mCollapsedParams.x = initialX + (int) (event.getRawX() - initialTouchX); + mCollapsedParams.y = initialY + (int) (event.getRawY() - initialTouchY); + mWindowManager.updateViewLayout(mCollapsedView, mCollapsedParams); + return true; + } + return false; + } + }); + + mCollapsedView.setOnDragListener((v, event) -> { + switch (event.getAction()) { + case DragEvent.ACTION_DRAG_STARTED: + Log.d(TAG, "onDrag: ACTION_DRAG_STARTED"); + if (event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN) || + event.getClipDescription().hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML)) { + v.setAlpha(1.0f); + return true; + } + return false; + case DragEvent.ACTION_DRAG_ENTERED: + Log.d(TAG, "onDrag: ACTION_DRAG_ENTERED"); + v.animate().scaleX(1.2f).scaleY(1.2f).setDuration(200).start(); + return true; + case DragEvent.ACTION_DRAG_EXITED: + Log.d(TAG, "onDrag: ACTION_DRAG_EXITED"); + v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(200).start(); + return true; + case DragEvent.ACTION_DROP: + Log.d(TAG, "onDrag: ACTION_DROP"); + ClipData.Item item = event.getClipData().getItemAt(0); + CharSequence text = item.getText(); + if (text != null) { + saveNote(text.toString()); + } + v.animate().scaleX(1.0f).scaleY(1.0f).setDuration(200).start(); + return true; + case DragEvent.ACTION_DRAG_ENDED: + Log.d(TAG, "onDrag: ACTION_DRAG_ENDED"); + v.setAlpha(0.8f); + return true; + } + return false; + }); + } + + private void setupExpandedListener() { + Button btnCancel = mExpandedView.findViewById(R.id.btn_cancel); + Button btnSave = mExpandedView.findViewById(R.id.btn_save); + EditText etContent = mExpandedView.findViewById(R.id.et_content); + + btnCancel.setOnClickListener(v -> showCollapsedView()); + + btnSave.setOnClickListener(v -> { + String content = etContent.getText().toString(); + if (!content.isEmpty()) { + saveNote(content); + etContent.setText(""); + showCollapsedView(); + } + }); + } + + private void showExpandedView() { + if (mCollapsedView.getParent() != null) { + mWindowManager.removeView(mCollapsedView); + } + if (mExpandedView.getParent() == null) { + mWindowManager.addView(mExpandedView, mExpandedParams); + + TextView tvSource = mExpandedView.findViewById(R.id.tv_source); + if (currentSourcePackage != null && !currentSourcePackage.isEmpty()) { + tvSource.setText("Source: " + currentSourcePackage); + tvSource.setVisibility(View.VISIBLE); + } else { + tvSource.setVisibility(View.GONE); + } + } + } + + private void showCollapsedView() { + if (mExpandedView.getParent() != null) { + mWindowManager.removeView(mExpandedView); + } + if (mCollapsedView.getParent() == null) { + mWindowManager.addView(mCollapsedView, mCollapsedParams); + } + } + + private void saveNote(String content) { + new Thread(() -> { + try { + // 1. Create new note in CAPSULE folder + long noteId = Note.getNewNoteId(this, Notes.ID_CAPSULE_FOLDER); + + // 2. Create Note object + Note note = new Note(); + note.setNoteValue(Notes.NoteColumns.ID, String.valueOf(noteId)); + note.setTextData(Notes.DataColumns.CONTENT, content); + + // Generate Summary (First 20 chars or first line) + String summary = content.length() > 20 ? content.substring(0, 20) + "..." : content; + int firstLineEnd = content.indexOf('\n'); + if (firstLineEnd > 0 && firstLineEnd < 20) { + summary = content.substring(0, firstLineEnd); + } + note.setNoteValue(Notes.NoteColumns.SNIPPET, summary); + + // Add Source Info if available + if (currentSourcePackage != null && !currentSourcePackage.isEmpty()) { + note.setTextData(Notes.DataColumns.DATA3, currentSourcePackage); + } + + boolean success = note.syncNote(this, noteId); + + mHandler.post(() -> { + if (success) { + Log.d(TAG, "saveNote: Success"); + Toast.makeText(this, "Saved to Notes", Toast.LENGTH_SHORT).show(); + } else { + Log.e(TAG, "saveNote: Failed"); + Toast.makeText(this, "Failed to save", Toast.LENGTH_SHORT).show(); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + Log.e(TAG, "saveNote: Exception", e); + mHandler.post(() -> Toast.makeText(this, "Error: " + e.getMessage(), Toast.LENGTH_SHORT).show()); + } + }).start(); + } + + private int dp2px(int dp) { + return (int) (dp * getResources().getDisplayMetrics().density); + } + + private void createNotificationChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel serviceChannel = new NotificationChannel( + CHANNEL_ID, + "Capsule Service Channel", + NotificationManager.IMPORTANCE_DEFAULT + ); + NotificationManager manager = getSystemService(NotificationManager.class); + if (manager != null) { + manager.createNotificationChannel(serviceChannel); + } + } + } + + private Notification createNotification() { + Notification.Builder builder; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + builder = new Notification.Builder(this, CHANNEL_ID); + } else { + builder = new Notification.Builder(this); + } + + return builder.setContentTitle("Global Capsule Running") + .setContentText("Tap to configure") + .setSmallIcon(R.mipmap.ic_launcher) + .build(); + } + + @Override + public void onDestroy() { + super.onDestroy(); + try { + unregisterReceiver(mSaveReceiver); + } catch (Exception e) { + Log.e(TAG, "Receiver not registered", e); + } + if (mCollapsedView != null && mCollapsedView.getParent() != null) { + mWindowManager.removeView(mCollapsedView); + } + if (mExpandedView != null && mExpandedView.getParent() != null) { + mWindowManager.removeView(mExpandedView); + } + } + + private void highlightCapsule() { + if (mCollapsedView != null && mCollapsedView.getParent() != null) { + mHandler.post(() -> { + mCollapsedView.animate().scaleX(1.5f).scaleY(1.5f).setDuration(200).withEndAction(() -> { + mCollapsedView.animate().scaleX(1.0f).scaleY(1.0f).setDuration(200).start(); + }).start(); + }); + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/capsule/ClipboardMonitorService.java b/src/Notesmaster/app/src/main/java/net/micode/notes/capsule/ClipboardMonitorService.java new file mode 100644 index 0000000..f9b93d5 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/capsule/ClipboardMonitorService.java @@ -0,0 +1,67 @@ +package net.micode.notes.capsule; + +import android.accessibilityservice.AccessibilityService; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.view.accessibility.AccessibilityEvent; +import android.widget.Toast; + +public class ClipboardMonitorService extends AccessibilityService { + + private ClipboardManager mClipboardManager; + private ClipboardManager.OnPrimaryClipChangedListener mClipListener; + private long mLastClipTime = 0; + private static final long MERGE_THRESHOLD = 2000; // 2 seconds + + @Override + public void onCreate() { + super.onCreate(); + mClipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + // Register clipboard listener + if (mClipboardManager != null) { + mClipListener = () -> { + handleClipChanged(); + }; + mClipboardManager.addPrimaryClipChangedListener(mClipListener); + } + } + + private void handleClipChanged() { + long now = System.currentTimeMillis(); + if (now - mLastClipTime < MERGE_THRESHOLD) { + // Notify CapsuleService to show "Merge" bubble + // For now just show a toast or log + // Intent intent = new Intent("net.micode.notes.capsule.ACTION_MERGE_SUGGESTION"); + // sendBroadcast(intent); + } + mLastClipTime = now; + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + if (event.getPackageName() != null) { + // Store current package name in CapsuleService + CapsuleService.setCurrentSourcePackage(event.getPackageName().toString()); + } + } + } + + @Override + public void onInterrupt() { + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (mClipboardManager != null && mClipListener != null) { + mClipboardManager.removePrimaryClipChangedListener(mClipListener); + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleActionActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleActionActivity.java new file mode 100644 index 0000000..dd47212 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleActionActivity.java @@ -0,0 +1,72 @@ +package net.micode.notes.ui; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.widget.Toast; +import android.util.Log; +import net.micode.notes.data.Notes; +import net.micode.notes.model.Note; +import net.micode.notes.capsule.CapsuleService; + +public class CapsuleActionActivity extends Activity { + + private static final String TAG = "CapsuleActionActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + CharSequence text = getIntent().getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT); + String sourcePackage = null; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) { + if (getReferrer() != null) { + sourcePackage = getReferrer().getAuthority(); // or getHost() + } + } + + if (text != null) { + saveNote(text.toString(), sourcePackage); + + // Notify CapsuleService to animate (if running) + Intent intent = new Intent("net.micode.notes.capsule.ACTION_SAVE_SUCCESS"); + sendBroadcast(intent); + } + + finish(); + } + + private void saveNote(String content, String source) { + new Thread(() -> { + try { + long noteId = Note.getNewNoteId(this, Notes.ID_CAPSULE_FOLDER); + Note note = new Note(); + note.setNoteValue(Notes.NoteColumns.ID, String.valueOf(noteId)); + note.setTextData(Notes.DataColumns.CONTENT, content); + + String summary = content.length() > 20 ? content.substring(0, 20) + "..." : content; + int firstLineEnd = content.indexOf('\n'); + if (firstLineEnd > 0 && firstLineEnd < 20) { + summary = content.substring(0, firstLineEnd); + } + note.setNoteValue(Notes.NoteColumns.SNIPPET, summary); + + if (source != null) { + note.setTextData(Notes.DataColumns.DATA3, source); + } + + boolean success = note.syncNote(this, noteId); + + runOnUiThread(() -> { + if (success) { + Toast.makeText(this, "已保存到胶囊", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "保存失败", Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListFragment.java new file mode 100644 index 0000000..0bebe45 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/CapsuleListFragment.java @@ -0,0 +1,164 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +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.model.Note; + +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +public class CapsuleListFragment extends Fragment { + + private RecyclerView mRecyclerView; + private CapsuleAdapter mAdapter; + private TextView mEmptyView; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_capsule_list, container, false); + mRecyclerView = view.findViewById(R.id.capsule_list); + mEmptyView = view.findViewById(R.id.tv_empty); + + mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + mAdapter = new CapsuleAdapter(); + mRecyclerView.setAdapter(mAdapter); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + loadCapsules(); + } + + private void loadCapsules() { + new Thread(() -> { + if (getContext() == null) return; + + // Query notes in CAPSULE folder + String selection = Notes.NoteColumns.PARENT_ID + "=?"; + String[] selectionArgs = new String[]{String.valueOf(Notes.ID_CAPSULE_FOLDER)}; + + Cursor cursor = getContext().getContentResolver().query( + Notes.CONTENT_NOTE_URI, + null, + selection, + selectionArgs, + Notes.NoteColumns.MODIFIED_DATE + " DESC" + ); + + List items = new ArrayList<>(); + if (cursor != null) { + while (cursor.moveToNext()) { + long id = cursor.getLong(cursor.getColumnIndexOrThrow(Notes.NoteColumns.ID)); + String snippet = cursor.getString(cursor.getColumnIndexOrThrow(Notes.NoteColumns.SNIPPET)); + long modifiedDate = cursor.getLong(cursor.getColumnIndexOrThrow(Notes.NoteColumns.MODIFIED_DATE)); + + // We need to fetch DATA3 (source) which is in DATA table. + // For performance, we might do a join or lazy load. + // For now, let's just use snippet and date. + // To get Source, we really should query DATA table or use a projection if CONTENT_NOTE_URI supports joining. + // NotesProvider usually joins. Let's check NoteColumns. + // Notes.DataColumns.DATA3 is NOT in NoteColumns. + + items.add(new CapsuleItem(id, snippet, modifiedDate, "Loading source...")); + } + cursor.close(); + } + + // Update UI + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + mAdapter.setItems(items); + mEmptyView.setVisibility(items.isEmpty() ? View.VISIBLE : View.GONE); + }); + } + }).start(); + } + + private static class CapsuleItem { + long id; + String summary; + long time; + String source; + + public CapsuleItem(long id, String summary, long time, String source) { + this.id = id; + this.summary = summary; + this.time = time; + this.source = source; + } + } + + private class CapsuleAdapter extends RecyclerView.Adapter { + private List mItems = new ArrayList<>(); + + public void setItems(List items) { + mItems = items; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_capsule, parent, false); + return new ViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + CapsuleItem item = mItems.get(position); + holder.tvSummary.setText(item.summary); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()); + holder.tvTime.setText(sdf.format(new Date(item.time))); + + if (item.source != null && !item.source.isEmpty()) { + holder.tvSource.setText("Source: " + item.source); + holder.tvSource.setVisibility(View.VISIBLE); + } else { + holder.tvSource.setVisibility(View.GONE); + } + + holder.itemView.setOnClickListener(v -> { + // Open Note Edit + // We need to implement this + }); + } + + @Override + public int getItemCount() { + return mItems.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + TextView tvSummary, tvTime, tvSource; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + tvSummary = itemView.findViewById(R.id.tv_summary); + tvTime = itemView.findViewById(R.id.tv_time); + tvSource = itemView.findViewById(R.id.tv_source); + } + } + } +} diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/FolderAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/FolderAdapter.java new file mode 100644 index 0000000..270d51f --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/FolderAdapter.java @@ -0,0 +1,91 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import net.micode.notes.R; +import net.micode.notes.data.NotesRepository; + +import java.util.ArrayList; +import java.util.List; + +public class FolderAdapter extends RecyclerView.Adapter { + + private Context context; + private List folders; + private long selectedFolderId = -1; + private OnFolderClickListener listener; + + public interface OnFolderClickListener { + void onFolderClick(long folderId); + } + + public FolderAdapter(Context context) { + this.context = context; + this.folders = new ArrayList<>(); + } + + public void setFolders(List folders) { + this.folders = folders != null ? folders : new ArrayList<>(); + notifyDataSetChanged(); + } + + public void setSelectedFolderId(long folderId) { + this.selectedFolderId = folderId; + notifyDataSetChanged(); + } + + public void setOnFolderClickListener(OnFolderClickListener listener) { + this.listener = listener; + } + + @NonNull + @Override + public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(context).inflate(R.layout.folder_tab_item, parent, false); + return new FolderViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull FolderViewHolder holder, int position) { + NotesRepository.NoteInfo folder = folders.get(position); + holder.bind(folder); + } + + @Override + public int getItemCount() { + return folders.size(); + } + + class FolderViewHolder extends RecyclerView.ViewHolder { + TextView tvName; + + public FolderViewHolder(View itemView) { + super(itemView); + tvName = itemView.findViewById(R.id.tv_folder_name); + itemView.setOnClickListener(v -> { + if (listener != null) { + int pos = getAdapterPosition(); + if (pos != RecyclerView.NO_POSITION) { + listener.onFolderClick(folders.get(pos).getId()); + } + } + }); + } + + public void bind(NotesRepository.NoteInfo folder) { + String name = folder.snippet; // Folder name is stored in snippet + if (name == null || name.isEmpty()) { + name = "Folder"; + } + tvName.setText(name); + tvName.setSelected(folder.getId() == selectedFolderId); + } + } +} \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java new file mode 100644 index 0000000..de8910c --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesListFragment.java @@ -0,0 +1,203 @@ +package net.micode.notes.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import android.app.AlertDialog; +import android.text.InputType; +import android.widget.EditText; +import android.widget.Toast; + +import android.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.data.NotesRepository; +import net.micode.notes.databinding.NoteListBinding; +import net.micode.notes.viewmodel.NotesListViewModel; + +public class NotesListFragment extends Fragment implements + NoteInfoAdapter.OnNoteItemClickListener, + NoteInfoAdapter.OnNoteItemLongClickListener { + + private static final String TAG = "NotesListFragment"; + private static final String PREF_KEY_IS_STAGGERED = "is_staggered"; + + private NotesListViewModel viewModel; + private NoteListBinding binding; + private NoteInfoAdapter adapter; + + private static final int REQUEST_CODE_OPEN_NODE = 102; + private static final int REQUEST_CODE_NEW_NODE = 103; + private static final int REQUEST_CODE_VERIFY_PASSWORD_FOR_OPEN = 107; + + private NotesRepository.NoteInfo pendingNote; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + binding = NoteListBinding.inflate(inflater, container, false); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + initViewModel(); + initViews(view); + observeViewModel(); + } + + private void initViewModel() { + NotesRepository repository = new NotesRepository(requireContext().getContentResolver()); + // Use requireActivity() to share ViewModel with Activity (for Sidebar filtering) + viewModel = new ViewModelProvider(requireActivity(), + new ViewModelProvider.Factory() { + @Override + public T create(Class modelClass) { + return (T) new NotesListViewModel(repository); + } + }).get(NotesListViewModel.class); + } + + private void initViews(View view) { + adapter = new NoteInfoAdapter(requireContext()); + binding.notesList.setAdapter(adapter); + + // Restore layout preference + boolean isStaggered = PreferenceManager.getDefaultSharedPreferences(requireContext()) + .getBoolean(PREF_KEY_IS_STAGGERED, true); + setLayoutManager(isStaggered); + + adapter.setOnNoteItemClickListener(this); + adapter.setOnNoteItemLongClickListener(this); + + // Fix FAB: Enable creating new notes + binding.btnNewNote.setOnClickListener(v -> { + Intent intent = new Intent(getActivity(), NoteEditActivity.class); + intent.setAction(Intent.ACTION_INSERT_OR_EDIT); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, viewModel.getCurrentFolderId()); + startActivityForResult(intent, REQUEST_CODE_NEW_NODE); + }); + } + + private void setLayoutManager(boolean isStaggered) { + if (isStaggered) { + StaggeredGridLayoutManager layoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); + layoutManager.setGapStrategy(StaggeredGridLayoutManager.GAP_HANDLING_MOVE_ITEMS_BETWEEN_SPANS); + binding.notesList.setLayoutManager(layoutManager); + } else { + binding.notesList.setLayoutManager(new LinearLayoutManager(requireContext())); + } + } + + public boolean toggleLayout() { + boolean isStaggered = binding.notesList.getLayoutManager() instanceof StaggeredGridLayoutManager; + boolean newIsStaggered = !isStaggered; + + setLayoutManager(newIsStaggered); + + PreferenceManager.getDefaultSharedPreferences(requireContext()) + .edit() + .putBoolean(PREF_KEY_IS_STAGGERED, newIsStaggered) + .apply(); + + return newIsStaggered; + } + + public boolean isStaggeredLayout() { + return binding.notesList.getLayoutManager() instanceof StaggeredGridLayoutManager; + } + + private void observeViewModel() { + viewModel.getNotesLiveData().observe(getViewLifecycleOwner(), notes -> { + adapter.setNotes(notes); + }); + + viewModel.getIsSelectionMode().observe(getViewLifecycleOwner(), isSelection -> { + adapter.setSelectionMode(isSelection); + }); + + viewModel.getSelectedIdsLiveData().observe(getViewLifecycleOwner(), selectedIds -> { + adapter.setSelectedIds(selectedIds); + }); + } + + @Override + public void onNoteItemClick(int position, long noteId) { + if (Boolean.TRUE.equals(viewModel.getIsSelectionMode().getValue())) { + boolean isSelected = viewModel.getSelectedIdsLiveData().getValue() != null && + viewModel.getSelectedIdsLiveData().getValue().contains(noteId); + viewModel.toggleNoteSelection(noteId, !isSelected); + return; + } + + if (viewModel.getNotesLiveData().getValue() != null && position < viewModel.getNotesLiveData().getValue().size()) { + NotesRepository.NoteInfo note = viewModel.getNotesLiveData().getValue().get(position); + if (note.type == Notes.TYPE_FOLDER) { + viewModel.enterFolder(note.getId()); + } else { + if (note.isLocked) { + pendingNote = note; + Intent intent = new Intent(getActivity(), PasswordActivity.class); + intent.setAction(PasswordActivity.ACTION_CHECK_PASSWORD); + startActivityForResult(intent, REQUEST_CODE_VERIFY_PASSWORD_FOR_OPEN); + } else { + openNoteEditor(note); + } + } + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_CODE_VERIFY_PASSWORD_FOR_OPEN && resultCode == android.app.Activity.RESULT_OK) { + if (pendingNote != null) { + openNoteEditor(pendingNote); + pendingNote = null; + } + } + } + + @Override + public void onNoteItemLongClick(int position, long noteId) { + if (!Boolean.TRUE.equals(viewModel.getIsSelectionMode().getValue())) { + viewModel.setIsSelectionMode(true); + viewModel.toggleNoteSelection(noteId, true); + } else { + boolean isSelected = viewModel.getSelectedIdsLiveData().getValue() != null && + viewModel.getSelectedIdsLiveData().getValue().contains(noteId); + viewModel.toggleNoteSelection(noteId, !isSelected); + } + } + + // Deprecated Context Menu + private void showContextMenu(NotesRepository.NoteInfo note) { + // ... kept for reference or removed + } + + private void openNoteEditor(NotesRepository.NoteInfo note) { + Intent intent = new Intent(getActivity(), NoteEditActivity.class); + intent.setAction(Intent.ACTION_VIEW); + intent.putExtra(Notes.INTENT_EXTRA_FOLDER_ID, note.getParentId()); + intent.putExtra(Intent.EXTRA_UID, note.getId()); + startActivityForResult(intent, REQUEST_CODE_OPEN_NODE); + } + + @Override + public void onResume() { + super.onResume(); + viewModel.refreshNotes(); + } +} \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java new file mode 100644 index 0000000..5dadec7 --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/NotesRecyclerAdapter.java @@ -0,0 +1,140 @@ +package net.micode.notes.ui; + +import android.content.Context; +import android.database.Cursor; +import android.text.format.DateUtils; +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.cardview.widget.CardView; +import androidx.recyclerview.widget.RecyclerView; + +import net.micode.notes.R; +import net.micode.notes.data.Notes; +import net.micode.notes.tool.ResourceParser; + +public class NotesRecyclerAdapter extends RecyclerView.Adapter { + + private Context mContext; + private Cursor mCursor; + private OnNoteItemClickListener mListener; + private boolean mChoiceMode; + + public interface OnNoteItemClickListener { + void onNoteClick(int position, long noteId); + boolean onNoteLongClick(int position, long noteId); + } + + public NotesRecyclerAdapter(Context context) { + mContext = context; + } + + public void setOnNoteItemClickListener(OnNoteItemClickListener listener) { + mListener = listener; + } + + public void swapCursor(Cursor newCursor) { + if (mCursor == newCursor) return; + if (mCursor != null) { + mCursor.close(); + } + mCursor = newCursor; + notifyDataSetChanged(); + } + + public Cursor getCursor() { + return mCursor; + } + + @NonNull + @Override + public NoteViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(mContext).inflate(R.layout.note_item, parent, false); + return new NoteViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull NoteViewHolder holder, int position) { + if (mCursor == null || !mCursor.moveToPosition(position)) { + return; + } + NoteItemData itemData = new NoteItemData(mContext, mCursor); + holder.bind(itemData, mChoiceMode, false); // Checked logic omitted for now + + holder.itemView.setOnClickListener(v -> { + if (mListener != null) { + mListener.onNoteClick(position, itemData.getId()); + } + }); + + holder.itemView.setOnLongClickListener(v -> { + if (mListener != null) { + return mListener.onNoteLongClick(position, itemData.getId()); + } + return false; + }); + } + + @Override + public int getItemCount() { + return mCursor == null ? 0 : mCursor.getCount(); + } + + class NoteViewHolder extends RecyclerView.ViewHolder { + CardView cardView; + TextView title, time, name; + ImageView typeIcon, lockIcon, alertIcon; + CheckBox checkBox; + + public NoteViewHolder(View itemView) { + super(itemView); + // Since root is CardView + cardView = (CardView) itemView; + title = itemView.findViewById(R.id.tv_title); + time = itemView.findViewById(R.id.tv_time); + name = itemView.findViewById(R.id.tv_name); + checkBox = itemView.findViewById(android.R.id.checkbox); + typeIcon = itemView.findViewById(R.id.iv_type_icon); + lockIcon = itemView.findViewById(R.id.iv_lock_icon); + alertIcon = itemView.findViewById(R.id.iv_alert_icon); + } + + public void bind(NoteItemData data, boolean choiceMode, boolean checked) { + if (choiceMode && data.getType() == Notes.TYPE_NOTE) { + checkBox.setVisibility(View.VISIBLE); + checkBox.setChecked(checked); + } else { + checkBox.setVisibility(View.GONE); + } + + if (data.getType() == Notes.TYPE_FOLDER) { + String snippet = data.getSnippet(); + if (snippet == null) snippet = ""; + title.setText(snippet + " (" + data.getNotesCount() + ")"); + time.setVisibility(View.GONE); + typeIcon.setVisibility(View.VISIBLE); + typeIcon.setImageResource(R.drawable.ic_folder); + cardView.setCardBackgroundColor(mContext.getColor(R.color.bg_white)); + } else { + typeIcon.setVisibility(View.GONE); + time.setVisibility(View.VISIBLE); + String titleStr = data.getTitle(); + if (titleStr == null || titleStr.isEmpty()) { + titleStr = data.getSnippet(); + } + title.setText(titleStr); + time.setText(DateUtils.getRelativeTimeSpanString(data.getModifiedDate())); + + // Background Color + int colorId = data.getBgColorId(); + int color = ResourceParser.getNoteBgColor(mContext, colorId); + cardView.setCardBackgroundColor(color); + } + } + } +} \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java new file mode 100644 index 0000000..8f9b68c --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/SettingsActivity.java @@ -0,0 +1,30 @@ +package net.micode.notes.ui; + +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.PreferenceFragmentCompat; +import net.micode.notes.R; + +public class SettingsActivity extends AppCompatActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_settings); + if (savedInstanceState == null) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings_container, new SettingsFragment()) + .commit(); + } + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setTitle(R.string.menu_settings); + } + } + + @Override + public boolean onSupportNavigateUp() { + finish(); + return true; + } +} \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListFragment.java b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListFragment.java new file mode 100644 index 0000000..2f04d4b --- /dev/null +++ b/src/Notesmaster/app/src/main/java/net/micode/notes/ui/TaskListFragment.java @@ -0,0 +1,123 @@ +package net.micode.notes.ui; + +import android.content.Intent; +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.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 TaskListFragment extends Fragment implements TaskListAdapter.OnTaskItemClickListener { + + private RecyclerView recyclerView; + private TaskListAdapter adapter; + private FloatingActionButton fab; + private static final int REQUEST_EDIT_TASK = 1001; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.activity_task_list, container, false); + + // Hide Toolbar in fragment if Activity has one or Tabs + // For now, let's keep it but remove navigation logic or hide it if needed + View toolbar = view.findViewById(R.id.toolbar); + if (toolbar != null) { + // toolbar.setVisibility(View.GONE); // Optional: Hide if using main tabs + } + + recyclerView = view.findViewById(R.id.task_list_view); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + adapter = new TaskListAdapter(getContext(), this); + recyclerView.setAdapter(adapter); + + fab = view.findViewById(R.id.btn_new_task); + fab.setOnClickListener(v -> { + Intent intent = new Intent(getActivity(), TaskEditActivity.class); + startActivityForResult(intent, REQUEST_EDIT_TASK); + }); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + loadTasks(); + } + + private void loadTasks() { + new Thread(() -> { + if (getContext() == null) return; + Cursor cursor = getContext().getContentResolver().query( + Notes.CONTENT_NOTE_URI, + null, + NoteColumns.TYPE + "=?", + new String[]{String.valueOf(Notes.TYPE_TASK)}, + null + ); + + List tasks = new ArrayList<>(); + if (cursor != null) { + while (cursor.moveToNext()) { + tasks.add(Task.fromCursor(cursor)); + } + cursor.close(); + } + + if (getActivity() != null) { + getActivity().runOnUiThread(() -> adapter.setTasks(tasks)); + } + }).start(); + } + + @Override + public void onItemClick(Task task) { + Intent intent = new Intent(getActivity(), 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(() -> { + if (getContext() != null) { + task.save(getContext()); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> loadTasks()); + } + } + }).start(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == REQUEST_EDIT_TASK && resultCode == android.app.Activity.RESULT_OK) { + loadTasks(); + } + } +} diff --git a/src/Notesmaster/app/src/main/res/color/selector_bottom_nav_color.xml b/src/Notesmaster/app/src/main/res/color/selector_bottom_nav_color.xml new file mode 100644 index 0000000..3c57e92 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/color/selector_bottom_nav_color.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/capsule_collapsed_bg.xml b/src/Notesmaster/app/src/main/res/drawable/capsule_collapsed_bg.xml new file mode 100644 index 0000000..dc7140e --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/capsule_collapsed_bg.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/folder_tab_bg_selector.xml b/src/Notesmaster/app/src/main/res/drawable/folder_tab_bg_selector.xml new file mode 100644 index 0000000..a180cd1 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/folder_tab_bg_selector.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/ic_checkbox_checked_round.xml b/src/Notesmaster/app/src/main/res/drawable/ic_checkbox_checked_round.xml new file mode 100644 index 0000000..03f6369 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/ic_checkbox_checked_round.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/ic_checkbox_unchecked_round.xml b/src/Notesmaster/app/src/main/res/drawable/ic_checkbox_unchecked_round.xml new file mode 100644 index 0000000..0859dbf --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/ic_checkbox_unchecked_round.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/ic_menu_hamburger.xml b/src/Notesmaster/app/src/main/res/drawable/ic_menu_hamburger.xml new file mode 100644 index 0000000..7ff2549 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/ic_menu_hamburger.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/ic_view_grid.xml b/src/Notesmaster/app/src/main/res/drawable/ic_view_grid.xml new file mode 100644 index 0000000..00cee0b --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/ic_view_grid.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/ic_view_list.xml b/src/Notesmaster/app/src/main/res/drawable/ic_view_list.xml new file mode 100644 index 0000000..58b67b3 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/ic_view_list.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/search_bar_bg.xml b/src/Notesmaster/app/src/main/res/drawable/search_bar_bg.xml new file mode 100644 index 0000000..3feadd2 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/search_bar_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/drawable/selector_checkbox_round.xml b/src/Notesmaster/app/src/main/res/drawable/selector_checkbox_round.xml new file mode 100644 index 0000000..fdd9ce6 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/drawable/selector_checkbox_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/activity_home.xml b/src/Notesmaster/app/src/main/res/layout/activity_home.xml new file mode 100644 index 0000000..1a3cd5a --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/folder_tab_item.xml b/src/Notesmaster/app/src/main/res/layout/folder_tab_item.xml new file mode 100644 index 0000000..7645a63 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/folder_tab_item.xml @@ -0,0 +1,15 @@ + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/fragment_capsule_list.xml b/src/Notesmaster/app/src/main/res/layout/fragment_capsule_list.xml new file mode 100644 index 0000000..e7df63f --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/fragment_capsule_list.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + diff --git a/src/Notesmaster/app/src/main/res/layout/item_capsule.xml b/src/Notesmaster/app/src/main/res/layout/item_capsule.xml new file mode 100644 index 0000000..650e18b --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/item_capsule.xml @@ -0,0 +1,36 @@ + + + + + + + + + + diff --git a/src/Notesmaster/app/src/main/res/layout/layout_capsule_collapsed.xml b/src/Notesmaster/app/src/main/res/layout/layout_capsule_collapsed.xml new file mode 100644 index 0000000..db340b7 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/layout_capsule_collapsed.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/src/Notesmaster/app/src/main/res/layout/layout_capsule_expanded.xml b/src/Notesmaster/app/src/main/res/layout/layout_capsule_expanded.xml new file mode 100644 index 0000000..62d1115 --- /dev/null +++ b/src/Notesmaster/app/src/main/res/layout/layout_capsule_expanded.xml @@ -0,0 +1,48 @@ + + + + + + + + +