init: GitHub/Gitea contribution heatmap apps
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
89
github/frontend/index.html
Normal file
89
github/frontend/index.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GitHub 贡献热力图</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="widget">
|
||||
<!-- Header -->
|
||||
<div class="header" data-tauri-drag-region>
|
||||
<div class="title-area" data-tauri-drag-region>
|
||||
<svg class="platform-icon" viewBox="0 0 24 24" fill="currentColor" data-tauri-drag-region>
|
||||
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||
</svg>
|
||||
<span class="username" id="username-display">shumengya</span>
|
||||
<span class="separator">•</span>
|
||||
<span class="stats" id="stats-display">加载中...</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="legend">
|
||||
<span class="legend-label">少</span>
|
||||
<div class="legend-cell" data-level="0"></div>
|
||||
<div class="legend-cell" data-level="1"></div>
|
||||
<div class="legend-cell" data-level="2"></div>
|
||||
<div class="legend-cell" data-level="3"></div>
|
||||
<div class="legend-cell" data-level="4"></div>
|
||||
<span class="legend-label">多</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Heatmap -->
|
||||
<div class="heatmap-area" id="heatmap-area">
|
||||
<div class="status-msg" id="status-loading">
|
||||
<div class="spinner"></div>
|
||||
<span>正在获取贡献数据...</span>
|
||||
</div>
|
||||
<div class="status-msg error" id="status-error" style="display:none">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
<span id="error-text">加载失败</span>
|
||||
</div>
|
||||
<canvas id="heatmap-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings Modal -->
|
||||
<div class="modal-overlay" id="settings-modal">
|
||||
<div class="modal" id="settings-panel">
|
||||
<div class="modal-title">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>
|
||||
GitHub 设置
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>主题</label>
|
||||
<select id="input-theme">
|
||||
<option value="dark">深色</option>
|
||||
<option value="light">浅色</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>GitHub 用户名</label>
|
||||
<input type="text" id="input-username" placeholder="例如: shumengya" autocomplete="off">
|
||||
<span class="form-error" id="form-error">请填写 GitHub 用户名</span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Personal Access Token</label>
|
||||
<input type="password" id="input-token" placeholder="ghp_xxxxxxxxxxxxxxxxxxxx" autocomplete="off">
|
||||
<span class="form-hint">需要 read:user 权限 · Settings → Developer settings → Tokens</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn" id="btn-cancel">取消</button>
|
||||
<button class="btn primary" id="btn-save">保存并刷新</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tooltip -->
|
||||
<div class="tooltip" id="tooltip"></div>
|
||||
|
||||
<script type="module" src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
391
github/frontend/main.js
Normal file
391
github/frontend/main.js
Normal file
@@ -0,0 +1,391 @@
|
||||
const { invoke } = window.__TAURI__.core;
|
||||
const { getCurrentWindow } = window.__TAURI__.window;
|
||||
|
||||
const GAP = 1;
|
||||
const WINDOW_WIDTH = 600;
|
||||
const LAYOUT_WEEKS = 53;
|
||||
const SETTINGS_HEIGHT = 350;
|
||||
|
||||
let config = { username: 'shumengya', token: '', window_x: null, window_y: null, theme: 'dark' };
|
||||
let isPinned = false;
|
||||
let moveTimer = null;
|
||||
let cellHitMap = [];
|
||||
let lastHeatmapData = null;
|
||||
|
||||
function getHeatmapColors() {
|
||||
const style = getComputedStyle(document.body);
|
||||
return [0, 1, 2, 3, 4].map((level) => style.getPropertyValue(`--hm-${level}`).trim());
|
||||
}
|
||||
|
||||
function computeHeatmapLayout() {
|
||||
const area = document.getElementById('heatmap-area');
|
||||
const widget = document.querySelector('.widget');
|
||||
const ws = widget ? getComputedStyle(widget) : null;
|
||||
const padX = ws ? parseFloat(ws.paddingLeft) + parseFloat(ws.paddingRight) : 10;
|
||||
const availW = (area && area.clientWidth > 0) ? area.clientWidth : (WINDOW_WIDTH - padX);
|
||||
const LEFT = 0;
|
||||
const TOP = 0;
|
||||
const gridW = availW;
|
||||
|
||||
let step = Math.floor((gridW + GAP) / LAYOUT_WEEKS);
|
||||
if (step < GAP + 2) step = GAP + 2;
|
||||
|
||||
const cell = step - GAP;
|
||||
const cssW = availW;
|
||||
const cssH = 7 * step - GAP;
|
||||
|
||||
return {
|
||||
LEFT,
|
||||
TOP,
|
||||
STEP: step,
|
||||
CELL: cell,
|
||||
cssW,
|
||||
cssH,
|
||||
radius: Math.max(1, Math.round(cell * 0.22)),
|
||||
};
|
||||
}
|
||||
|
||||
function computeWidgetHeight() {
|
||||
const { cssH } = computeHeatmapLayout();
|
||||
const header = document.querySelector('.header');
|
||||
const widget = document.querySelector('.widget');
|
||||
const ws = widget ? getComputedStyle(widget) : null;
|
||||
const padY = ws ? parseFloat(ws.paddingTop) + parseFloat(ws.paddingBottom) : 7;
|
||||
const borderY = ws ? parseFloat(ws.borderTopWidth) + parseFloat(ws.borderBottomWidth) : 2;
|
||||
const headerH = header ? header.offsetHeight : 20;
|
||||
return Math.ceil(borderY + padY + headerH + cssH);
|
||||
}
|
||||
|
||||
async function ensureWidgetSize() {
|
||||
await setWindowHeight(computeWidgetHeight());
|
||||
}
|
||||
|
||||
function applyTheme(theme, rerender = true) {
|
||||
config.theme = theme === 'light' ? 'light' : 'dark';
|
||||
document.body.classList.toggle('theme-light', config.theme === 'light');
|
||||
const select = document.getElementById('input-theme');
|
||||
if (select) select.value = config.theme;
|
||||
if (rerender && lastHeatmapData) {
|
||||
renderHeatmap(lastHeatmapData);
|
||||
}
|
||||
}
|
||||
|
||||
async function persistTheme() {
|
||||
try {
|
||||
await invoke('save_config', { config });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function toggleTheme() {
|
||||
applyTheme(config.theme === 'light' ? 'dark' : 'light');
|
||||
await persistTheme();
|
||||
}
|
||||
|
||||
function levelOf(n) {
|
||||
if (n === 0) return 0;
|
||||
if (n <= 3) return 1;
|
||||
if (n <= 8) return 2;
|
||||
if (n <= 15) return 3;
|
||||
return 4;
|
||||
}
|
||||
|
||||
function localDateStr(date) {
|
||||
const y = date.getFullYear();
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(date.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
document.getElementById('status-loading').style.display = 'flex';
|
||||
document.getElementById('status-error').style.display = 'none';
|
||||
document.getElementById('heatmap-canvas').style.display = 'none';
|
||||
document.getElementById('stats-display').textContent = '加载中...';
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
document.getElementById('status-loading').style.display = 'none';
|
||||
document.getElementById('status-error').style.display = 'flex';
|
||||
document.getElementById('error-text').textContent = msg;
|
||||
document.getElementById('heatmap-canvas').style.display = 'none';
|
||||
document.getElementById('stats-display').textContent = '加载失败';
|
||||
}
|
||||
|
||||
function renderHeatmap(calendar) {
|
||||
lastHeatmapData = calendar;
|
||||
document.getElementById('status-loading').style.display = 'none';
|
||||
document.getElementById('status-error').style.display = 'none';
|
||||
|
||||
const total = calendar.totalContributions;
|
||||
document.getElementById('stats-display').textContent =
|
||||
`最近一年贡献 ${total.toLocaleString()} 次`;
|
||||
|
||||
const canvas = document.getElementById('heatmap-canvas');
|
||||
canvas.style.display = 'block';
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const weeks = calendar.weeks.slice(0, LAYOUT_WEEKS);
|
||||
const { LEFT, TOP, STEP, CELL, cssW, cssH, radius } = computeHeatmapLayout();
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
|
||||
canvas.style.width = `${cssW}px`;
|
||||
canvas.style.height = `${cssH}px`;
|
||||
document.getElementById('heatmap-area').style.minHeight = `${cssH}px`;
|
||||
canvas.width = Math.round(cssW * dpr);
|
||||
canvas.height = Math.round(cssH * dpr);
|
||||
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.clearRect(0, 0, cssW, cssH);
|
||||
|
||||
// Draw cells and build hit map
|
||||
cellHitMap = [];
|
||||
const colors = getHeatmapColors();
|
||||
weeks.forEach((week, wi) => {
|
||||
week.contributionDays.forEach((day) => {
|
||||
const x = LEFT + wi * STEP;
|
||||
const y = TOP + day.weekday * STEP;
|
||||
const color = colors[levelOf(day.contributionCount)];
|
||||
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, CELL, CELL, radius);
|
||||
ctx.fill();
|
||||
|
||||
cellHitMap.push({ x, y, date: day.date, count: day.contributionCount });
|
||||
});
|
||||
});
|
||||
|
||||
// Tooltip
|
||||
const tooltip = document.getElementById('tooltip');
|
||||
canvas.onmousemove = (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const mx = e.clientX - rect.left;
|
||||
const my = e.clientY - rect.top;
|
||||
const hit = cellHitMap.find(c => mx >= c.x && mx < c.x + CELL && my >= c.y && my < c.y + CELL);
|
||||
if (hit) {
|
||||
const dateObj = new Date(hit.date + 'T12:00:00');
|
||||
const fmt = dateObj.toLocaleDateString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', weekday: 'short' });
|
||||
tooltip.textContent = hit.count === 0 ? `${fmt} · 无贡献` : `${fmt} · ${hit.count} 次贡献`;
|
||||
tooltip.style.display = 'block';
|
||||
tooltip.style.left = `${e.clientX + 12}px`;
|
||||
tooltip.style.top = `${e.clientY - 34}px`;
|
||||
} else {
|
||||
tooltip.style.display = 'none';
|
||||
}
|
||||
};
|
||||
canvas.onmouseleave = () => { tooltip.style.display = 'none'; };
|
||||
|
||||
ensureWidgetSize().catch(() => {});
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
if (!config.token) {
|
||||
showError('请点击 ⚙ 设置 GitHub Token(需 read:user 权限)');
|
||||
return;
|
||||
}
|
||||
showLoading();
|
||||
try {
|
||||
const result = await invoke('fetch_github_contributions', {
|
||||
username: config.username,
|
||||
token: config.token,
|
||||
});
|
||||
|
||||
if (result.errors?.length) {
|
||||
showError(result.errors[0].message);
|
||||
return;
|
||||
}
|
||||
|
||||
const calendar = result?.data?.user?.contributionsCollection?.contributionCalendar;
|
||||
if (!calendar) {
|
||||
const msg = result?.data?.user ? '无法获取贡献日历数据' : '用户不存在或 Token 权限不足';
|
||||
showError(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
renderHeatmap(calendar);
|
||||
} catch (e) {
|
||||
showError('请求失败: ' + String(e));
|
||||
}
|
||||
}
|
||||
|
||||
// Window position persistence
|
||||
async function initWindowPos() {
|
||||
if (config.window_x != null && config.window_y != null) {
|
||||
try {
|
||||
const win = getCurrentWindow();
|
||||
await win.setPosition({ type: 'Physical', x: config.window_x, y: config.window_y });
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
async function setWindowHeight(height) {
|
||||
try {
|
||||
await getCurrentWindow().setSize({ type: 'Logical', width: WINDOW_WIDTH, height });
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function clearFormError() {
|
||||
document.getElementById('form-error').classList.remove('visible');
|
||||
}
|
||||
|
||||
function showFormError() {
|
||||
document.getElementById('form-error').classList.add('visible');
|
||||
}
|
||||
|
||||
async function openSettingsModal() {
|
||||
clearFormError();
|
||||
document.getElementById('input-username').value = config.username;
|
||||
document.getElementById('input-token').value = config.token;
|
||||
document.getElementById('input-theme').value = config.theme === 'light' ? 'light' : 'dark';
|
||||
await setWindowHeight(SETTINGS_HEIGHT);
|
||||
document.body.classList.add('settings-open');
|
||||
document.querySelector('.widget').classList.add('mouse-over');
|
||||
document.getElementById('settings-modal').classList.add('visible');
|
||||
document.getElementById('input-username').focus();
|
||||
}
|
||||
|
||||
async function closeSettingsModal() {
|
||||
clearFormError();
|
||||
document.getElementById('settings-modal').classList.remove('visible');
|
||||
document.body.classList.remove('settings-open');
|
||||
await ensureWidgetSize();
|
||||
}
|
||||
|
||||
async function saveSettings() {
|
||||
const username = document.getElementById('input-username').value.trim();
|
||||
const token = document.getElementById('input-token').value.trim();
|
||||
const theme = document.getElementById('input-theme').value === 'light' ? 'light' : 'dark';
|
||||
if (!username) {
|
||||
showFormError();
|
||||
document.getElementById('input-username').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const newConfig = { ...config, username, token, theme };
|
||||
try {
|
||||
await invoke('save_config', { config: newConfig });
|
||||
config = newConfig;
|
||||
applyTheme(config.theme);
|
||||
document.getElementById('username-display').textContent = config.username;
|
||||
await closeSettingsModal();
|
||||
await fetchData();
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e);
|
||||
}
|
||||
}
|
||||
|
||||
// Settings modal
|
||||
async function togglePin() {
|
||||
isPinned = !isPinned;
|
||||
try {
|
||||
await getCurrentWindow().setAlwaysOnTop(isPinned);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function initTrayActions() {
|
||||
try {
|
||||
const { listen } = window.__TAURI__.event;
|
||||
await listen('tray-action', async ({ payload }) => {
|
||||
switch (payload) {
|
||||
case 'refresh':
|
||||
await fetchData();
|
||||
break;
|
||||
case 'pin':
|
||||
await togglePin();
|
||||
break;
|
||||
case 'theme':
|
||||
await toggleTheme();
|
||||
break;
|
||||
case 'settings':
|
||||
await openSettingsModal();
|
||||
break;
|
||||
}
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
document.getElementById('btn-cancel').addEventListener('click', () => {
|
||||
closeSettingsModal().catch(() => {});
|
||||
});
|
||||
|
||||
document.getElementById('settings-modal').addEventListener('click', (e) => {
|
||||
if (e.target === document.getElementById('settings-modal')) {
|
||||
closeSettingsModal().catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('settings-panel').addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.getElementById('btn-save').addEventListener('click', () => {
|
||||
saveSettings().catch(() => {});
|
||||
});
|
||||
|
||||
document.getElementById('settings-modal').addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
closeSettingsModal().catch(() => {});
|
||||
} else if (e.key === 'Enter' && e.target.tagName === 'INPUT') {
|
||||
e.preventDefault();
|
||||
saveSettings().catch(() => {});
|
||||
}
|
||||
});
|
||||
|
||||
['input-username', 'input-token'].forEach((id) => {
|
||||
document.getElementById(id).addEventListener('input', clearFormError);
|
||||
});
|
||||
|
||||
function initHeatmapResize() {
|
||||
const area = document.getElementById('heatmap-area');
|
||||
if (!area || typeof ResizeObserver === 'undefined') return;
|
||||
let lastWidth = 0;
|
||||
const observer = new ResizeObserver(() => {
|
||||
const w = area.clientWidth;
|
||||
if (!w || w === lastWidth || !lastHeatmapData) return;
|
||||
lastWidth = w;
|
||||
renderHeatmap(lastHeatmapData);
|
||||
});
|
||||
observer.observe(area);
|
||||
}
|
||||
|
||||
// Window drag (canvas blocks native drag-region; use startDragging as fallback)
|
||||
function initWindowDrag() {
|
||||
document.querySelector('.widget').addEventListener('mousedown', (e) => {
|
||||
if (e.button !== 0) return;
|
||||
if (document.getElementById('settings-modal').classList.contains('visible')) return;
|
||||
if (e.target.closest('input') || e.target.closest('button')) return;
|
||||
getCurrentWindow().startDragging().catch(() => {});
|
||||
});
|
||||
}
|
||||
|
||||
// Save window position on move
|
||||
(async () => {
|
||||
try {
|
||||
const win = getCurrentWindow();
|
||||
await win.listen('tauri://move', (e) => {
|
||||
clearTimeout(moveTimer);
|
||||
moveTimer = setTimeout(() => {
|
||||
const { x, y } = e.payload;
|
||||
invoke('save_window_pos', { x, y }).catch(() => {});
|
||||
}, 600);
|
||||
});
|
||||
} catch (_) {}
|
||||
})();
|
||||
|
||||
// Auto refresh every 30 minutes
|
||||
setInterval(fetchData, 30 * 60 * 1000);
|
||||
|
||||
// Init
|
||||
(async () => {
|
||||
config = await invoke('load_config').catch(() => config);
|
||||
if (!config.theme) config.theme = 'dark';
|
||||
applyTheme(config.theme, false);
|
||||
document.getElementById('username-display').textContent = config.username;
|
||||
initWindowDrag();
|
||||
initHeatmapResize();
|
||||
await initTrayActions();
|
||||
await ensureWidgetSize();
|
||||
await initWindowPos();
|
||||
await fetchData();
|
||||
})();
|
||||
397
github/frontend/style.css
Normal file
397
github/frontend/style.css
Normal file
@@ -0,0 +1,397 @@
|
||||
:root {
|
||||
--bg-widget: #0d1117;
|
||||
--border: #30363d;
|
||||
--shadow: 0 8px 32px rgba(0, 0, 0, 0.6), 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||
--text-primary: #e6edf3;
|
||||
--text-secondary: #8b949e;
|
||||
--separator: #30363d;
|
||||
--btn-icon: #7d8590;
|
||||
--btn-hover-bg: #21262d;
|
||||
--btn-hover-text: #e6edf3;
|
||||
--accent: #388bfd;
|
||||
--overlay: rgba(0, 0, 0, 0.82);
|
||||
--modal-bg: #161b22;
|
||||
--modal-footer-bg: #161b22;
|
||||
--input-bg: #0d1117;
|
||||
--input-border: #30363d;
|
||||
--input-text: #e6edf3;
|
||||
--label-text: #c9d1d9;
|
||||
--hint-text: #8b949e;
|
||||
--input-focus: #388bfd;
|
||||
--input-focus-shadow: rgba(56, 139, 253, 0.2);
|
||||
--btn-bg: #21262d;
|
||||
--btn-text: #e6edf3;
|
||||
--btn-hover: #30363d;
|
||||
--btn-primary-bg: #238636;
|
||||
--btn-primary-border: #2ea043;
|
||||
--btn-primary-hover: #2ea043;
|
||||
--tooltip-bg: #1c2128;
|
||||
--tooltip-border: #444c56;
|
||||
--tooltip-text: #e6edf3;
|
||||
--error: #f85149;
|
||||
--spinner-track: #30363d;
|
||||
--status-text: #8b949e;
|
||||
--platform-icon: #e6edf3;
|
||||
--hm-0: #161b22;
|
||||
--hm-0-border: #30363d;
|
||||
--hm-1: #0e4429;
|
||||
--hm-2: #006d32;
|
||||
--hm-3: #26a641;
|
||||
--hm-4: #39d353;
|
||||
}
|
||||
|
||||
body.theme-light {
|
||||
--bg-widget: #ffffff;
|
||||
--border: #d0d7de;
|
||||
--shadow: 0 4px 16px rgba(140, 149, 159, 0.22), 0 1px 4px rgba(27, 31, 36, 0.08);
|
||||
--text-primary: #1f2328;
|
||||
--text-secondary: #656d76;
|
||||
--separator: #d0d7de;
|
||||
--btn-icon: #656d76;
|
||||
--btn-hover-bg: #f3f4f6;
|
||||
--btn-hover-text: #1f2328;
|
||||
--accent: #0969da;
|
||||
--overlay: rgba(27, 31, 36, 0.4);
|
||||
--modal-bg: #ffffff;
|
||||
--modal-footer-bg: #ffffff;
|
||||
--input-bg: #ffffff;
|
||||
--input-border: #d0d7de;
|
||||
--input-text: #1f2328;
|
||||
--label-text: #1f2328;
|
||||
--hint-text: #656d76;
|
||||
--input-focus-shadow: rgba(9, 105, 218, 0.25);
|
||||
--btn-bg: #f6f8fa;
|
||||
--btn-text: #1f2328;
|
||||
--btn-hover: #eaeef2;
|
||||
--tooltip-bg: #ffffff;
|
||||
--tooltip-border: #d0d7de;
|
||||
--tooltip-text: #1f2328;
|
||||
--error: #cf222e;
|
||||
--spinner-track: #d0d7de;
|
||||
--status-text: #656d76;
|
||||
--platform-icon: #24292f;
|
||||
--hm-0: #ebedf0;
|
||||
--hm-0-border: #d0d7de;
|
||||
--hm-1: #9be9a8;
|
||||
--hm-2: #40c463;
|
||||
--hm-3: #30a14e;
|
||||
--hm-4: #216e39;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
background: transparent;
|
||||
font-family: "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", sans-serif;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
|
||||
.widget {
|
||||
background: var(--bg-widget);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 4px 5px 3px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
box-shadow: var(--shadow);
|
||||
transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
min-height: 20px;
|
||||
cursor: move;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.title-area {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: move;
|
||||
-webkit-app-region: drag;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.platform-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: var(--platform-icon);
|
||||
flex-shrink: 0;
|
||||
shape-rendering: geometricPrecision;
|
||||
}
|
||||
|
||||
.username {
|
||||
color: var(--text-primary);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.stats {
|
||||
color: var(--text-secondary);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.separator {
|
||||
color: var(--separator);
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
cursor: move;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.heatmap-area {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
cursor: move;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
#heatmap-canvas {
|
||||
display: none;
|
||||
cursor: crosshair;
|
||||
-webkit-app-region: no-drag;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.status-msg {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--status-text);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-msg.error {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid var(--spinner-track);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.legend-cell {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.legend-cell[data-level="0"] {
|
||||
background: var(--hm-0);
|
||||
border: 1px solid var(--hm-0-border);
|
||||
}
|
||||
|
||||
.legend-cell[data-level="1"] { background: var(--hm-1); }
|
||||
.legend-cell[data-level="2"] { background: var(--hm-2); }
|
||||
.legend-cell[data-level="3"] { background: var(--hm-3); }
|
||||
.legend-cell[data-level="4"] { background: var(--hm-4); }
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--overlay);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 100;
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-app-region: no-drag;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.modal-overlay.visible {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
body.settings-open .widget {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--modal-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 16px 18px 14px;
|
||||
width: min(320px, calc(100vw - 16px));
|
||||
max-height: calc(100vh - 16px);
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow);
|
||||
-webkit-app-region: no-drag;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--label-text);
|
||||
margin-bottom: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
background: var(--input-bg);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
color: var(--input-text);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
font-family: inherit;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
border-color: var(--input-focus);
|
||||
box-shadow: 0 0 0 2px var(--input-focus-shadow);
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--hint-text);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
display: none;
|
||||
margin-top: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.form-error.visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
background: var(--modal-footer-bg);
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 7px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--btn-bg);
|
||||
color: var(--btn-text);
|
||||
transition: background 0.12s;
|
||||
font-family: inherit;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: var(--btn-hover);
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: var(--btn-primary-bg);
|
||||
border-color: var(--btn-primary-border);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn.primary:hover {
|
||||
background: var(--btn-primary-hover);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: fixed;
|
||||
background: var(--tooltip-bg);
|
||||
border: 1px solid var(--tooltip-border);
|
||||
border-radius: 6px;
|
||||
padding: 5px 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--tooltip-text);
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
z-index: 200;
|
||||
white-space: nowrap;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user