support badge
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:window:allow-set-overlay-icon",
|
||||
"opener:default",
|
||||
"notification:default"
|
||||
]
|
||||
|
||||
@@ -220,3 +220,10 @@ pub fn toggle_reminder(path: &str, id: i64, is_completed: bool) -> anyhow::Resul
|
||||
conn.execute("UPDATE reminders SET is_completed=?1 WHERE id=?2", params![is_completed, id])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get_overdue_reminders_count(path: &str, date: &str, minute: i32) -> anyhow::Result<i32> {
|
||||
let conn = Connection::open(path)?;
|
||||
let mut stmt = conn.prepare("SELECT COUNT(*) FROM reminders WHERE is_completed = 0 AND (date < ?1 OR (date = ?1 AND minute < ?2))")?;
|
||||
let count: i32 = stmt.query_row(params![date, minute], |row| row.get(0))?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,13 @@ pub fn toggle_reminder(state: tauri::State<'_, AppState>, id: i64, is_completed:
|
||||
crate::db::toggle_reminder(path, id, is_completed).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_overdue_reminders_count(state: tauri::State<'_, AppState>, date: String, minute: i32) -> Result<i32, String> {
|
||||
let path = state.db_path.lock().unwrap();
|
||||
let path = path.as_ref().ok_or("Database path not set")?;
|
||||
crate::db::get_overdue_reminders_count(path, &date, minute).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn update_interval(state: tauri::State<'_, AppState>, seconds: u64) {
|
||||
state.capture_interval_secs.store(seconds, Ordering::SeqCst);
|
||||
|
||||
@@ -36,6 +36,7 @@ pub fn run() {
|
||||
engine::save_reminder,
|
||||
engine::delete_reminder,
|
||||
engine::toggle_reminder,
|
||||
engine::get_overdue_reminders_count,
|
||||
engine::write_file
|
||||
])
|
||||
.setup(|app| {
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
retainDays, captureInterval,
|
||||
TIME_OFFSET_MINUTES, TOTAL_MINUTES, getTagColor, getTagName, mainTags, getSubTags,
|
||||
logicalMinutesToTime, logicalMinutesFromTime, formatDuration,
|
||||
loadTags, loadEvents, loadReminders, toISODate
|
||||
loadTags, loadEvents, loadReminders, toISODate, refreshBadgeCount
|
||||
} from "./store";
|
||||
import { DBEvent, TimelineItem } from "./types";
|
||||
|
||||
@@ -109,6 +109,8 @@ const updateCurrentMinute = () => {
|
||||
} else {
|
||||
currentLogicalMinute.value = -1;
|
||||
}
|
||||
// 无论是否看今天,每分钟都尝试刷新全局徽章
|
||||
refreshBadgeCount();
|
||||
};
|
||||
|
||||
let currentMinuteTimeout: number | null = null;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ref, computed } from "vue";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { Image } from "@tauri-apps/api/image";
|
||||
import { Tag, DBEvent, TimelineItem, Toast, Reminder } from "../types";
|
||||
|
||||
export const TIME_OFFSET_MINUTES = 180;
|
||||
@@ -111,4 +113,73 @@ export const loadEvents = async () => {
|
||||
};
|
||||
export const loadReminders = async () => {
|
||||
reminders.value = await invoke("get_reminders", { date: currentDate.value });
|
||||
refreshBadgeCount();
|
||||
};
|
||||
|
||||
export const overdueCount = ref(0);
|
||||
|
||||
// 获取当前真实的逻辑时间和日期(不依赖 UI 状态)
|
||||
export const getRealNowLogicalTime = () => {
|
||||
const now = new Date();
|
||||
const d = new Date(now.getTime() - TIME_OFFSET_MINUTES * 60000);
|
||||
const date = toISODate(d);
|
||||
const m = now.getHours() * 60 + now.getMinutes();
|
||||
const minute = (m < TIME_OFFSET_MINUTES ? m + 1440 : m) - TIME_OFFSET_MINUTES;
|
||||
return { date, minute };
|
||||
};
|
||||
|
||||
export const refreshBadgeCount = async () => {
|
||||
if (!dbPath.value) return;
|
||||
try {
|
||||
const { date, minute } = getRealNowLogicalTime();
|
||||
const count: number = await invoke("get_overdue_reminders_count", {
|
||||
date: date,
|
||||
minute: minute
|
||||
});
|
||||
overdueCount.value = count;
|
||||
await updateTaskbarBadge(count);
|
||||
} catch (e) {
|
||||
console.error("Failed to refresh badge count:", e);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const updateTaskbarBadge = async (count: number) => {
|
||||
try {
|
||||
const win = getCurrentWindow();
|
||||
if (count <= 0) {
|
||||
await win.setOverlayIcon(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const size = 64;
|
||||
canvas.width = size;
|
||||
canvas.height = size;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
if (!ctx) return;
|
||||
|
||||
// Draw red circle
|
||||
ctx.fillStyle = '#FF3B30';
|
||||
ctx.beginPath();
|
||||
ctx.arc(size/2, size/2, size/2 - 2, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
||||
// Draw white text
|
||||
ctx.fillStyle = '#FFFFFF';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.font = 'bold 38px Arial';
|
||||
const displayCount = count > 99 ? '99+' : count.toString();
|
||||
if (displayCount.length > 2) ctx.font = 'bold 28px Arial';
|
||||
ctx.fillText(displayCount, size/2, size/2 + 2);
|
||||
|
||||
// Get raw RGBA pixels and wrap in Tauri Image object
|
||||
const imageData = ctx.getImageData(0, 0, size, size);
|
||||
const img = await Image.new(new Uint8Array(imageData.data), size, size);
|
||||
|
||||
await win.setOverlayIcon(img);
|
||||
} catch (e) {
|
||||
console.error("Failed to set overlay icon:", e);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user