Skip to content

Commit d25225c

Browse files
committed
feat: add browser name to domain stats and implement filtering by browser in UI
1 parent a37717d commit d25225c

8 files changed

Lines changed: 65 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
2222
### Changed
2323

2424
- **Widget refresh orchestration** — refresh now targets only the selected or event-affected widget, with a polling fallback when no event is available.
25+
- **Browser usage classification** — browser domain and hourly statistics now retain the browser name, so usage from Chrome, Edge, and other supported browsers is kept separate; the Browser Usage page also provides a browser filter.
2526
- **Permission revocation behavior** — revoked widget permissions now clear active subscriptions and emit a `widget-permission-revoked` event so external widgets can enter a degraded state.
2627
- **Skin asset handling** — selected images are validated and copied into the managed application `skins` directory instead of retaining arbitrary source paths.
2728
- **Third-party widget recovery** — failed widgets now show recovery guidance and a real remount/retry action.
@@ -30,6 +31,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
3031

3132
### Fixed
3233

34+
- **Dashboard refresh flicker** — background statistics refreshes no longer insert a loading card after the initial load, preventing the main dashboard from visibly jumping during periodic updates.
3335
- **Main app skin visibility** — fixed opaque layout backgrounds covering the configured app background image.
3436
- **Skin reset appearance** — clearing a background image now removes its overlay and restores the original theme instead of leaving a gray veil.
3537
- **Managed skin URL resolution** — fixed imported skin paths so Asset Protocol can resolve copied files in the application data directory.
@@ -48,7 +50,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4850

4951
### Tests
5052

51-
- Frontend tests: **69 passed**.
53+
- Frontend tests: **71 passed**.
5254
- TypeScript typecheck: passed.
5355
- ESLint: 0 errors; 8 existing warnings remain.
5456
- Rust `cargo check`: passed.

src-tauri/src/db/mod.rs

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1654,7 +1654,8 @@ pub fn get_browser_domain_stats(
16541654
end_date: &str,
16551655
) -> Result<Vec<crate::models::BrowserDomainStats>> {
16561656
let mut stmt = conn.prepare(
1657-
"SELECT host,
1657+
"SELECT browser_name,
1658+
host,
16581659
SUM(duration_seconds) as total_seconds,
16591660
COUNT(1) as visit_count,
16601661
MAX(ended_at) as last_visited_at
@@ -1663,15 +1664,16 @@ pub fn get_browser_domain_stats(
16631664
AND date(ended_at, 'localtime') <= ?2
16641665
AND host NOT IN (SELECT host FROM browser_ignored_domains)
16651666
AND host != ''
1666-
GROUP BY host
1667+
GROUP BY browser_name, host
16671668
ORDER BY total_seconds DESC",
16681669
)?;
16691670
let rows = stmt.query_map(params![start_date, end_date], |row| {
16701671
Ok(crate::models::BrowserDomainStats {
1671-
host: row.get(0)?,
1672-
total_seconds: row.get(1)?,
1673-
visit_count: row.get(2)?,
1674-
last_visited_at: row.get(3)?,
1672+
browser_name: row.get(0)?,
1673+
host: row.get(1)?,
1674+
total_seconds: row.get(2)?,
1675+
visit_count: row.get(3)?,
1676+
last_visited_at: row.get(4)?,
16751677
})
16761678
})?;
16771679
rows.collect()
@@ -1688,7 +1690,8 @@ pub fn get_browser_domain_stats_for_hour(
16881690
let hour_text = format!("{:02}", normalized_hour);
16891691

16901692
let mut stmt = conn.prepare(
1691-
"SELECT host,
1693+
"SELECT browser_name,
1694+
host,
16921695
SUM(duration_seconds) as total_seconds,
16931696
COUNT(1) as visit_count,
16941697
MAX(ended_at) as last_visited_at
@@ -1697,16 +1700,17 @@ pub fn get_browser_domain_stats_for_hour(
16971700
AND strftime('%H', ended_at, 'localtime') = ?2
16981701
AND host NOT IN (SELECT host FROM browser_ignored_domains)
16991702
AND host != ''
1700-
GROUP BY host
1703+
GROUP BY browser_name, host
17011704
ORDER BY total_seconds DESC
17021705
LIMIT ?3",
17031706
)?;
17041707
let rows = stmt.query_map(params![date, hour_text, safe_limit], |row| {
17051708
Ok(crate::models::BrowserHourDomainStats {
1706-
host: row.get(0)?,
1707-
total_seconds: row.get(1)?,
1708-
visit_count: row.get(2)?,
1709-
last_visited_at: row.get(3)?,
1709+
browser_name: row.get(0)?,
1710+
host: row.get(1)?,
1711+
total_seconds: row.get(2)?,
1712+
visit_count: row.get(3)?,
1713+
last_visited_at: row.get(4)?,
17101714
})
17111715
})?;
17121716
rows.collect()

src-tauri/src/models/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ impl Default for WidgetConfig {
294294

295295
#[derive(Debug, Serialize, Deserialize, Clone)]
296296
pub struct BrowserDomainStats {
297+
pub browser_name: String,
297298
pub host: String,
298299
pub total_seconds: i64,
299300
pub visit_count: i64,
@@ -302,6 +303,7 @@ pub struct BrowserDomainStats {
302303

303304
#[derive(Debug, Serialize, Deserialize, Clone)]
304305
pub struct BrowserHourDomainStats {
306+
pub browser_name: String,
305307
pub host: String,
306308
pub total_seconds: i64,
307309
pub visit_count: i64,

src/i18n/locales/en/browserUsage.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"deleteView": "Delete view",
2020
"rangeHint": "Current range: {{startDate}} to {{endDate}}",
2121
"searchDomain": "Search domain...",
22+
"filterBrowser": "Filter browser",
23+
"allBrowsers": "All browsers",
24+
"unknownBrowser": "Unknown browser",
2225
"domain": "Domain",
2326
"timeSpent": "Time Spent",
2427
"visits": "Visits",

src/i18n/locales/zh-CN/browserUsage.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"deleteView": "删除视图",
2020
"rangeHint": "当前范围:{{startDate}} 至 {{endDate}}",
2121
"searchDomain": "搜索域名...",
22+
"filterBrowser": "筛选浏览器",
23+
"allBrowsers": "全部浏览器",
24+
"unknownBrowser": "未知浏览器",
2225
"domain": "域名",
2326
"timeSpent": "使用时长",
2427
"visits": "访问次数",

src/i18n/locales/zh-TW/browserUsage.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
"deleteView": "刪除視圖",
2020
"rangeHint": "目前範圍:{{startDate}} 至 {{endDate}}",
2121
"searchDomain": "搜尋網域...",
22+
"filterBrowser": "篩選瀏覽器",
23+
"allBrowsers": "所有瀏覽器",
24+
"unknownBrowser": "未知瀏覽器",
2225
"domain": "網域",
2326
"timeSpent": "使用時間",
2427
"visits": "造訪次數",

src/pages/BrowserUsage/index.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ function DomainRow({
137137
<div className="flex-1 min-w-0">
138138
<div className="flex items-center gap-2 flex-wrap">
139139
<span className="text-sm font-medium text-text-primary truncate">{stat.host}</span>
140+
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-accent-blue/10 text-accent-blue">
141+
{stat.browser_name || t("browserUsage:unknownBrowser")}
142+
</span>
140143
{isIgnored && (
141144
<span className="text-[10px] px-1.5 py-0.5 rounded-full bg-surface-hover text-text-muted">
142145
{t("browserUsage:ignored")}
@@ -241,6 +244,7 @@ export default function BrowserUsage() {
241244
}
242245
});
243246
const [search, setSearch] = useState("");
247+
const [browserFilter, setBrowserFilter] = useState("all");
244248
const [showIgnored, setShowIgnored] = useState(false);
245249

246250
const [stats, setStats] = useState<BrowserDomainStats[]>([]);
@@ -349,15 +353,29 @@ export default function BrowserUsage() {
349353

350354
const ignoredSet = useMemo(() => new Set(ignoredDomains), [ignoredDomains]);
351355

356+
const browserOptions = useMemo(() => {
357+
const names = stats
358+
.map((stat) => stat.browser_name || t("browserUsage:unknownBrowser"))
359+
.filter((name, index, all) => all.indexOf(name) === index)
360+
.sort((a, b) => a.localeCompare(b));
361+
return names;
362+
}, [stats, t]);
363+
352364
// Active stats = not ignored (or show all when showIgnored = true)
353365
const filteredStats = useMemo(() => {
354366
let rows = showIgnored ? stats : stats.filter((s) => !ignoredSet.has(s.host));
355367
if (search.trim()) {
356368
const q = search.toLowerCase();
357-
rows = rows.filter((s) => s.host.toLowerCase().includes(q));
369+
rows = rows.filter((s) =>
370+
s.host.toLowerCase().includes(q)
371+
|| (s.browser_name || t("browserUsage:unknownBrowser")).toLowerCase().includes(q)
372+
);
373+
}
374+
if (browserFilter !== "all") {
375+
rows = rows.filter((s) => (s.browser_name || t("browserUsage:unknownBrowser")) === browserFilter);
358376
}
359377
return rows;
360-
}, [stats, ignoredSet, showIgnored, search]);
378+
}, [stats, ignoredSet, showIgnored, search, browserFilter, t]);
361379

362380
const ignoredStats = useMemo(() =>
363381
stats.filter((s) => ignoredSet.has(s.host)),
@@ -731,6 +749,18 @@ export default function BrowserUsage() {
731749
className="w-full pl-8 pr-3 py-2 bg-surface-hover border border-surface-border rounded-xl text-sm text-text-primary placeholder:text-text-muted outline-none focus:border-accent-blue transition-colors"
732750
/>
733751
</div>
752+
<select
753+
value={browserFilter}
754+
onChange={(e) => setBrowserFilter(e.target.value)}
755+
className="ui-select min-w-36 !py-2 !text-sm"
756+
title={t("browserUsage:filterBrowser")}
757+
aria-label={t("browserUsage:filterBrowser")}
758+
>
759+
<option value="all">{t("browserUsage:allBrowsers")}</option>
760+
{browserOptions.map((browser) => (
761+
<option key={browser} value={browser}>{browser}</option>
762+
))}
763+
</select>
734764
{ignoredStats.length > 0 && (
735765
<button
736766
onClick={() => setShowIgnored((v) => !v)}
@@ -768,7 +798,7 @@ export default function BrowserUsage() {
768798
<div className="glass-card divide-y divide-surface-border">
769799
{filteredStats.map((stat) => (
770800
<DomainRow
771-
key={stat.host}
801+
key={`${stat.browser_name || "unknown"}:${stat.host}`}
772802
stat={stat}
773803
limit={limitsMap.get(stat.host)}
774804
isIgnored={ignoredSet.has(stat.host)}

src/types/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,7 @@ export interface BrowserExtensionStatus {
410410
}
411411

412412
export interface BrowserHourDomainStats {
413+
browser_name: string;
413414
host: string;
414415
total_seconds: number;
415416
visit_count: number;
@@ -575,6 +576,7 @@ export interface LimitToast {
575576
}
576577

577578
export interface BrowserDomainStats {
579+
browser_name: string;
578580
host: string;
579581
total_seconds: number;
580582
visit_count: number;

0 commit comments

Comments
 (0)