将简单的日程列表升级为功能完整的日程管理系统 #30

Merged
pvexk5qol merged 1 commits from caoweiqiong_branch into master 4 weeks ago

@ -237,6 +237,10 @@ public class Notes {
* Type: TEXT
*/
public static final String EMOTION_TAG = "emotion_tag";
public static final String AGENDA_END_DATE = "agenda_end_date";
public static final String TIME_LABEL = "time_label";
}
// [新增] 用户账号表列名定义

@ -30,7 +30,7 @@ import net.micode.notes.data.Notes.NoteColumns;
public class NotesDatabaseHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "note.db";
private static final int DB_VERSION = 9;
private static final int DB_VERSION = 10;
public interface TABLE {
public static final String NOTE = "note";
@ -75,7 +75,9 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
NoteColumns.IS_AGENDA + " INTEGER DEFAULT 0," +
NoteColumns.AGENDA_DATE + " INTEGER DEFAULT 0," +
NoteColumns.IS_COMPLETED + " INTEGER DEFAULT 0," +
NoteColumns.EMOTION_TAG + " TEXT" +
NoteColumns.EMOTION_TAG + " TEXT," +
NoteColumns.AGENDA_END_DATE + " INTEGER DEFAULT 0," +
NoteColumns.TIME_LABEL + " TEXT DEFAULT ''" +
")";
// [新增] 用户表建表语句
@ -380,6 +382,12 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
oldVersion = 9;
}
// [新增点] 处理 v10 的升级
if (oldVersion < 10) {
upgradeToV10(db);
oldVersion = 10;
}
if (reCreateTriggers) {
reCreateNoteTableTriggers(db);
reCreateDataTableTriggers(db);
@ -391,6 +399,22 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
}
}
private void upgradeToV10(SQLiteDatabase db) {
Log.d(TAG, "Upgrading database to version 10...");
// 使用 try-catch 确保即使字段已存在(手动清过数据的情况)也不会崩
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.AGENDA_END_DATE + " INTEGER DEFAULT 0");
} catch (Exception e) {
Log.w(TAG, "Column agenda_end_date already exists");
}
try {
db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN " + NoteColumns.TIME_LABEL + " TEXT DEFAULT ''");
} catch (Exception e) {
Log.w(TAG, "Column time_label already exists");
}
}
// 确保存在 V6 升级方法,防止字段缺失
private void upgradeToV6(SQLiteDatabase db) {
// 使用 try-catch 忽略"列已存在"的错误,防止重复添加崩溃
@ -451,5 +475,7 @@ public class NotesDatabaseHelper extends SQLiteOpenHelper {
// 2. 创建新表
try { db.execSQL(CREATE_USER_TABLE_SQL); } catch(Exception e){ Log.e(TAG, "Create user table failed: " + e); }
try { db.execSQL(CREATE_CHAT_TABLE_SQL); } catch(Exception e){ Log.e(TAG, "Create chat table failed: " + e); }
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN agenda_end_date INTEGER DEFAULT 0"); } catch(Exception e){}
try { db.execSQL("ALTER TABLE " + TABLE.NOTE + " ADD COLUMN time_label TEXT DEFAULT ''"); } catch(Exception e){}
}
}

@ -1,12 +1,17 @@
package net.micode.notes.ui;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CalendarView;
import android.widget.EditText;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -16,18 +21,24 @@ import androidx.recyclerview.widget.RecyclerView;
import net.micode.notes.R;
import net.micode.notes.data.Notes;
import net.micode.notes.data.Notes.NoteColumns;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import android.widget.ImageButton; // [关键修复] 导入图片按钮类
public class AgendaFragment extends Fragment {
private RecyclerView mRecyclerView;
private NotesListItemAdapter mAdapter;
private AgendaAdapter mAdapter; // [关键修复] 类型改为 AgendaAdapter
private CalendarView mCalendarView;
private TextView mTvEmpty;
private long mSelectedDayStart;
private long mSelectedDayEnd;
private TextView tvDateHeader;
private View emptyState;
private View emptyState; // <--- 确保有这一行
private long mSelectedDayStart, mSelectedDayEnd;
private boolean isCalendarExpanded = true;
private ImageButton btnToggleCalendar;
private int mQuickHour = -1;
private int mQuickMinute = -1;
private TextView tvQuickTime;
@Nullable
@Override
@ -42,86 +53,157 @@ public class AgendaFragment extends Fragment {
mRecyclerView = view.findViewById(R.id.agenda_list);
tvDateHeader = view.findViewById(R.id.tv_agenda_date_header);
emptyState = view.findViewById(R.id.ll_empty_state);
btnToggleCalendar = view.findViewById(R.id.btn_toggle_calendar);
// [新增] 日历折叠逻辑
btnToggleCalendar.setOnClickListener(v -> {
if (isCalendarExpanded) {
// 执行收起
mCalendarView.setVisibility(View.GONE);
btnToggleCalendar.setImageResource(android.R.drawable.arrow_down_float);
isCalendarExpanded = false;
} else {
// 执行展开
mCalendarView.setVisibility(View.VISIBLE);
btnToggleCalendar.setImageResource(android.R.drawable.arrow_up_float);
isCalendarExpanded = true;
}
});
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mAdapter = new NotesListItemAdapter(getContext());
mAdapter = new AgendaAdapter(getContext()); // [修复] 赋值
mRecyclerView.setAdapter(mAdapter);
// 默认显示今天
updateDateDisplay(Calendar.getInstance().get(Calendar.YEAR),
Calendar.getInstance().get(Calendar.MONTH),
Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
mCalendarView.setOnDateChangeListener((v, y, m, d) -> {
updateDateDisplay(y, m, d);
loadAgendaData();
// [体验优化] 点击日期后,如果日历是展开的,则自动收起
if (isCalendarExpanded) {
mCalendarView.setVisibility(View.GONE);
btnToggleCalendar.setImageResource(android.R.drawable.arrow_down_float);
isCalendarExpanded = false;
}
});
// 设置监听器
mAdapter.setOnAgendaActionListener(new AgendaAdapter.OnAgendaActionListener() {
@Override
public void onToggleComplete(AgendaAdapter.AgendaItem item) {
ContentValues values = new ContentValues();
values.put(NoteColumns.IS_COMPLETED, item.isCompleted ? 0 : 1);
getContext().getContentResolver().update(Notes.CONTENT_NOTE_URI, values, NoteColumns.ID + "=?", new String[]{String.valueOf(item.id)});
loadAgendaData();
}
@Override
public void onDelete(AgendaAdapter.AgendaItem item) {
new AlertDialog.Builder(getContext())
.setTitle("确认删除")
.setMessage("删除此日程?")
.setPositiveButton("删除", (d, w) -> {
getContext().getContentResolver().delete(Notes.CONTENT_NOTE_URI, NoteColumns.ID + "=?", new String[]{String.valueOf(item.id)});
loadAgendaData();
}).setNegativeButton("取消", null).show();
}
});
tvQuickTime = view.findViewById(R.id.tv_set_quick_time);
// 1. 点击“全天”弹出时间选择器
tvQuickTime.setOnClickListener(v -> {
Calendar now = Calendar.getInstance();
new android.app.TimePickerDialog(getContext(), (view1, hourOfDay, minute) -> {
mQuickHour = hourOfDay;
mQuickMinute = minute;
tvQuickTime.setText(String.format("%02d:%02d", hourOfDay, minute));
}, now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE), true).show();
});
mCalendarView.setOnDateChangeListener((view1, year, month, dayOfMonth) -> {
updateDateDisplay(year, month, dayOfMonth);
// 2. 修改 btn_quick_add 的点击逻辑
view.findViewById(R.id.btn_quick_add).setOnClickListener(v -> {
EditText et = view.findViewById(R.id.et_quick_add);
String title = et.getText().toString().trim();
if (TextUtils.isEmpty(title)) return;
ContentValues cv = new ContentValues();
cv.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
cv.put(NoteColumns.IS_AGENDA, 1);
cv.put(NoteColumns.SNIPPET, title);
// 计算具体时间
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(mSelectedDayStart); // 基于日历选中的那一天
if (mQuickHour != -1) {
cal.set(Calendar.HOUR_OF_DAY, mQuickHour);
cal.set(Calendar.MINUTE, mQuickMinute);
cv.put(NoteColumns.TIME_LABEL, String.format("%02d:%02d", mQuickHour, mQuickMinute));
cv.put(NoteColumns.AGENDA_DATE, cal.getTimeInMillis());
cv.put(NoteColumns.AGENDA_END_DATE, cal.getTimeInMillis()); // 点事件
} else {
cv.put(NoteColumns.TIME_LABEL, "全天");
cv.put(NoteColumns.AGENDA_DATE, mSelectedDayStart);
cv.put(NoteColumns.AGENDA_END_DATE, mSelectedDayEnd);
}
cv.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
cv.put(NoteColumns.LOCAL_MODIFIED, 1);
getContext().getContentResolver().insert(Notes.CONTENT_NOTE_URI, cv);
// 重置 UI
et.setText("");
tvQuickTime.setText("全天");
mQuickHour = -1;
mQuickMinute = -1;
loadAgendaData();
});
mCalendarView.setOnDateChangeListener((v, y, m, d) -> {
updateDateDisplay(y, m, d);
loadAgendaData();
});
updateDateDisplay(Calendar.getInstance().get(Calendar.YEAR), Calendar.getInstance().get(Calendar.MONTH), Calendar.getInstance().get(Calendar.DAY_OF_MONTH));
loadAgendaData();
}
private void updateDateDisplay(int year, int month, int day) {
tvDateHeader.setText(String.format("%d年%d月%d日 的日程", year, month + 1, day));
Calendar cal = Calendar.getInstance();
cal.set(year, month, day);
calculateDayRange(cal.getTimeInMillis());
}
// 计算选中日期的 [00:00:00, 23:59:59] 时间戳
private void calculateDayRange(long timeInMillis) {
Calendar cal = Calendar.getInstance();
cal.setTimeInMillis(timeInMillis);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.set(year, month, day, 0, 0, 0);
mSelectedDayStart = cal.getTimeInMillis();
cal.set(Calendar.HOUR_OF_DAY, 23);
cal.set(Calendar.MINUTE, 59);
cal.set(Calendar.SECOND, 59);
cal.set(year, month, day, 23, 59, 59);
mSelectedDayEnd = cal.getTimeInMillis();
}
private void loadAgendaData() {
// 增加空检查,防止 fragment 已经 detach 时还执行异步回调
if (!isAdded() || getContext() == null) return;
ContentResolver resolver = getContext().getContentResolver();
// 使用异步查询(推荐)或者简单的后台线程
new Thread(() -> {
String selection = NoteColumns.IS_AGENDA + "=1 AND "
+ NoteColumns.AGENDA_DATE + " >= ? AND "
+ NoteColumns.AGENDA_DATE + " <= ?";
String[] selectionArgs = new String[] {
String.valueOf(mSelectedDayStart),
String.valueOf(mSelectedDayEnd)
};
// 执行查询
final Cursor cursor = resolver.query(
Notes.CONTENT_NOTE_URI,
NoteItemData.PROJECTION,
selection,
selectionArgs,
NoteColumns.AGENDA_DATE + " ASC"
);
// 切回主线程更新 UI
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
mAdapter.changeCursor(cursor);
if (cursor == null || cursor.getCount() == 0) {
emptyState.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
} else {
emptyState.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
}
});
if (getContext() == null) return;
Cursor cursor = getContext().getContentResolver().query(
Notes.CONTENT_NOTE_URI,
new String[]{NoteColumns.ID, NoteColumns.SNIPPET, NoteColumns.TIME_LABEL, NoteColumns.AGENDA_DATE, NoteColumns.IS_COMPLETED},
// [关键修复] 删掉最后的 OR agenda_end_date = 0
NoteColumns.IS_AGENDA + "=1 AND " + NoteColumns.AGENDA_DATE + "<=? AND " + NoteColumns.AGENDA_END_DATE + ">=?",
new String[]{String.valueOf(mSelectedDayEnd), String.valueOf(mSelectedDayStart)},
null);
List<AgendaAdapter.AgendaItem> list = new ArrayList<>();
if (cursor != null) {
while (cursor.moveToNext()) {
AgendaAdapter.AgendaItem item = new AgendaAdapter.AgendaItem();
item.id = cursor.getLong(0);
item.title = cursor.getString(1);
item.timeLabel = cursor.getString(2);
item.startTime = cursor.getLong(3);
item.isCompleted = cursor.getInt(4) == 1;
list.add(item);
}
}).start();
cursor.close();
}
mAdapter.updateData(list);
// [关键修复] 使用 emptyState 控制显示
if (emptyState != null) {
emptyState.setVisibility(list.isEmpty() ? View.VISIBLE : View.GONE);
}
}
}

@ -61,6 +61,9 @@ import android.view.MotionEvent;
import android.net.Uri;
import android.graphics.drawable.Drawable;
import android.graphics.Color;
import net.micode.notes.tool.DeepSeekHelper;
import android.app.ProgressDialog;
import net.micode.notes.data.Notes.NoteColumns;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
@ -1200,12 +1203,98 @@ public class NoteEditActivity extends AppCompatActivity implements OnClickListen
case R.id.menu_delete_remind:
mWorkingNote.setAlertDate(0, false);
break;
case R.id.menu_add_to_agenda_ai:
performAIAnalysis(); // [新增]
break;
default:
break;
}
return true;
}
// 3. 添加核心处理方法
private void performAIAnalysis() {
// 获取当前便签内容(自动保存先,确保内容最新)
saveNote();
String content = mWorkingNote.getContent();
// 简单的去HTML标签处理如果之前没做这里简单做一个避免把HTML标签发给AI
String plainText = android.text.Html.fromHtml(content).toString();
if (TextUtils.isEmpty(plainText.trim())) {
showToast(R.string.error_note_empty_for_clock); // 复用“内容为空”的提示
return;
}
// 显示加载框
ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setMessage("DeepSeek 正在分析日程...");
progressDialog.setCancelable(false);
progressDialog.show();
// 调用 AI
DeepSeekHelper.analyzeContent(plainText, new DeepSeekHelper.AICallback() {
@Override
public void onSuccess(java.util.List<DeepSeekHelper.AgendaResult> results) {
progressDialog.dismiss();
int count = 0;
for (DeepSeekHelper.AgendaResult res : results) {
try {
// --- 第一步:插入 Note 表 (增强型元数据) ---
ContentValues noteValues = new ContentValues();
noteValues.put(NoteColumns.TYPE, Notes.TYPE_NOTE);
noteValues.put(NoteColumns.IS_AGENDA, 1);
// 在 NoteEditActivity.java 中确保 end_date 有值
noteValues.put(Notes.NoteColumns.AGENDA_DATE, res.startTime);
// [关键修复] 如果 AI 返回的结束时间等于开始时间(点事件),
// 也要确保 end_date 被写入,而不是保留默认值 0
noteValues.put(Notes.NoteColumns.AGENDA_END_DATE, res.endTime > 0 ? res.endTime : res.startTime);
// [新增] 存储显示标签(如 "16:00" 或 "12月"
noteValues.put(NoteColumns.TIME_LABEL, res.timeLabel);
noteValues.put(NoteColumns.SNIPPET, res.title);
noteValues.put(NoteColumns.PARENT_ID, Notes.ID_ROOT_FOLDER);
noteValues.put(NoteColumns.LOCAL_MODIFIED, 1);
Uri newNoteUri = getContentResolver().insert(Notes.CONTENT_NOTE_URI, noteValues);
if (newNoteUri != null) {
long newNoteId = ContentUris.parseId(newNoteUri);
// --- 第二步:插入 Data 表 (正文内容) ---
ContentValues dataValues = new ContentValues();
dataValues.put(Notes.DataColumns.NOTE_ID, newNoteId);
dataValues.put(Notes.DataColumns.MIME_TYPE, Notes.TextNote.CONTENT_ITEM_TYPE);
// AI 生成的备注信息
dataValues.put(Notes.DataColumns.CONTENT, "AI 识别日程:" + res.title);
getContentResolver().insert(Notes.CONTENT_DATA_URI, dataValues);
count++;
}
} catch (Exception e) {
Log.e(TAG, "Insert AI Agenda Failed", e);
}
}
if (count > 0) {
Toast.makeText(NoteEditActivity.this,
"成功添加 " + count + " 个日程到日历", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(NoteEditActivity.this,
"添加失败,请查看日志", Toast.LENGTH_SHORT).show();
}
}
@Override
public void onFailure(String error) {
progressDialog.dismiss();
Toast.makeText(NoteEditActivity.this, error, Toast.LENGTH_LONG).show();
}
});
}
private void setReminder() {
DateTimePickerDialog d = new DateTimePickerDialog(this, System.currentTimeMillis());
d.setOnDateTimeSetListener(new OnDateTimeSetListener() {

@ -500,7 +500,7 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe
} else if (itemId == R.id.nav_agenda) {
// --- 切换到日程视图模式 ---
findViewById(R.id.fragment_container).setVisibility(View.VISIBLE);
if (getSupportActionBar() != null) getSupportActionBar().setTitle("智能日程");
if (getSupportActionBar() != null) getSupportActionBar().setTitle("日程");
// 使用 TAG 查找,避免重复创建引发的 GMS 校验冲突
androidx.fragment.app.Fragment agenda = fm.findFragmentByTag("AGENDA");
@ -982,28 +982,38 @@ public class NotesListActivity extends AppCompatActivity implements OnClickListe
String[] selectionArgs;
if (Intent.ACTION_SEARCH.equals(getIntent().getAction())) {
// --- 分支 1搜索模式 ---
String query = getIntent().getStringExtra(SearchManager.QUERY);
// [修改点] 搜索时除了排除回收站,也要排除日程项,保证主列表搜索结果纯净
selection = NoteColumns.SNIPPET + " LIKE ? AND " + NoteColumns.PARENT_ID + "<>"
+ Notes.ID_TRASH_FOLER;
+ Notes.ID_TRASH_FOLER + " AND " + NoteColumns.IS_AGENDA + "=0";
selectionArgs = new String[] {
"%" + query + "%"
"%" + query + "%"
};
} else if (mState == ListEditState.PRIVATE_FOLDER) {
// [新增] 私密模式查询:查询所有 is_private = 1 的便签
selection = NoteColumns.IS_PRIVATE + "=1 AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE;
// --- 分支 2私密文件夹模式 ---
// [修改点] 即使在私密空间我们也希望日程只出现在“日程”Tab中所以追加 IS_AGENDA=0
selection = NoteColumns.IS_PRIVATE + "=1 AND " + NoteColumns.TYPE + "=" + Notes.TYPE_NOTE
+ " AND " + NoteColumns.IS_AGENDA + "=0";
selectionArgs = null;
} else {
// [修改] 普通模式:必须增加 is_private = 0 的条件,防止私密便签显示在普通列表中
// --- 分支 3普通/根目录/自定义文件夹模式 ---
// ROOT_FOLDER_SELECTION 或 NORMAL_SELECTION 已经包含了 parent_id 的过滤
selection = (mCurrentFolderId == Notes.ID_ROOT_FOLDER) ? ROOT_FOLDER_SELECTION
: NORMAL_SELECTION;
// 拼接过滤私密的条件
selection += " AND " + NoteColumns.IS_PRIVATE + "=0";
// [核心修改点]
// 1. 排除私密便签 (IS_PRIVATE=0)
// 2. 排除日程项 (IS_AGENDA=0)
// 这样日程项就会从“便签”Tab 彻底消失只在“日程”Tab 通过特定查询显示
selection += " AND " + NoteColumns.IS_PRIVATE + "=0 AND " + NoteColumns.IS_AGENDA + "=0";
selectionArgs = new String[] {
String.valueOf(mCurrentFolderId)
};
}
// 执行查询:排序规则保持不变(类型、置顶、修改时间)
mBackgroundQueryHandler.startQuery(FOLDER_NOTE_LIST_QUERY_TOKEN, null,
Notes.CONTENT_NOTE_URI, NoteItemData.PROJECTION, selection, selectionArgs,
NoteColumns.TYPE + " DESC," + NoteColumns.IS_PINNED + " DESC," + NoteColumns.MODIFIED_DATE + " DESC");

Loading…
Cancel
Save