From 2d51467bb9186ba3faed1fc3462c07e8ccdfdc62 Mon Sep 17 00:00:00 2001 From: Julian Freeman Date: Fri, 27 Mar 2026 12:02:41 -0400 Subject: [PATCH] support badge --- src-tauri/capabilities/default.json | 1 + src-tauri/src/db.rs | 7 +++ src-tauri/src/engine.rs | 7 +++ src-tauri/src/lib.rs | 1 + src/App.vue | 4 +- src/store/index.ts | 71 +++++++++++++++++++++++++++++ 6 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4625545..634b104 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -5,6 +5,7 @@ "windows": ["main"], "permissions": [ "core:default", + "core:window:allow-set-overlay-icon", "opener:default", "notification:default" ] diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 6525164..1f05e2d 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -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 { + 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) +} diff --git a/src-tauri/src/engine.rs b/src-tauri/src/engine.rs index 415dfa1..75d3fa1 100644 --- a/src-tauri/src/engine.rs +++ b/src-tauri/src/engine.rs @@ -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 { + 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); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9fead5c..683a8f3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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| { diff --git a/src/App.vue b/src/App.vue index 9dec544..65e8a17 100644 --- a/src/App.vue +++ b/src/App.vue @@ -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; diff --git a/src/store/index.ts b/src/store/index.ts index 4eb3978..1b41e85 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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); + } };