Skip to content

Commit 669585a

Browse files
PythonSmall-QCopilot
andcommitted
feat: add app limits feature with daily usage limits and notifications
- Implemented a new Limits page to set daily usage limits for applications. - Added functionality to search and select running or recent applications. - Users can add, remove, and toggle limits for applications. - Integrated local storage to persist limits across sessions. - Added translations for limits feature in English and Chinese. - Updated settings to include options for excluding TimeLens applications. - Enhanced settings page with new UI elements for managing executable options. - Updated API services to handle recent and running executables. - Improved overall user experience with new UI components and styles. Co-authored-by: Copilot <copilot@github.com>
1 parent 51fea2f commit 669585a

34 files changed

Lines changed: 1594 additions & 110 deletions

CHANGELOG.md

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,77 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
55

66
---
77

8-
## [0.1.0][0.1.0]
8+
## [0.3.0] - 2026-04-25
9+
10+
### Added
11+
12+
#### Dashboard — Period Stats
13+
14+
- **Day / Week / Month selector** — switch between daily, weekly, and monthly views from the dashboard header
15+
- **Week navigator**`<select>` dropdown listing every week of the year; prev / next arrow buttons
16+
- **Month navigator**`<select>` dropdown listing every month; prev / next arrow buttons
17+
- **Average daily usage** — displayed per period (weekly / monthly view) showing mean screen time per day
18+
- **Week-over-week comparison** — shows total usage change vs. the previous week (absolute + percentage delta, color-coded)
19+
- **Week date range labels** — week dropdown now shows `W17: Apr 21 – Apr 27` instead of the raw ISO code
20+
- **First day of week** — configurable in Settings (Monday or Sunday); affects the week date range display and data window
21+
22+
#### Settings — Excluded Apps
23+
24+
- New **Excluded Apps** section under Data settings
25+
- Searchable app list populated from currently running processes (Windows: `tasklist`) merged with recent historical executables
26+
- Checkbox-based multi-select; changes saved via a dedicated Save button
27+
- Excluded apps are hidden from all stats queries (dashboard, charts, rankings)
28+
- **Default exclude TimeLens** — on first run, TimeLens's own executable is auto-added to the exclusion list; a toggle in Settings can re-include it
29+
30+
#### Widgets
31+
32+
- **Start on launch** toggle per widget — each widget card in Widget Center now has a checkbox; widgets with this disabled are not restored at startup
33+
34+
#### UI / UX
35+
36+
- **Dropdown beautification** — unified `.ui-select` style with custom SVG chevron, 12 px rounded corners, and theme-aware option backgrounds
37+
- **Dropdown beautification (enhanced)** — 14 px rounded corners, subtle box-shadow, smooth hover/active scale transitions
38+
- **App exe path tooltip** — hovering over an app name in the ranked list shows the full executable path
39+
40+
#### App Usage Limits (new page)
41+
42+
- New **App Limits** sidebar page (`/limits`) for per-app daily usage warnings
43+
- Searchable app picker; per-limit enable/disable toggle and inline hour/minute editing
44+
- **80 % warning** — dismissable floating banner toast when 80 % of daily limit is reached
45+
- **90 % warning** — orange banner toast when 90 % of limit is reached
46+
- **100 % alert** — blocking modal popup when daily limit is fully consumed
47+
- Warning state stored in localStorage, reset daily (fires once per threshold per day)
48+
49+
#### Update Check
50+
51+
- On startup (4 s delay) TimeLens fetches the latest GitHub release
52+
- If a newer version is found, an **Update Available** modal shows release notes and a direct GitHub link
53+
- Users can dismiss or choose "Remind me later"
54+
55+
#### Internationalization
56+
57+
- **System language auto-detect** — on first launch, TimeLens automatically selects `zh-CN` if the OS locale is Chinese, otherwise defaults to `en`; preference is saved to localStorage
58+
- Language switcher removed from the Sidebar; language is now changed only from Settings
59+
- New **limits** i18n namespace (`en` / `zh-CN`) covering the App Limits page
60+
- `common` namespace extended with update-check message keys
61+
- `settings` namespace extended with week-start-day and exclude-TimeLens keys
62+
63+
### Fixed
64+
65+
- **Startup race condition**`DbState` is now registered with Tauri's state manager *before* widget windows are restored, preventing early IPC calls from failing with a missing-state error
66+
- Two Rust compile errors introduced by `exe_path` tracking: missing tuple element in `monitor/mod.rs` and missing `start_on_launch` field in `WidgetConfig` initializer
67+
68+
### Changed
69+
70+
- `exe_path` column added to `app_usage` table; existing databases are migrated automatically via `ALTER TABLE`
71+
- `start_on_launch` column added to `widget_configs` table; existing databases are migrated automatically
72+
- New `ignored_apps` table created on first run
73+
- All stat queries now filter out rows whose `exe_path` is in the ignored-apps list
74+
- Version bumped to **0.3.0** in `package.json`, `Cargo.toml`, and `tauri.conf.json`
75+
76+
---
77+
78+
## [0.1.0]
979

1080
### Added
1181

@@ -44,4 +114,19 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
44114
- GitHub Actions CI (lint + typecheck + `cargo check`) on push/PR
45115
- GitHub Actions release workflow: builds Windows `.msi`/`.exe` + macOS universal `.dmg` on `v*` tags
46116

47-
[0.1.0]: https://github.com/PythonSmall-Q/TimeLens/releases/tag/v0.1.0
117+
---
118+
119+
#### Future Development
120+
121+
122+
| 方向 | 具体想法 |
123+
| -------------------- | --------------------------------------------------------------- |
124+
| **数据洞察** | 生产力分数、专注时段识别、与历史均值的对比趋势图 |
125+
| **目标系统** | 每日/每周使用目标(不只是限制,还有"至少用 X 小时"鼓励类) |
126+
| **分类管理** | 将 App 手动分入"工作/娱乐/社交"类别,按类别统计 |
127+
| **报告导出** | 周报/月报 PDF/CSV,可分享 |
128+
| **通知集成** | Windows 原生通知(目前用内嵌 toast),支持声音提醒 |
129+
| **跨设备同步** | 可选云同步(S3/本地 NAS),多机汇总统计 |
130+
| **专注模式** | 限额触发时可选择屏蔽某应用窗口(需管理员权限) |
131+
| **API / 插件** | 暴露本地 REST API,让第三方工具(Obsidian、Raycast 等)读取数据 |
132+
| **移动端伴侣** | 查看统计的手机端 App(只读),Tauri 2.x 支持 Android/iOS |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "timelens",
33
"private": true,
4-
"version": "1.0.0",
4+
"version": "0.3.0",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "timelens"
3-
version = "0.1.0"
3+
version = "0.3.0"
44
description = "A screen time tracker and desktop widget manager"
55
authors = []
66
edition = "2021"
@@ -13,6 +13,10 @@ crate-type = ["staticlib", "cdylib", "rlib"]
1313
[build-dependencies]
1414
tauri-build = { version = "2", features = [] }
1515

16+
[features]
17+
default = []
18+
macos-private-api = ["tauri/macos-private-api"]
19+
1620
[dependencies]
1721
tauri = { version = "2", features = ["tray-icon", "image-png", "image-ico"] }
1822
tauri-plugin-shell = "2"

src-tauri/src/commands/storage_cmd.rs

Lines changed: 162 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ use chrono::Local;
33
use tauri::State;
44

55
use crate::db;
6-
use crate::models::{AppUsageSummary, HourlyDistribution, DailyUsage, TodoItem, WidgetConfig};
6+
use crate::models::{
7+
AppUsageComparison, AppUsageSummary, DailyUsage, ExecutableOption, HourlyDistribution,
8+
TodoItem, WidgetConfig,
9+
};
710

811
pub type DbState = Arc<Mutex<rusqlite::Connection>>;
912

@@ -20,7 +23,11 @@ pub fn get_today_app_totals(db: State<DbState>) -> Result<Vec<AppUsageSummary>,
2023
let rows = db::get_daily_app_totals(&conn, &today()).map_err(|e| e.to_string())?;
2124
Ok(rows
2225
.into_iter()
23-
.map(|(app_name, total_seconds)| AppUsageSummary { app_name, total_seconds })
26+
.map(|(app_name, exe_path, total_seconds)| AppUsageSummary {
27+
app_name,
28+
exe_path,
29+
total_seconds,
30+
})
2431
.collect())
2532
}
2633

@@ -31,10 +38,89 @@ pub fn get_app_totals_for_date(date: String, db: State<DbState>) -> Result<Vec<A
3138
let rows = db::get_daily_app_totals(&conn, &date).map_err(|e| e.to_string())?;
3239
Ok(rows
3340
.into_iter()
34-
.map(|(app_name, total_seconds)| AppUsageSummary { app_name, total_seconds })
41+
.map(|(app_name, exe_path, total_seconds)| AppUsageSummary {
42+
app_name,
43+
exe_path,
44+
total_seconds,
45+
})
3546
.collect())
3647
}
3748

49+
#[tauri::command]
50+
pub fn get_app_totals_in_range(
51+
start_date: String,
52+
end_date: String,
53+
db: State<DbState>,
54+
) -> Result<Vec<AppUsageSummary>, String> {
55+
let conn = db.lock().map_err(|e| e.to_string())?;
56+
let rows = db::get_app_totals_in_range(&conn, &start_date, &end_date).map_err(|e| e.to_string())?;
57+
Ok(rows
58+
.into_iter()
59+
.map(|(app_name, exe_path, total_seconds)| AppUsageSummary {
60+
app_name,
61+
exe_path,
62+
total_seconds,
63+
})
64+
.collect())
65+
}
66+
67+
#[tauri::command]
68+
pub fn get_app_comparison_in_ranges(
69+
current_start: String,
70+
current_end: String,
71+
previous_start: String,
72+
previous_end: String,
73+
db: State<DbState>,
74+
) -> Result<Vec<AppUsageComparison>, String> {
75+
let conn = db.lock().map_err(|e| e.to_string())?;
76+
let current = db::get_app_totals_in_range(&conn, &current_start, &current_end)
77+
.map_err(|e| e.to_string())?;
78+
let previous = db::get_app_totals_in_range(&conn, &previous_start, &previous_end)
79+
.map_err(|e| e.to_string())?;
80+
81+
let mut map: std::collections::BTreeMap<String, (String, i64, i64)> =
82+
std::collections::BTreeMap::new();
83+
for (name, exe_path, secs) in current {
84+
map.insert(name, (exe_path, secs, 0));
85+
}
86+
for (name, exe_path, secs) in previous {
87+
if let Some(v) = map.get_mut(&name) {
88+
v.2 = secs;
89+
if v.0.is_empty() {
90+
v.0 = exe_path;
91+
}
92+
} else {
93+
map.insert(name, (exe_path, 0, secs));
94+
}
95+
}
96+
97+
let mut rows: Vec<AppUsageComparison> = map
98+
.into_iter()
99+
.map(|(app_name, (exe_path, current_seconds, previous_seconds))| {
100+
let delta_seconds = current_seconds - previous_seconds;
101+
let delta_ratio = if previous_seconds > 0 {
102+
delta_seconds as f64 / previous_seconds as f64
103+
} else if current_seconds > 0 {
104+
1.0
105+
} else {
106+
0.0
107+
};
108+
109+
AppUsageComparison {
110+
app_name,
111+
exe_path,
112+
current_seconds,
113+
previous_seconds,
114+
delta_seconds,
115+
delta_ratio,
116+
}
117+
})
118+
.collect();
119+
120+
rows.sort_by(|a, b| b.current_seconds.cmp(&a.current_seconds));
121+
Ok(rows)
122+
}
123+
38124
/// Returns hourly usage distribution for today (hour 0-23 → seconds).
39125
#[tauri::command]
40126
pub fn get_today_hourly(db: State<DbState>) -> Result<Vec<HourlyDistribution>, String> {
@@ -126,3 +212,76 @@ pub fn remove_widget_config(id: String, db: State<DbState>) -> Result<(), String
126212
let conn = db.lock().map_err(|e| e.to_string())?;
127213
db::delete_widget_config(&conn, &id).map_err(|e| e.to_string())
128214
}
215+
216+
#[tauri::command]
217+
pub fn get_ignored_apps(db: State<DbState>) -> Result<Vec<String>, String> {
218+
let conn = db.lock().map_err(|e| e.to_string())?;
219+
db::get_ignored_apps(&conn).map_err(|e| e.to_string())
220+
}
221+
222+
#[tauri::command]
223+
pub fn set_ignored_apps(exe_paths: Vec<String>, db: State<DbState>) -> Result<(), String> {
224+
let conn = db.lock().map_err(|e| e.to_string())?;
225+
db::set_ignored_apps(&conn, &exe_paths).map_err(|e| e.to_string())
226+
}
227+
228+
#[tauri::command]
229+
pub fn get_recent_executables(limit: Option<u32>, db: State<DbState>) -> Result<Vec<ExecutableOption>, String> {
230+
let conn = db.lock().map_err(|e| e.to_string())?;
231+
let rows = db::get_recent_executables(&conn, limit.unwrap_or(200) as i64)
232+
.map_err(|e| e.to_string())?;
233+
Ok(rows
234+
.into_iter()
235+
.map(|(app_name, exe_path)| ExecutableOption { app_name, exe_path })
236+
.collect())
237+
}
238+
239+
#[tauri::command]
240+
pub fn get_running_executables() -> Result<Vec<ExecutableOption>, String> {
241+
#[cfg(target_os = "windows")]
242+
{
243+
use std::collections::BTreeSet;
244+
use std::process::Command;
245+
246+
let output = Command::new("tasklist")
247+
.args(["/fo", "csv", "/nh"])
248+
.output()
249+
.map_err(|e| e.to_string())?;
250+
251+
if !output.status.success() {
252+
return Ok(vec![]);
253+
}
254+
255+
let text = String::from_utf8_lossy(&output.stdout);
256+
let mut set = BTreeSet::new();
257+
for line in text.lines() {
258+
let line = line.trim();
259+
if line.is_empty() {
260+
continue;
261+
}
262+
let first = line
263+
.trim_matches('"')
264+
.split("\",\"")
265+
.next()
266+
.unwrap_or("")
267+
.trim();
268+
if first.is_empty() {
269+
continue;
270+
}
271+
set.insert(first.to_string());
272+
}
273+
274+
return Ok(set
275+
.into_iter()
276+
.map(|exe| ExecutableOption {
277+
app_name: exe.clone(),
278+
exe_path: exe,
279+
})
280+
.collect());
281+
}
282+
283+
#[cfg(not(target_os = "windows"))]
284+
{
285+
Ok(vec![])
286+
}
287+
}

src-tauri/src/commands/widget_cmd.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub async fn create_widget(
3737
opacity: 0.88,
3838
always_on_top_mode: "focus".to_string(),
3939
pinned: false,
40+
start_on_launch: true,
4041
};
4142

4243
build_widget_window(&app, &config)?;

0 commit comments

Comments
 (0)