init: GitHub/Gitea contribution heatmap apps

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-23 21:57:49 +08:00
commit 2d1c7fc59b
46 changed files with 17315 additions and 0 deletions

View File

@@ -0,0 +1,26 @@
[package]
name = "gitea-heatmap"
version = "1.0.0"
edition = "2021"
[lib]
name = "gitea_heatmap_lib"
crate-type = ["lib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
[profile.dev]
incremental = true
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"

3
gitea/src-tauri/build.rs Normal file
View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

View File

@@ -0,0 +1,23 @@
{
"identifier": "default",
"description": "Default capabilities for Gitea HeatMap widget",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-hide",
"core:window:allow-show",
"core:window:allow-set-focus",
"core:window:allow-unminimize",
"core:window:allow-set-always-on-top",
"core:window:allow-is-always-on-top",
"core:window:allow-set-position",
"core:window:allow-inner-position",
"core:window:allow-outer-position",
"core:window:allow-start-dragging",
"core:window:allow-set-size",
"core:window:allow-inner-size",
"core:event:allow-listen",
"core:event:allow-unlisten"
]
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"default":{"identifier":"default","description":"Default capabilities for Gitea HeatMap widget","local":true,"windows":["main"],"permissions":["core:default","core:window:allow-close","core:window:allow-hide","core:window:allow-show","core:window:allow-set-focus","core:window:allow-unminimize","core:window:allow-set-always-on-top","core:window:allow-is-always-on-top","core:window:allow-set-position","core:window:allow-inner-position","core:window:allow-outer-position","core:window:allow-start-dragging","core:window:allow-set-size","core:window:allow-inner-size","core:event:allow-listen","core:event:allow-unlisten"]}}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

221
gitea/src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,221 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Emitter, Manager,
};
const TRAY_TOOLTIP: &str = "Gitea 贡献热力图";
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Config {
pub server: String,
pub username: String,
pub token: String,
#[serde(default)]
pub window_x: Option<i32>,
#[serde(default)]
pub window_y: Option<i32>,
#[serde(default = "default_theme")]
pub theme: String,
}
fn default_theme() -> String {
"dark".to_string()
}
impl Default for Config {
fn default() -> Self {
Config {
server: "https://git.shumengya.top".to_string(),
username: "shumengya".to_string(),
token: String::new(),
window_x: None,
window_y: None,
theme: default_theme(),
}
}
}
fn data_dir() -> PathBuf {
std::env::current_exe()
.unwrap_or_default()
.parent()
.unwrap_or(std::path::Path::new("."))
.join("data")
}
#[tauri::command]
fn load_config() -> Config {
let path = data_dir().join("gitea_config.json");
fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default()
}
#[tauri::command]
fn save_config(config: Config) -> Result<(), String> {
let dir = data_dir();
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let content = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?;
fs::write(dir.join("gitea_config.json"), content).map_err(|e| e.to_string())
}
#[tauri::command]
fn save_window_pos(x: i32, y: i32) -> Result<(), String> {
let path = data_dir().join("gitea_config.json");
let mut config: Config = fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
config.window_x = Some(x);
config.window_y = Some(y);
let dir = data_dir();
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let content = serde_json::to_string_pretty(&config).map_err(|e| e.to_string())?;
fs::write(dir.join("gitea_config.json"), content).map_err(|e| e.to_string())
}
#[tauri::command]
async fn fetch_gitea_contributions(
server: String,
username: String,
token: String,
) -> Result<serde_json::Value, String> {
let url = format!(
"{}/api/v1/users/{}/heatmap",
server.trim_end_matches('/'),
username
);
let client = reqwest::Client::builder()
.user_agent("GitHeatMap-Widget/1.0")
.build()
.map_err(|e| e.to_string())?;
let mut req = client.get(&url).header("Accept", "application/json");
if !token.is_empty() {
req = req.header("Authorization", format!("token {}", token));
}
let resp = req.send().await.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!(
"HTTP {}: {}",
resp.status(),
resp.status().canonical_reason().unwrap_or("Unknown")
));
}
resp.json::<serde_json::Value>()
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
fn hide_app(window: tauri::Window) {
let _ = window.hide();
}
fn show_main_window(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
fn hide_main_window(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
fn setup_tray(app: &mut tauri::App) -> tauri::Result<()> {
let show_i = MenuItem::with_id(app, "show", "显示窗口", true, None::<&str>)?;
let refresh_i = MenuItem::with_id(app, "refresh", "刷新", true, None::<&str>)?;
let pin_i = MenuItem::with_id(app, "pin", "置顶", true, None::<&str>)?;
let theme_i = MenuItem::with_id(app, "theme", "切换主题", true, None::<&str>)?;
let settings_i = MenuItem::with_id(app, "settings", "设置", true, None::<&str>)?;
let hide_i = MenuItem::with_id(app, "hide", "隐藏窗口", true, None::<&str>)?;
let sep = PredefinedMenuItem::separator(app)?;
let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(
app,
&[
&show_i,
&refresh_i,
&pin_i,
&theme_i,
&settings_i,
&hide_i,
&sep,
&quit_i,
],
)?;
let icon = app
.default_window_icon()
.cloned()
.expect("default window icon missing");
TrayIconBuilder::with_id("main-tray")
.icon(icon)
.tooltip(TRAY_TOOLTIP)
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(|app, event| match event.id().as_ref() {
"show" => show_main_window(app),
"hide" => hide_main_window(app),
"quit" => app.exit(0),
"refresh" | "pin" | "theme" | "settings" => {
show_main_window(app);
if let Some(window) = app.get_webview_window("main") {
let _ = window.emit("tray-action", event.id().as_ref());
}
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
show_main_window(tray.app_handle());
}
})
.build(app)?;
Ok(())
}
pub fn run() {
tauri::Builder::default()
.setup(|app| {
setup_tray(app)?;
Ok(())
})
.on_window_event(|window, event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
api.prevent_close();
let _ = window.hide();
}
})
.invoke_handler(tauri::generate_handler![
load_config,
save_config,
save_window_pos,
fetch_gitea_contributions,
hide_app,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

View File

@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
gitea_heatmap_lib::run()
}

View File

@@ -0,0 +1,39 @@
{
"productName": "Gitea HeatMap",
"version": "1.0.0",
"identifier": "top.shumengya.gitea-heatmap",
"build": {
"frontendDist": "../frontend"
},
"app": {
"withGlobalTauri": true,
"windows": [
{
"label": "main",
"title": "Gitea 贡献热力图",
"width": 600,
"height": 108,
"resizable": false,
"decorations": false,
"transparent": true,
"alwaysOnTop": false,
"center": true,
"skipTaskbar": true
}
],
"security": {
"csp": null
}
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.png",
"icons/icon.ico"
]
}
}