mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-22 00:03:49 +01:00
feat(gui): add activity calendar to the homepage (#2160)
* feat(gui): add activity calendar to the homepage * localise week start
This commit is contained in:
parent
7984f9ef0c
commit
b8be23ee99
@ -174,6 +174,24 @@ impl HistoryDB {
|
||||
Ok(history)
|
||||
}
|
||||
|
||||
pub async fn calendar(&self) -> Result<Vec<(String, u64)>, String> {
|
||||
let query = "select count(1) as count, strftime('%F', datetime(timestamp / 1000000000, 'unixepoch')) as day from history where timestamp > ((unixepoch() - 31536000) * 1000000000) group by day;";
|
||||
|
||||
let calendar: Vec<(String, u64)> = sqlx::query(query)
|
||||
// safe to cast, count(x) is never < 0
|
||||
.map(|row: SqliteRow| {
|
||||
(
|
||||
row.get::<String, _>("day"),
|
||||
row.get::<i64, _>("count") as u64,
|
||||
)
|
||||
})
|
||||
.fetch_all(&self.0.pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(calendar)
|
||||
}
|
||||
|
||||
pub async fn global_stats(&self) -> Result<GlobalStats, String> {
|
||||
let day_ago = time::OffsetDateTime::now_utc() - time::Duration::days(1);
|
||||
let day_ago = day_ago.unix_timestamp_nanos();
|
||||
|
@ -167,6 +167,54 @@ async fn home_info() -> Result<HomeInfo, String> {
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
// Match the format that the frontend library we use expects
|
||||
// All the processing in Rust, not JS.
|
||||
// Faaaassssssst af ⚡️🦀
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct HistoryCalendarDay {
|
||||
pub date: String,
|
||||
pub count: u64,
|
||||
pub level: u8,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn history_calendar() -> Result<Vec<HistoryCalendarDay>, String> {
|
||||
let settings = Settings::new().map_err(|e| e.to_string())?;
|
||||
let db_path = PathBuf::from(settings.db_path.as_str());
|
||||
let db = HistoryDB::new(db_path, settings.local_timeout).await?;
|
||||
|
||||
let calendar = db.calendar().await?;
|
||||
|
||||
// probs don't want to iterate _this_ many times, but it's only the last year. so 365
|
||||
// iterations at max. should be quick.
|
||||
|
||||
let max = calendar
|
||||
.iter()
|
||||
.max_by_key(|d| d.1)
|
||||
.expect("Can't find max count");
|
||||
|
||||
let ret = calendar
|
||||
.iter()
|
||||
.map(|d| {
|
||||
// calculate the "level". we have 5, so figure out which 5th it fits into
|
||||
let percent: f64 = d.1 as f64 / max.1 as f64;
|
||||
let level = if d.1 == 0 {
|
||||
0.0
|
||||
} else {
|
||||
(percent / 0.2).round() + 1.0
|
||||
};
|
||||
|
||||
HistoryCalendarDay {
|
||||
date: d.0.clone(),
|
||||
count: d.1,
|
||||
level: std::cmp::min(4, level as u8),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
fn show_window(app: &AppHandle) {
|
||||
let windows = app.webview_windows();
|
||||
|
||||
@ -190,6 +238,7 @@ fn main() {
|
||||
session,
|
||||
login,
|
||||
register,
|
||||
history_calendar,
|
||||
install::install_cli,
|
||||
install::is_cli_installed,
|
||||
install::setup_cli,
|
||||
|
@ -32,8 +32,10 @@
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.3.1",
|
||||
"react-activity-calendar": "^2.2.10",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-tooltip": "^5.27.0",
|
||||
"react-window": "^1.8.10",
|
||||
"react-window-infinite-loader": "^1.0.9",
|
||||
"recharts": "^2.12.7",
|
||||
|
@ -71,12 +71,18 @@ dependencies:
|
||||
react:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1
|
||||
react-activity-calendar:
|
||||
specifier: ^2.2.10
|
||||
version: 2.2.10(react-dom@18.3.1)(react@18.3.1)
|
||||
react-dom:
|
||||
specifier: ^18.3.1
|
||||
version: 18.3.1(react@18.3.1)
|
||||
react-spinners:
|
||||
specifier: ^0.13.8
|
||||
version: 0.13.8(react-dom@18.3.1)(react@18.3.1)
|
||||
react-tooltip:
|
||||
specifier: ^5.27.0
|
||||
version: 5.27.0(react-dom@18.3.1)(react@18.3.1)
|
||||
react-window:
|
||||
specifier: ^1.8.10
|
||||
version: 1.8.10(react-dom@18.3.1)(react@18.3.1)
|
||||
@ -1669,6 +1675,10 @@ packages:
|
||||
'@babel/types': 7.24.7
|
||||
dev: true
|
||||
|
||||
/@types/chroma-js@2.4.4:
|
||||
resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==}
|
||||
dev: false
|
||||
|
||||
/@types/d3-array@3.2.1:
|
||||
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
|
||||
dev: false
|
||||
@ -1870,12 +1880,20 @@ packages:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
/chroma-js@2.4.2:
|
||||
resolution: {integrity: sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==}
|
||||
dev: false
|
||||
|
||||
/class-variance-authority@0.7.0:
|
||||
resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==}
|
||||
dependencies:
|
||||
clsx: 2.0.0
|
||||
dev: false
|
||||
|
||||
/classnames@2.5.1:
|
||||
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==}
|
||||
dev: false
|
||||
|
||||
/clsx@2.0.0:
|
||||
resolution: {integrity: sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==}
|
||||
engines: {node: '>=6'}
|
||||
@ -2500,6 +2518,19 @@ packages:
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
/react-activity-calendar@2.2.10(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-6UsPmw6jD5TM5DHAVCIKkOhqdcJ1reOrFsMd3pDnQ5Yo8WfkFoDLjYQRYUkH6BWhishVpZ3JTn39Tf+3JyVY6w==}
|
||||
peerDependencies:
|
||||
react: ^17.0.0 || ^18.0.0
|
||||
react-dom: ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
'@types/chroma-js': 2.4.4
|
||||
chroma-js: 2.4.2
|
||||
date-fns: 3.6.0
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/react-dom@18.3.1(react@18.3.1):
|
||||
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
|
||||
peerDependencies:
|
||||
@ -2594,6 +2625,18 @@ packages:
|
||||
tslib: 2.6.3
|
||||
dev: false
|
||||
|
||||
/react-tooltip@5.27.0(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-JXROcdfCEbCqkAkh8LyTSP3guQ0dG53iY2E2o4fw3D8clKzziMpE6QG6CclDaHELEKTzpMSeAOsdtg0ahoQosw==}
|
||||
peerDependencies:
|
||||
react: '>=16.14.0'
|
||||
react-dom: '>=16.14.0'
|
||||
dependencies:
|
||||
'@floating-ui/dom': 1.6.5
|
||||
classnames: 2.5.1
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/react-transition-group@4.4.5(react-dom@18.3.1)(react@18.3.1):
|
||||
resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
|
||||
peerDependencies:
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { formatRelative } from "date-fns";
|
||||
import { Tooltip as ReactTooltip } from "react-tooltip";
|
||||
|
||||
import { useStore } from "@/state/store";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import ActivityCalendar from "react-activity-calendar";
|
||||
|
||||
function Stats({ stats }: any) {
|
||||
return (
|
||||
<div>
|
||||
@ -44,16 +47,32 @@ function Header({ name }: any) {
|
||||
);
|
||||
}
|
||||
|
||||
const explicitTheme: ThemeInput = {
|
||||
light: ["#f0f0f0", "#c4edde", "#7ac7c4", "#f73859", "#384259"],
|
||||
dark: ["#383838", "#4D455D", "#7DB9B6", "#F5E9CF", "#E96479"],
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
const [weekStart, setWeekStart] = useState(0);
|
||||
|
||||
const homeInfo = useStore((state) => state.homeInfo);
|
||||
const user = useStore((state) => state.user);
|
||||
const calendar = useStore((state) => state.calendar);
|
||||
|
||||
const refreshHomeInfo = useStore((state) => state.refreshHomeInfo);
|
||||
const refreshUser = useStore((state) => state.refreshUser);
|
||||
const refreshCalendar = useStore((state) => state.refreshCalendar);
|
||||
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
let locale = new Intl.Locale(navigator.language);
|
||||
let weekinfo = locale.getWeekInfo();
|
||||
setWeekStart(weekinfo.firstDay);
|
||||
|
||||
refreshHomeInfo();
|
||||
refreshUser();
|
||||
refreshCalendar();
|
||||
|
||||
let setup = async () => {
|
||||
let installed = await invoke("is_cli_installed");
|
||||
@ -112,6 +131,24 @@ export default function Home() {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-10">
|
||||
<ActivityCalendar
|
||||
theme={explicitTheme}
|
||||
data={calendar}
|
||||
weekStart={weekStart}
|
||||
renderBlock={(block, activity) =>
|
||||
React.cloneElement(block, {
|
||||
"data-tooltip-id": "react-tooltip",
|
||||
"data-tooltip-html": `${activity.count} commands on ${activity.date}`,
|
||||
})
|
||||
}
|
||||
labels={{
|
||||
totalCount: "{{count}} history records in the last year",
|
||||
}}
|
||||
/>
|
||||
<ReactTooltip id="react-tooltip" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -25,8 +25,10 @@ interface AtuinState {
|
||||
aliases: Alias[];
|
||||
vars: Var[];
|
||||
shellHistory: ShellHistory[];
|
||||
calendar: any[];
|
||||
|
||||
refreshHomeInfo: () => void;
|
||||
refreshCalendar: () => void;
|
||||
refreshAliases: () => void;
|
||||
refreshVars: () => void;
|
||||
refreshUser: () => void;
|
||||
@ -40,6 +42,7 @@ export const useStore = create<AtuinState>()((set, get) => ({
|
||||
aliases: [],
|
||||
vars: [],
|
||||
shellHistory: [],
|
||||
calendar: [],
|
||||
|
||||
refreshAliases: () => {
|
||||
invoke("aliases").then((aliases: any) => {
|
||||
@ -47,6 +50,12 @@ export const useStore = create<AtuinState>()((set, get) => ({
|
||||
});
|
||||
},
|
||||
|
||||
refreshCalendar: () => {
|
||||
invoke("history_calendar").then((calendar: any) => {
|
||||
set({ calendar: calendar });
|
||||
});
|
||||
},
|
||||
|
||||
refreshVars: () => {
|
||||
invoke("vars").then((vars: any) => {
|
||||
set({ vars: vars });
|
||||
|
Loading…
Reference in New Issue
Block a user