hzlume_obsidian-note-heatmap/main.js
lu 76fd5cdb46 fix: 修复 ObsidianReviewBot 指出的代码问
题

fix: CacheData

fix: i18n/index

fix: UI text
2026-04-17 10:18:53 +08:00

2072 lines
72 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/settings.ts
var settings_exports = {};
__export(settings_exports, {
DEFAULT_SETTINGS: () => DEFAULT_SETTINGS
});
var DEFAULT_SETTINGS;
var init_settings = __esm({
"src/settings.ts"() {
DEFAULT_SETTINGS = {
targetField: "last-modified",
targetFolder: "",
// 空字符串表示整个 vault
colors: ["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"],
// 年度笔记默认配置(文件夹路径为空表示根目录)
enableYearlyNote: true,
yearlyNoteFolder: "",
yearlyNoteFormat: "YYYY",
// 月度笔记默认配置(文件夹路径为空表示根目录)
enableMonthlyNote: true,
monthlyNoteFolder: "",
monthlyNoteFormat: "YYYY-[monthly]/YYYY-MM",
// 日记默认配置(文件夹路径为空表示根目录)
enableDailyNote: true,
dailyNoteFolder: "",
dailyNoteFormat: "YYYY-[daily]/YYYY-MM-[daily]/YYYY-MM-DD",
enableGitDiff: false,
// 将在 onload 时根据依赖检测自动设置
createdField: "created"
// 默认使用 "created" 字段
};
}
});
// src/main.ts
var main_exports = {};
__export(main_exports, {
default: () => NoteHeatmapPlugin
});
module.exports = __toCommonJS(main_exports);
var import_obsidian4 = require("obsidian");
// src/heatmapView.ts
var import_obsidian = require("obsidian");
// src/i18n/en.ts
var en = {
// Settings - General
settings: {
title: "Note heatmap settings",
targetField: {
name: "Target field",
desc: "Frontmatter field name for tracking modification dates. Supports single value or list."
},
createdField: {
name: "Created field",
desc: "Frontmatter field name for determining if a note is new. "
},
targetFolder: {
name: "Target folder",
desc: "Which folder to track notes from (relative to vault root)"
}
},
// Settings - Colors
colors: {
title: "Color configuration",
desc: "From left to right: no edits / 1-3 notes / 4-7 notes / 8-12 notes / 13+ notes.",
labels: [
"No edits (0)",
"Light green (1-3)",
"Medium green (4-7)",
"Dark green (8-12)",
"Darkest (\u226513)"
]
},
// Settings - Periodic Notes
periodicNotes: {
title: "Periodic note path configuration",
yearly: {
name: "Yearly note",
desc: "Enable to show link to yearly note when clicking the year",
folder: {
name: "Folder path",
desc: "Root folder for yearly notes (relative to vault root). Leave empty for vault root",
placeholder: "Leave empty for root"
},
format: {
name: "File format",
desc: "File path format for yearly notes, based on the folder above.",
placeholder: "YYYY"
}
},
daily: {
name: "Daily note",
desc: "Enable to show link to daily note when clicking a date",
folder: {
name: "Folder path",
desc: "Root folder for daily notes (relative to vault root). Leave empty for vault root",
placeholder: "Leave empty for root"
},
format: {
name: "File format",
desc: "File path format for daily notes, based on the folder above. ",
placeholder: ""
}
},
monthly: {
name: "Monthly note",
desc: "Enable to show link to monthly note when clicking a month",
folder: {
name: "Folder path",
desc: "Root folder for monthly notes (relative to vault root). Leave empty for vault root",
placeholder: "Leave empty for root"
},
format: {
name: "File format",
desc: "File path format for monthly notes, based on the folder above.",
placeholder: ""
}
}
},
// Settings - Git Diff
gitDiff: {
title: "Git diff feature",
enable: {
name: "Enable Git diff feature",
descAllAvailable: "\u2705 all dependencies installed(Git and version-history-diff).",
descMissingObsidianGit: "\u26A0\uFE0F missing",
descMissingVHD: "\u26A0\uFE0F missing",
descMissingBoth: "\u26A0\uFE0F missing both dependencies:",
pluginObsidianGit: "Obsidian Git",
pluginVHD: "Plugin version-history-diff",
separator: " + "
}
},
// Settings - Reset
reset: {
name: "Reset to default settings",
button: "Reset"
},
// Commands
commands: {
openView: "Open note heatmap",
refreshView: "Refresh note heatmap"
},
// Heatmap View
view: {
title: "Note heatmap",
header: {
refresh: "Refresh",
prevYear: "Previous year",
nextYear: "Next year",
openYearlyNote: "Open {{year}} yearly note"
},
tooltip: {
noEdits: "No edits",
edits: "{{count}} edits",
new: "{{count}} new",
outOfYear: "Not in {{year}}"
},
weekdays: ["Sun", "", "Tue", "", "Thu", "", "Sat"],
months: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
periodicNote: {
createFailed: "Failed to create {{type}} note"
}
},
// Result Panel
resultPanel: {
hint: "\u{1F4A1} click a square above to view edits for that day",
noEdits: "No edits for this day",
noEditsMonth: "No edits for this month",
close: "Close file list",
totalEdits: "{{total}} edits, {{new}} new",
diffButton: "View diff",
vhdButton: "View diff",
searchPlaceholder: "\u{1F50D} search note names...",
sortBy: {
name: "Name",
activeDays: "Active days",
lastModified: "Last modified"
},
days: "{{count}}d"
},
// Git Service
git: {
installNotice: "\u{1F4A1} install the Obsidian Git plugin to view detailed diff",
noCommits: "No commits found",
vhdNotInstalled: "Plugin version-history-diff is not installed or enabled",
vhdOpenFailed: "Failed to open version-history-diff plugin."
}
};
// src/i18n/zh.ts
var zh = {
// Settings - General
settings: {
title: "Note Heatmap \u8BBE\u7F6E",
targetField: {
name: "\u76EE\u6807\u5B57\u6BB5",
desc: "\u7528\u4E8E\u7EDF\u8BA1\u4FEE\u6539\u65E5\u671F\u7684 frontmatter \u5B57\u6BB5\u540D\u3002\u652F\u6301\u5355\u4E2A\u503C\u6216\u5217\u8868\u3002\u5B57\u6BB5\u503C\u683C\u5F0F\uFF1AISO 8601 (\u5982 2026-04-14T10:30:00)\u3001YYYY-MM-DD (\u5982 2026-04-14)\u3001Obsidian \u65E5\u671F\u5BF9\u8C61"
},
createdField: {
name: "\u521B\u5EFA\u65F6\u95F4\u5B57\u6BB5",
desc: "\u7528\u4E8E\u5224\u65AD\u7B14\u8BB0\u662F\u5426\u4E3A\u65B0\u589E\u7684 frontmatter \u5B57\u6BB5\u540D\u3002\u5B57\u6BB5\u503C\u683C\u5F0F\uFF1AISO 8601 (\u5982 2026-04-14T10:30:00)\u3001YYYY-MM-DD (\u5982 2026-04-14)\u3001Obsidian \u65E5\u671F\u5BF9\u8C61\u3002\u82E5\u5B57\u6BB5\u4E0D\u5B58\u5728\u5219\u4F7F\u7528\u6587\u4EF6\u521B\u5EFA\u65F6\u95F4"
},
targetFolder: {
name: "\u76EE\u6807\u6587\u4EF6\u5939",
desc: "\u7EDF\u8BA1\u54EA\u4E2A\u6587\u4EF6\u5939\u4E0B\u7684\u7B14\u8BB0\uFF08\u76F8\u5BF9\u4E8E vault \u6839\u76EE\u5F55\uFF09"
}
},
// Settings - Colors
colors: {
title: "\u989C\u8272\u914D\u7F6E",
desc: "\u4ECE\u5DE6\u5230\u53F3\u4F9D\u6B21\u5BF9\u5E94\uFF1A\u65E0\u4FEE\u6539 / 1-3\u7BC7 / 4-7\u7BC7 / 8-12\u7BC7 / 13\u7BC7\u4EE5\u4E0A",
labels: [
"\u65E0\u4FEE\u6539\uFF080\u7BC7\uFF09",
"\u6D45\u7EFF\uFF081-3\u7BC7\uFF09",
"\u4E2D\u7EFF\uFF084-7\u7BC7\uFF09",
"\u6DF1\u7EFF\uFF088-12\u7BC7\uFF09",
"\u6700\u6DF1\uFF08\u226513\u7BC7\uFF09"
]
},
// Settings - Periodic Notes
periodicNotes: {
title: "\u5468\u671F\u7B14\u8BB0\u8DEF\u5F84\u914D\u7F6E",
yearly: {
name: "\u5E74\u5EA6\u7B14\u8BB0",
desc: "\u5F00\u542F\u540E\uFF0C\u70B9\u51FB\u5E74\u4EFD\u65F6\u4F1A\u663E\u793A\u8DF3\u8F6C\u5230\u5E74\u5EA6\u7B14\u8BB0\u7684\u94FE\u63A5",
folder: {
name: "\u6587\u4EF6\u5939\u8DEF\u5F84",
desc: "\u5E74\u5EA6\u7B14\u8BB0\u7684\u6839\u6587\u4EF6\u5939\uFF08\u76F8\u5BF9\u4E8E vault \u6839\u76EE\u5F55\uFF09\uFF0C\u7559\u7A7A\u8868\u793A vault \u6839\u76EE\u5F55",
placeholder: "\u7559\u7A7A\u8868\u793A\u6839\u76EE\u5F55"
},
format: {
name: "\u6587\u4EF6\u683C\u5F0F",
desc: "\u5E74\u5EA6\u7B14\u8BB0\u7684\u6587\u4EF6\u8DEF\u5F84\u683C\u5F0F\uFF0C\u57FA\u4E8E\u4E0A\u8FF0\u6587\u4EF6\u5939\u3002",
placeholder: ""
}
},
daily: {
name: "\u6BCF\u65E5\u7B14\u8BB0",
desc: "\u5F00\u542F\u540E\uFF0C\u70B9\u51FB\u65E5\u671F\u65F6\u4F1A\u663E\u793A\u8DF3\u8F6C\u5230\u6BCF\u65E5\u7B14\u8BB0\u7684\u94FE\u63A5",
folder: {
name: "\u6587\u4EF6\u5939\u8DEF\u5F84",
desc: "\u6BCF\u65E5\u7B14\u8BB0\u7684\u6839\u6587\u4EF6\u5939\uFF08\u76F8\u5BF9\u4E8E vault \u6839\u76EE\u5F55\uFF09\uFF0C\u7559\u7A7A\u8868\u793A vault \u6839\u76EE\u5F55",
placeholder: "\u7559\u7A7A\u8868\u793A\u6839\u76EE\u5F55"
},
format: {
name: "\u6587\u4EF6\u683C\u5F0F",
desc: "\u6BCF\u65E5\u7B14\u8BB0\u7684\u6587\u4EF6\u8DEF\u5F84\u683C\u5F0F\uFF0C\u57FA\u4E8E\u4E0A\u8FF0\u6587\u4EF6\u5939\u3002",
placeholder: ""
}
},
monthly: {
name: "\u6708\u5EA6\u7B14\u8BB0",
desc: "\u5F00\u542F\u540E\uFF0C\u70B9\u51FB\u6708\u4EFD\u65F6\u4F1A\u663E\u793A\u8DF3\u8F6C\u5230\u6708\u5EA6\u7B14\u8BB0\u7684\u94FE\u63A5",
folder: {
name: "\u6587\u4EF6\u5939\u8DEF\u5F84",
desc: "\u6708\u5EA6\u7B14\u8BB0\u7684\u6839\u6587\u4EF6\u5939\uFF08\u76F8\u5BF9\u4E8E vault \u6839\u76EE\u5F55\uFF09\uFF0C\u7559\u7A7A\u8868\u793A vault \u6839\u76EE\u5F55",
placeholder: "\u7559\u7A7A\u8868\u793A\u6839\u76EE\u5F55"
},
format: {
name: "\u6587\u4EF6\u683C\u5F0F",
desc: "\u6708\u5EA6\u7B14\u8BB0\u7684\u6587\u4EF6\u8DEF\u5F84\u683C\u5F0F\uFF0C\u57FA\u4E8E\u4E0A\u8FF0\u6587\u4EF6\u5939\u3002",
placeholder: ""
}
}
},
// Settings - Git Diff
gitDiff: {
title: "Git Diff \u529F\u80FD",
enable: {
name: "\u542F\u7528 Git Diff \u529F\u80FD",
descAllAvailable: "\u2705 \u4F9D\u8D56\u5DF2\u6EE1\u8DB3\u3002\u9700\u8981\uFF1AObsidian Git + Version History Diff",
descMissingObsidianGit: "\u26A0\uFE0F \u7F3A\u5C11",
descMissingVHD: "\u26A0\uFE0F \u7F3A\u5C11",
descMissingBoth: "\u26A0\uFE0F \u7F3A\u5C11\u4EE5\u4E0B\u4F9D\u8D56\uFF1A",
pluginObsidianGit: "Obsidian Git",
pluginVHD: "Version History Diff",
separator: " + "
}
},
// Settings - Reset
reset: {
name: "\u91CD\u7F6E\u4E3A\u9ED8\u8BA4\u8BBE\u7F6E",
button: "\u91CD\u7F6E"
},
// Commands
commands: {
openView: "\u6253\u5F00\u7B14\u8BB0\u70ED\u529B\u56FE",
refreshView: "\u5237\u65B0\u7B14\u8BB0\u70ED\u529B\u56FE"
},
// Heatmap View
view: {
title: "\u7B14\u8BB0\u70ED\u529B\u56FE",
header: {
refresh: "\u5237\u65B0",
prevYear: "\u4E0A\u4E00\u5E74",
nextYear: "\u4E0B\u4E00\u5E74",
openYearlyNote: "\u6253\u5F00 {{year}} \u5E74\u5EA6\u7B14\u8BB0"
},
tooltip: {
noEdits: "\u65E0\u4FEE\u6539",
edits: "\u4FEE\u6539\u4E86 {{count}} \u7BC7",
new: "\u5176\u4E2D {{count}} \u7BC7\u4E3A\u65B0\u589E",
outOfYear: "\u4E0D\u5728{{year}}\u5E74\u5185"
},
weekdays: ["\u65E5", "", "\u4E8C", "", "\u56DB", "", "\u516D"],
months: ["1\u6708", "2\u6708", "3\u6708", "4\u6708", "5\u6708", "6\u6708", "7\u6708", "8\u6708", "9\u6708", "10\u6708", "11\u6708", "12\u6708"],
periodicNote: {
createFailed: "\u521B\u5EFA{{type}}\u7B14\u8BB0\u5931\u8D25"
}
},
// Result Panel
resultPanel: {
hint: "\u{1F4A1} \u70B9\u51FB\u4E0A\u65B9\u65B9\u5757\u67E5\u770B\u8BE5\u65E5\u4FEE\u6539\u8BB0\u5F55",
noEdits: "\u5F53\u65E5\u65E0\u4FEE\u6539\u8BB0\u5F55",
noEditsMonth: "\u5F53\u6708\u65E0\u4FEE\u6539\u8BB0\u5F55",
close: "\u5173\u95ED\u6587\u4EF6\u5217\u8868",
totalEdits: "\u5171\u4FEE\u6539 {{total}} \u7BC7\uFF0C\u5176\u4E2D {{new}} \u7BC7\u4E3A\u65B0\u589E",
diffButton: "\u67E5\u770B Diff",
vhdButton: "\u67E5\u770B Diff",
searchPlaceholder: "\u{1F50D} \u641C\u7D22\u7B14\u8BB0\u540D\u79F0...",
sortBy: {
name: "\u540D\u79F0",
activeDays: "\u6D3B\u8DC3\u5929\u6570",
lastModified: "\u6700\u540E\u4FEE\u6539"
},
days: "{{count}}\u5929"
},
// Git Service
git: {
installNotice: "\u{1F4A1} \u5B89\u88C5 Obsidian Git \u63D2\u4EF6\u53EF\u67E5\u770B\u8BE6\u7EC6 Diff",
noCommits: "\u65E0\u63D0\u4EA4\u8BB0\u5F55",
vhdNotInstalled: "Version History Diff \u63D2\u4EF6\u672A\u5B89\u88C5\u6216\u672A\u542F\u7528",
vhdOpenFailed: "\u6253\u5F00 Version History Diff \u5931\u8D25"
}
};
// src/i18n/index.ts
var translations = {
en,
zh
};
function getCurrentLang() {
var _a, _b;
const momentLocale = ((_b = (_a = window.moment) == null ? void 0 : _a.locale) == null ? void 0 : _b.call(_a)) || "en";
if (momentLocale.startsWith("zh")) {
return "zh";
}
return "en";
}
function t(key, vars) {
const lang = getCurrentLang();
const trans = translations[lang] || translations.en;
const keys = key.split(".");
let value = trans;
for (const k of keys) {
if (value && typeof value === "object" && k in value) {
value = value[k];
} else {
return key;
}
}
if (typeof value !== "string") {
return key;
}
if (vars) {
return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
var _a, _b;
return (_b = (_a = vars[varName]) == null ? void 0 : _a.toString()) != null ? _b : match;
});
}
return value;
}
function tArray(key) {
const lang = getCurrentLang();
const trans = translations[lang] || translations.en;
const keys = key.split(".");
let value = trans;
for (const k of keys) {
if (value && typeof value === "object" && k in value) {
value = value[k];
} else {
return [];
}
}
if (Array.isArray(value)) {
return value;
}
return [];
}
// src/vhdUtils.ts
function selectCommitsInVHD(commits, previousCommit) {
var _a;
try {
const vhdModal = document.querySelector(".modal-container .diff");
if (!vhdModal)
return;
const historyListContainers = vhdModal.querySelectorAll(".sync-history-list-container");
if (historyListContainers.length < 2)
return;
const leftList = historyListContainers[0].querySelector(".sync-history-list");
const rightList = historyListContainers[1].querySelector(".sync-history-list");
if (!leftList || !rightList)
return;
const rightCommitHash = (_a = commits[commits.length - 1]) == null ? void 0 : _a.hash;
if (!rightCommitHash)
return;
selectCommitInList(rightList, rightCommitHash);
if (previousCommit) {
selectCommitInList(leftList, previousCommit.hash);
} else if (commits.length > 1) {
selectCommitInList(leftList, commits[0].hash);
}
} catch (err) {
console.error("[NoteHeatmap] \u8BBE\u7F6E\u9009\u4E2D\u72B6\u6001\u5931\u8D25:", err);
}
}
function selectCommitInList(listContainer, targetHash) {
const items = Array.from(listContainer.querySelectorAll(".sync-history-list-item-header"));
if (items.length === 0)
return null;
for (const item of items) {
const itemText = item.textContent || "";
if (itemText.includes(targetHash.substring(0, 7))) {
const element = item;
element.click();
setTimeout(() => {
element.scrollIntoView({ behavior: "smooth", block: "center" });
}, 100);
return element;
}
}
return null;
}
// src/heatmapView.ts
var HEATMAP_VIEW_TYPE = "note-heatmap-view";
var BOX_SIZE = 12;
var GAP = 3;
var COLUMN_WIDTH = BOX_SIZE + GAP;
var HeatmapView = class extends import_obsidian.ItemView {
constructor(leaf, plugin) {
super(leaf);
this.tooltipDiv = null;
this.renderDebounceTimer = null;
this.isRendering = false;
this.plugin = plugin;
this.currentYear = new Date().getFullYear();
}
/**
* 格式化日期路径
* 支持YYYY、MM、DD
* 方括号 [text] 中的内容原样输出
* 使用 Moment.js 处理,与 Periodic Notes 保持一致
*/
formatDatePath(format, year, month, day) {
const m = window.moment();
m.year(Number(year));
m.month(Number(month) - 1);
if (day !== void 0) {
m.date(Number(day));
}
return m.format(format);
}
getViewType() {
return HEATMAP_VIEW_TYPE;
}
getDisplayText() {
return t("view.title");
}
getIcon() {
return "calendar-with-checkmark";
}
async onOpen() {
try {
await this.plugin.dataCache.waitForReady();
setTimeout(() => {
this.render();
}, 0);
} catch (err) {
console.error("[NoteHeatmap] Failed to wait for cache initialization:", err);
this.render();
}
}
onClose() {
if (this.tooltipDiv) {
this.tooltipDiv.remove();
this.tooltipDiv = null;
}
if (this.renderDebounceTimer) {
clearTimeout(this.renderDebounceTimer);
}
}
/**
* 防抖渲染 - 避免频繁更新造成卡顿
*/
render() {
if (this.isRendering) {
if (this.renderDebounceTimer) {
clearTimeout(this.renderDebounceTimer);
}
this.renderDebounceTimer = window.setTimeout(() => {
void this.doRender();
}, 100);
return;
}
if (this.renderDebounceTimer) {
clearTimeout(this.renderDebounceTimer);
}
this.renderDebounceTimer = window.setTimeout(() => {
void this.doRender();
}, 50);
}
doRender() {
if (this.isRendering)
return;
this.isRendering = true;
try {
this.performRender();
} catch (err) {
console.error("[NoteHeatmap] Render failed:", err);
} finally {
this.isRendering = false;
}
}
performRender() {
const container = this.containerEl.children[1];
container.empty();
container.addClass("note-heatmap-container");
const year = this.currentYear;
const { colors } = this.plugin.settings;
const dataMap = this.plugin.dataCache.getYearData(year);
const monthlyData = this.prepareMonthlyData(dataMap);
const monthOffsets = this.calcMonthOffsets(year);
const wrapper = this.createWrapper(container);
const header = this.renderHeader(wrapper, year);
const { calGrid, monthElements } = this.renderHeatmap(wrapper, year, colors, dataMap, monthOffsets);
const resPanel = this.renderResultPanel(wrapper);
this.bindHeaderEvents(header, year);
this.bindMonthEvents(monthElements, year, monthlyData, resPanel);
this.bindGridEvents(calGrid, dataMap, resPanel);
}
/** 创建包装容器 */
createWrapper(container) {
const wrapper = container.createDiv({ cls: "heatmap-plugin-wrapper note-heatmap-flex-col note-heatmap-width-full" });
return wrapper;
}
/** 渲染头部(年份导航) */
renderHeader(wrapper, year) {
const header = wrapper.createDiv({ cls: "heatmap-header" });
const yearNav = header.createDiv({ cls: "year-nav" });
yearNav.createEl("button", { cls: "year-nav-btn prev-year", text: "\u2039", title: t("view.header.prevYear") });
yearNav.createDiv({ cls: "year-display", text: String(year) });
yearNav.createEl("button", { cls: "year-nav-btn next-year", text: "\u203A", title: t("view.header.nextYear") });
header.createEl("button", { cls: "heatmap-refresh-btn", text: t("view.header.refresh") });
return header;
}
/** 渲染热力图网格 */
renderHeatmap(wrapper, year, colors, dataMap, monthOffsets) {
const scrollContainer = wrapper.createDiv({ cls: "heatmap-scroll-container" });
const heatmapSection = scrollContainer.createDiv({ cls: "heatmap-section" });
const monthContainer = heatmapSection.createDiv({ cls: "month-container" });
const monthElements = [];
const monthNames = tArray("view.months");
for (let i = 0; i < 12; i++) {
const mDiv = monthContainer.createDiv({ cls: "m-name" });
mDiv.setCssStyles({ left: `${monthOffsets[i]}px` });
mDiv.setText(monthNames[i]);
mDiv.dataset.monthIndex = String(i);
monthElements.push(mDiv);
}
const calBody = heatmapSection.createDiv({ cls: "cal-body" });
const wLabs = calBody.createDiv({ cls: "w-labs" });
const weekDays = tArray("view.weekdays");
for (const w of weekDays) {
wLabs.createDiv({ cls: "w-lab", text: w });
}
const calGrid = calBody.createDiv({ cls: "cal-grid" });
this.buildCalendarGrid(calGrid, year, colors, dataMap);
return { calGrid, monthElements };
}
/** 渲染结果面板(初始状态) */
renderResultPanel(wrapper) {
const resPanel = wrapper.createDiv({ attr: { id: "res-panel" } });
const hint = resPanel.createEl("span", { cls: "note-heatmap-empty" });
hint.setText(t("resultPanel.hint"));
return resPanel;
}
/** 绑定头部事件 */
bindHeaderEvents(header, year) {
const currentYear = new Date().getFullYear();
const prevBtn = header.querySelector(".prev-year");
const nextBtn = header.querySelector(".next-year");
const yearDisplay = header.querySelector(".year-display");
const refreshBtn = header.querySelector(".heatmap-refresh-btn");
if (year >= currentYear) {
nextBtn.disabled = true;
}
prevBtn.addEventListener("click", () => {
this.currentYear--;
this.render();
});
nextBtn.addEventListener("click", () => {
if (this.currentYear < currentYear) {
this.currentYear++;
this.render();
}
});
if (this.plugin.settings.enableYearlyNote) {
yearDisplay.addClass("clickable");
yearDisplay.title = t("view.header.openYearlyNote", { year: String(year) });
this.bindYearlyNoteHover(yearDisplay, year);
yearDisplay.addEventListener("click", () => {
void this.openPeriodicNote("yearly", year);
});
}
refreshBtn.addEventListener("click", () => {
void this.plugin.forceRefresh();
});
}
/** 绑定年度笔记悬停预览 */
bindYearlyNoteHover(yearDisplay, year) {
const yearStr = String(year);
let filePath = this.formatDatePath(this.plugin.settings.yearlyNoteFormat, yearStr, "01", "01");
if (this.plugin.settings.yearlyNoteFolder) {
filePath = `${this.plugin.settings.yearlyNoteFolder}/${filePath}`;
}
if (!filePath.endsWith(".md")) {
filePath += ".md";
}
yearDisplay.addEventListener("mouseover", (e) => {
this.app.workspace.trigger("hover-link", {
event: e,
source: "preview",
hoverParent: yearDisplay,
targetEl: yearDisplay,
linktext: filePath,
sourcePath: ""
});
});
}
/** 绑定月份标签事件 */
bindMonthEvents(monthElements, year, monthlyData, resPanel) {
const { totals, notes, newCounts } = monthlyData;
for (let i = 0; i < 12; i++) {
const el = monthElements[i];
const tooltipText = `\u{1F4A1} ${year}-M${String(i + 1).padStart(2, "0")} ${t("view.tooltip.edits", { count: totals[i] })}, ${t("view.tooltip.new", { count: newCounts[i] })}`;
el.addEventListener("mouseenter", () => this.showTooltip(el, tooltipText));
el.addEventListener("mouseleave", () => this.hideTooltip());
el.addEventListener("click", () => {
const titleText = `${year}-M${String(i + 1).padStart(2, "0")}`;
this.displayNotesList(resPanel, titleText, notes[i], i, year, () => this.resetPanel(resPanel));
});
}
}
/** 绑定网格点击事件 */
bindGridEvents(calGrid, dataMap, resPanel) {
calGrid.addEventListener("click", (e) => {
const target = e.target;
const box = target.closest(".h-box");
if (!(box instanceof HTMLElement) || !box.dataset.date)
return;
calGrid.querySelectorAll(".h-box.active").forEach((b) => b.removeClass("active"));
box.addClass("active");
const dateStr = box.dataset.date;
const notes = dataMap[dateStr] || [];
this.displayDayPanel(resPanel, dateStr, notes, dataMap, () => this.resetPanel(resPanel));
});
}
/** 显示 tooltip */
showTooltip(el, text) {
if (!this.tooltipDiv) {
this.tooltipDiv = document.createElement("div");
this.tooltipDiv.className = "heatmap-tooltip note-heatmap-tooltip";
document.body.appendChild(this.tooltipDiv);
}
this.tooltipDiv.textContent = text;
const rect = el.getBoundingClientRect();
this.tooltipDiv.setCssStyles({
left: `${rect.left}px`,
top: `${rect.bottom + 5}px`
});
this.tooltipDiv.addClass("visible");
}
/** 隐藏 tooltip */
hideTooltip() {
if (this.tooltipDiv)
this.tooltipDiv.removeClass("visible");
}
/** 重置结果面板 */
resetPanel(resPanel) {
resPanel.empty();
const span = resPanel.createEl("span", { cls: "note-heatmap-empty" });
span.setText(t("resultPanel.hint"));
}
/**
* 在 Version History Diff 中打开文件
* @param note 笔记条目
* @param dateStr 日期字符串(可选,用于选中当日提交)
* @param yearMonth 年月字符串(可选,用于选中当月提交)
*/
async openInVHD(note, dateStr, yearMonth) {
const file = this.app.vault.getAbstractFileByPath(note.path);
if (!(file instanceof import_obsidian.TFile)) {
new import_obsidian.Notice(t("git.noCommits"));
return;
}
try {
const vhdPlugin = this.app.plugins.plugins["obsidian-version-history-diff"];
if (!vhdPlugin) {
new import_obsidian.Notice(t("git.vhdNotInstalled"));
return;
}
let diff = null;
if (dateStr) {
diff = await this.plugin.gitService.getDayDiff(file, dateStr);
} else if (yearMonth) {
diff = await this.plugin.gitService.getMonthDiff(file, yearMonth);
}
vhdPlugin.openGitDiffModal(file);
if ((diff == null ? void 0 : diff.commits) && diff.commits.length > 0) {
setTimeout(() => {
selectCommitsInVHD(diff.commits, diff.previousCommit);
}, 500);
}
} catch (err) {
console.error("[NoteHeatmap] \u6253\u5F00 Version History Diff \u5931\u8D25:", err);
new import_obsidian.Notice(t("git.vhdOpenFailed"));
}
}
createInternalLink(text, path) {
const a = document.createElement("a");
a.className = "res-link internal-link";
a.textContent = text;
a.setAttribute("data-href", path);
a.setAttribute("href", path);
a.addEventListener("click", (mouseEvt) => {
mouseEvt.preventDefault();
const useNewLeaf = mouseEvt.ctrlKey || mouseEvt.metaKey;
void this.app.workspace.openLinkText(path, "", useNewLeaf);
});
a.addEventListener("mouseover", (evt) => {
const target = evt.target;
this.app.workspace.trigger("hover-link", {
event: evt,
source: "preview",
hoverParent: target,
targetEl: target,
linktext: path,
sourcePath: ""
});
});
return a;
}
displayNotesList(resPanel, titleText, notesArray, monthIndex, year, onClose) {
resPanel.empty();
resPanel.scrollTop = 0;
const { newCount, monthStr } = this.calcMonthStats(notesArray, monthIndex, year);
this.renderPanelHeader(resPanel, titleText, notesArray.length, newCount, onClose, "month", year, monthStr);
if (notesArray.length === 0) {
this.renderEmptyMessage(resPanel, t("resultPanel.noEditsMonth"));
return;
}
const noteModDaysMap = this.buildNoteModDaysMap(year, monthIndex);
const { searchQuery, sortKey, sortDesc } = this.renderSearchAndSortControls(resPanel, () => {
renderList(searchQuery.value, sortKey.value, sortDesc.value);
});
const listContainer = resPanel.createDiv({ cls: "note-heatmap-list-container" });
const ul = listContainer.createEl("ul", { cls: "note-heatmap-list" });
const renderList = (search, key, desc) => {
const filtered = this.filterNotes(notesArray, search);
const sorted = this.sortNotes(filtered, key, desc, noteModDaysMap);
this.renderNoteList(ul, sorted, year, monthIndex, noteModDaysMap, (note) => {
const yearMonth = `${year}-${monthStr}`;
return this.openInVHD(note, void 0, yearMonth);
});
};
renderList(searchQuery.value, sortKey.value, sortDesc.value);
}
/** 计算月份统计数据 */
calcMonthStats(notesArray, monthIndex, year) {
const firstDayOfMonth = new Date(year, monthIndex, 1);
const lastDayOfMonth = new Date(year, monthIndex + 1, 0);
const newCount = notesArray.filter((n) => {
if (!n.createdDate)
return false;
const cd = new Date(n.createdDate);
return cd >= firstDayOfMonth && cd <= lastDayOfMonth;
}).length;
return { newCount, monthStr: String(monthIndex + 1).padStart(2, "0") };
}
/** 构建笔记修改天数映射 */
buildNoteModDaysMap(year, monthIndex) {
const yearData = this.plugin.dataCache.getYearData(year);
const noteModDaysMap = /* @__PURE__ */ new Map();
for (const [dateStr, notes] of Object.entries(yearData)) {
const dateMonth = parseInt(dateStr.split("-")[1]) - 1;
if (dateMonth !== monthIndex)
continue;
for (const note of notes) {
noteModDaysMap.set(note.path, (noteModDaysMap.get(note.path) || 0) + 1);
}
}
return noteModDaysMap;
}
/** 渲染面板标题栏(月份/日期面板共用) */
renderPanelHeader(resPanel, titleText, totalCount, newCount, onClose, type, year, monthStr, dayStr) {
const titleDiv = resPanel.createDiv({ cls: "note-heatmap-title-row" });
const leftDiv = titleDiv.createDiv({ cls: "title-left" });
if (type === "month" && this.plugin.settings.enableMonthlyNote || type === "day" && this.plugin.settings.enableDailyNote) {
const format = type === "month" ? this.plugin.settings.monthlyNoteFormat : this.plugin.settings.dailyNoteFormat;
const folder = type === "month" ? this.plugin.settings.monthlyNoteFolder : this.plugin.settings.dailyNoteFolder;
const notePath = this.buildNotePath(format, folder, year, monthStr, dayStr);
const link = this.createInternalLink(titleText, notePath);
leftDiv.appendChild(link);
} else {
leftDiv.createEl("span", { text: titleText });
}
const modCountSpan = leftDiv.createEl("span", { cls: "mod-count" });
if (totalCount === 0 && type === "day") {
modCountSpan.setText(t("view.tooltip.noEdits"));
} else {
modCountSpan.setText(t("resultPanel.totalEdits", { total: totalCount, new: newCount }));
}
const closeBtn = titleDiv.createEl("button", { cls: "close-btn", text: "\u2715" });
closeBtn.setAttribute("aria-label", t("resultPanel.close"));
closeBtn.addEventListener("click", (e) => {
e.stopPropagation();
onClose();
});
}
/** 构建周期笔记路径 */
buildNotePath(format, folder, year, monthStr, dayStr) {
const path = this.formatDatePath(format, String(year), monthStr, dayStr || "01");
const fullPath = folder ? `${folder}/${path}` : path;
return fullPath.endsWith(".md") ? fullPath : `${fullPath}.md`;
}
/** 渲染空消息 */
renderEmptyMessage(resPanel, message) {
const emptyDiv = resPanel.createDiv({ cls: "note-heatmap-empty" });
emptyDiv.setText(message);
}
/** 渲染搜索和排序控件 */
renderSearchAndSortControls(resPanel, onChange) {
const state = {
searchQuery: { value: "" },
sortKey: { value: "modifiedTime" },
sortDesc: { value: true }
};
const searchDiv = resPanel.createDiv({ cls: "note-heatmap-search-container" });
const searchInput = searchDiv.createEl("input", { cls: "note-heatmap-search-input" });
searchInput.type = "text";
searchInput.placeholder = t("resultPanel.searchPlaceholder");
searchInput.addEventListener("input", (e) => {
state.searchQuery.value = e.target.value;
onChange();
});
const sortDiv = resPanel.createDiv({ cls: "note-heatmap-sort-container" });
const sortButtons = /* @__PURE__ */ new Map();
const updateSortButtons = () => {
sortButtons.forEach((btn, key) => {
const isActive = state.sortKey.value === key;
const arrow = isActive ? state.sortDesc.value ? "\u2193" : "\u2191" : "";
btn.setText(`${t(`resultPanel.sortBy.${key === "modifiedTime" ? "lastModified" : key}`)} ${arrow}`.trim());
if (isActive) {
btn.addClass("active");
} else {
btn.removeClass("active");
}
});
};
const createSortBtn = (key) => {
const btn = sortDiv.createEl("button", { cls: "note-heatmap-sort-btn" });
sortButtons.set(key, btn);
btn.addEventListener("click", () => {
if (state.sortKey.value === key) {
state.sortDesc.value = !state.sortDesc.value;
} else {
state.sortKey.value = key;
state.sortDesc.value = true;
}
updateSortButtons();
onChange();
});
return btn;
};
createSortBtn("name");
createSortBtn("activeDays");
createSortBtn("modifiedTime");
updateSortButtons();
return state;
}
/** 过滤笔记 */
filterNotes(notes, query) {
if (!query.trim())
return notes;
const lowerQuery = query.toLowerCase();
return notes.filter((n) => n.name.toLowerCase().includes(lowerQuery));
}
/** 排序笔记 */
sortNotes(notes, key, desc, noteModDaysMap) {
const sorted = [...notes].sort((a, b) => {
if (key === "name")
return a.name.localeCompare(b.name);
if (key === "activeDays") {
const daysA = noteModDaysMap.get(a.path) || 1;
const daysB = noteModDaysMap.get(b.path) || 1;
return daysA - daysB;
}
return a.modifiedTime - b.modifiedTime;
});
return desc ? sorted.reverse() : sorted;
}
/** 渲染笔记列表 */
renderNoteList(ul, notes, year, monthIndex, noteModDaysMap, onVhdClick) {
ul.empty();
const firstDayOfMonth = new Date(year, monthIndex, 1);
const lastDayOfMonth = new Date(year, monthIndex + 1, 0);
notes.forEach((note) => {
const li = ul.createEl("li", { cls: "res-item note-heatmap-list-item" });
const leftDiv = li.createDiv({ cls: "note-heatmap-list-item-left" });
const createdDate = note.createdDate ? new Date(note.createdDate) : null;
const isNew = createdDate !== null && createdDate >= firstDayOfMonth && createdDate <= lastDayOfMonth;
const displayName = isNew ? `\u{1F195} ${note.name}` : note.name;
const a = this.createInternalLink(displayName, note.path);
a.addClass("note-heatmap-link-text");
leftDiv.appendChild(a);
const modDays = noteModDaysMap.get(note.path) || 1;
const daysSpan = leftDiv.createEl("span", { cls: "note-heatmap-days-badge" });
daysSpan.setText(t("resultPanel.days", { count: modDays }));
const modTime = new Date(note.modifiedTime);
const timeStr = `${String(modTime.getMonth() + 1).padStart(2, "0")}-${String(modTime.getDate()).padStart(2, "0")} ${String(modTime.getHours()).padStart(2, "0")}:${String(modTime.getMinutes()).padStart(2, "0")}`;
const timeSpan = leftDiv.createEl("span", { cls: "note-heatmap-time" });
timeSpan.setText(timeStr);
if (this.plugin.settings.enableGitDiff) {
const vhdBtn = li.createEl("button", { cls: "diff-btn note-heatmap-vhd-btn", text: t("resultPanel.vhdButton") });
vhdBtn.addEventListener("click", (e) => {
e.stopPropagation();
void onVhdClick(note);
});
}
});
}
/**
* 准备月度统计数据
*/
prepareMonthlyData(dataMap) {
const uniqueNotesMap = Array.from({ length: 12 }, () => /* @__PURE__ */ new Map());
for (const [dateStr, notes2] of Object.entries(dataMap)) {
const month = parseInt(dateStr.split("-")[1]) - 1;
if (month >= 0 && month < 12) {
for (const note of notes2) {
if (!uniqueNotesMap[month].has(note.path)) {
uniqueNotesMap[month].set(note.path, note);
}
}
}
}
const totals = uniqueNotesMap.map((m) => m.size);
const notes = uniqueNotesMap.map((m) => Array.from(m.values()));
const newCounts = notes.map(
(monthNotes, monthIndex) => monthNotes.filter((n) => {
if (!n.createdDate)
return false;
const m = parseInt(n.createdDate.split("-")[1]) - 1;
return m === monthIndex;
}).length
);
return { totals, notes, newCounts };
}
/**
* 计算月份标签的偏移量(像素)
*/
calcMonthOffsets(year) {
const yearStartForOffset = new Date(year, 0, 1);
const firstSunday = new Date(yearStartForOffset);
firstSunday.setDate(yearStartForOffset.getDate() - yearStartForOffset.getDay());
const offsets = [];
for (let m = 0; m < 12; m++) {
const firstDayOfMonth = new Date(year, m, 1);
const daysSinceFirstSunday = Math.floor(
(firstDayOfMonth.getTime() - firstSunday.getTime()) / (24 * 60 * 60 * 1e3)
);
const colIndex = Math.floor(daysSinceFirstSunday / 7);
offsets.push(colIndex * COLUMN_WIDTH);
}
return offsets;
}
/**
* 构建热力图网格方块
*/
buildCalendarGrid(calGrid, year, colors, dataMap) {
const yearStart = new Date(year, 0, 1);
const yearEnd = new Date(year, 11, 31);
const gridStart = new Date(yearStart);
gridStart.setDate(yearStart.getDate() - yearStart.getDay());
const gridEnd = new Date(yearEnd);
if (yearEnd.getDay() !== 6) {
gridEnd.setDate(yearEnd.getDate() + (6 - yearEnd.getDay()));
}
const toLocalDateString = (d) => {
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
};
let cur = new Date(gridStart);
while (cur <= gridEnd) {
const s = toLocalDateString(cur);
const isInYear = cur.getFullYear() === year;
const notes = isInYear ? dataMap[s] || [] : [];
let color = colors[0];
if (notes.length > 0)
color = colors[1];
if (notes.length > 3)
color = colors[2];
if (notes.length > 7)
color = colors[3];
if (notes.length > 12)
color = colors[4];
const newCount = notes.filter((n) => n.createdDate === s).length;
let titleText;
if (isInYear) {
if (notes.length > 0) {
titleText = `${s}: ${t("view.tooltip.edits", { count: notes.length })}, ${t("view.tooltip.new", { count: newCount })}`;
} else {
titleText = `${s}: ${t("view.tooltip.noEdits")}`;
}
} else {
titleText = `${s}: ${t("view.tooltip.outOfYear", { year: String(year) })}`;
}
const box = calGrid.createDiv({ cls: "h-box" });
box.setCssStyles({ backgroundColor: isInYear ? color : colors[0] });
if (!isInYear) {
box.addClass("out-of-year");
}
box.dataset.date = s;
box.title = titleText;
cur.setDate(cur.getDate() + 1);
}
}
/**
* 显示日期面板(点击方块后的详情)
*/
displayDayPanel(resPanel, dateStr, notes, dataMap, resetPanel) {
resPanel.empty();
resPanel.scrollTop = 0;
const newCount = notes.filter((n) => n.createdDate === dateStr).length;
const [y, m, d] = dateStr.split("-");
this.renderPanelHeader(resPanel, dateStr, notes.length, newCount, resetPanel, "day", parseInt(y), m, d);
if (notes.length === 0) {
this.renderEmptyMessage(resPanel, t("resultPanel.noEdits"));
return;
}
const ul = resPanel.createEl("ul", { cls: "note-heatmap-list" });
const uniqueNotes = this.deduplicateAndSortNotes(notes);
this.renderSimpleNoteList(ul, uniqueNotes, dateStr);
}
/** 去重并排序笔记 */
deduplicateAndSortNotes(notes) {
const seen = /* @__PURE__ */ new Set();
return notes.filter((n) => {
if (seen.has(n.path))
return false;
seen.add(n.path);
return true;
}).sort((a, b) => b.modifiedTime - a.modifiedTime);
}
/** 渲染简化笔记列表(日期面板用) */
renderSimpleNoteList(ul, notes, dateStr) {
ul.empty();
notes.forEach((n) => {
const li = ul.createEl("li", { cls: "res-item note-heatmap-list-item" });
const leftDiv = li.createDiv({ cls: "note-heatmap-list-item-left" });
const isNew = n.createdDate === dateStr;
const displayName = isNew ? `\u{1F195} ${n.name}` : n.name;
const a = this.createInternalLink(displayName, n.path);
leftDiv.appendChild(a);
const timeSpan = leftDiv.createEl("span", { cls: "note-heatmap-time" });
const date = new Date(n.modifiedTime);
timeSpan.textContent = date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
if (this.plugin.settings.enableGitDiff) {
const vhdBtn = li.createEl("button", { cls: "diff-btn note-heatmap-vhd-btn", text: t("resultPanel.vhdButton") });
vhdBtn.addEventListener("click", (e) => {
e.stopPropagation();
void this.openInVHD(n, dateStr);
});
}
});
}
/**
* 打开周期笔记(年度/月度/每日)
*/
async openPeriodicNote(type, year, month, day) {
let folder;
let format;
switch (type) {
case "yearly":
folder = this.plugin.settings.yearlyNoteFolder;
format = this.plugin.settings.yearlyNoteFormat;
break;
case "monthly":
folder = this.plugin.settings.monthlyNoteFolder;
format = this.plugin.settings.monthlyNoteFormat;
break;
case "daily":
folder = this.plugin.settings.dailyNoteFolder;
format = this.plugin.settings.dailyNoteFormat;
break;
}
const yearStr = String(year);
const monthStr = month ? String(month).padStart(2, "0") : "01";
const dayStr = day ? String(day).padStart(2, "0") : "01";
let filePath = this.formatDatePath(format, yearStr, monthStr, dayStr);
if (folder) {
filePath = `${folder}/${filePath}`;
}
if (!filePath.endsWith(".md")) {
filePath += ".md";
}
let file = this.app.vault.getAbstractFileByPath(filePath);
if (!file) {
try {
file = await this.app.vault.create(filePath, "");
} catch (err) {
console.error(`[NoteHeatmap] Failed to create ${type} note:`, err);
new import_obsidian.Notice(t("view.periodicNote.createFailed", { type }));
return;
}
}
if (file instanceof import_obsidian.TFile) {
await this.app.workspace.getLeaf().openFile(file);
}
}
};
// src/settingTab.ts
var import_obsidian2 = require("obsidian");
function createLink(text, href) {
const a = document.createElement("a");
a.textContent = text;
a.href = href;
a.target = "_blank";
a.rel = "noopener";
return a;
}
var HeatmapSettingTab = class extends import_obsidian2.PluginSettingTab {
constructor(app, plugin) {
super(app, plugin);
this.plugin = plugin;
}
display() {
const { containerEl } = this;
containerEl.empty();
new import_obsidian2.Setting(containerEl).setName(t("settings.title")).setHeading();
this.renderBasicSettings(containerEl);
this.renderColorSettings(containerEl);
this.renderPeriodicNoteSettings(containerEl);
this.renderGitDiffSettings(containerEl);
this.renderResetButton(containerEl);
}
/** 基础设置 */
renderBasicSettings(containerEl) {
new import_obsidian2.Setting(containerEl).setName(t("settings.targetField.name")).setDesc(t("settings.targetField.desc")).addText(
(text) => text.setPlaceholder("Last modified").setValue(this.plugin.settings.targetField).onChange(async (value) => {
this.plugin.settings.targetField = value.trim();
await this.plugin.saveSettings();
})
);
new import_obsidian2.Setting(containerEl).setName(t("settings.createdField.name")).setDesc(t("settings.createdField.desc")).addText(
(text) => text.setPlaceholder("Created").setValue(this.plugin.settings.createdField).onChange(async (value) => {
this.plugin.settings.createdField = value.trim();
await this.plugin.saveSettings();
this.plugin.dataCache.updateSettings(this.plugin.settings.targetField, this.plugin.settings.targetFolder);
})
);
new import_obsidian2.Setting(containerEl).setName(t("settings.targetFolder.name")).setDesc(t("settings.targetFolder.desc")).addText(
(text) => text.setPlaceholder("").setValue(this.plugin.settings.targetFolder).onChange(async (value) => {
this.plugin.settings.targetFolder = value.trim().replace(/\/$/, "");
await this.plugin.saveSettings();
})
);
}
/** 颜色设置 */
renderColorSettings(containerEl) {
new import_obsidian2.Setting(containerEl).setName(t("colors.title")).setHeading();
containerEl.createEl("p", {
text: t("colors.desc"),
cls: "setting-item-description"
});
const colorLabels = tArray("colors.labels");
this.plugin.settings.colors.forEach((color, index) => {
new import_obsidian2.Setting(containerEl).setName(`${t("colors.title")} ${index + 1}\uFF1A${colorLabels[index]}`).addColorPicker(
(picker) => picker.setValue(color).onChange(async (value) => {
this.plugin.settings.colors[index] = value;
await this.plugin.saveSettings();
})
);
});
}
/** 周期笔记设置 */
renderPeriodicNoteSettings(containerEl) {
new import_obsidian2.Setting(containerEl).setName(t("periodicNotes.title")).setHeading();
this.injectStyles();
const periodicTypes = ["daily", "monthly", "yearly"];
for (const type of periodicTypes) {
this.renderPeriodicNoteGroup(containerEl, type);
}
}
/** 渲染单个周期笔记组 */
renderPeriodicNoteGroup(containerEl, type) {
const enableKey = `enable${this.capitalize(type)}Note`;
const folderKey = `${type}NoteFolder`;
const formatKey = `${type}NoteFormat`;
const isEnabled = this.plugin.settings[enableKey];
new import_obsidian2.Setting(containerEl).setName(t(`periodicNotes.${type}.name`)).setDesc(t(`periodicNotes.${type}.desc`)).setClass("heatmap-setting-group-header").addToggle(
(toggle) => toggle.setValue(isEnabled).onChange(async (value) => {
this.plugin.settings[enableKey] = value;
await this.plugin.saveSettings();
this.display();
})
);
if (isEnabled) {
this.renderTextSetting(
containerEl,
`periodicNotes.${type}.folder`,
this.plugin.settings[folderKey],
(value) => {
this.plugin.settings[folderKey] = value.trim().replace(/\/$/, "");
}
);
this.renderTextSetting(
containerEl,
`periodicNotes.${type}.format`,
this.plugin.settings[formatKey],
(value) => {
this.plugin.settings[formatKey] = value.trim().replace(/^\//, "");
}
);
}
}
/** 渲染文本设置项 */
renderTextSetting(containerEl, key, value, onChange) {
new import_obsidian2.Setting(containerEl).setName(t(`${key}.name`)).setDesc(t(`${key}.desc`)).setClass("heatmap-setting-subitem").addText(
(text) => text.setPlaceholder(t(`${key}.placeholder`)).setValue(value).onChange(async (newValue) => {
onChange(newValue);
await this.plugin.saveSettings();
})
);
}
/** 注入样式 - 已迁移到 styles.css */
injectStyles() {
}
/** 首字母大写 */
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
/** Git Diff 设置 */
renderGitDiffSettings(containerEl) {
var _a, _b, _c, _d;
new import_obsidian2.Setting(containerEl).setName(t("gitDiff.title")).setHeading();
const gitAvailable = (_b = (_a = this.plugin.gitService) == null ? void 0 : _a.isGitPluginAvailable()) != null ? _b : false;
const vhdAvailable = !!((_d = (_c = this.app.plugins) == null ? void 0 : _c.plugins) == null ? void 0 : _d["obsidian-version-history-diff"]);
const allDependenciesMet = gitAvailable && vhdAvailable;
const gitDiffDesc = this.buildGitDiffDesc(gitAvailable, vhdAvailable, allDependenciesMet);
new import_obsidian2.Setting(containerEl).setName(t("gitDiff.enable.name")).setDesc(gitDiffDesc).addToggle(
(toggle) => toggle.setValue(allDependenciesMet && this.plugin.settings.enableGitDiff).setDisabled(!allDependenciesMet).onChange(async (value) => {
if (value && !allDependenciesMet) {
toggle.setValue(false);
return;
}
this.plugin.settings.enableGitDiff = value;
await this.plugin.saveSettings();
})
);
}
/** 构建 Git Diff 描述 */
buildGitDiffDesc(gitAvailable, vhdAvailable, allMet) {
const frag = document.createDocumentFragment();
if (allMet) {
frag.append(t("gitDiff.enable.descAllAvailable"));
} else if (!gitAvailable && !vhdAvailable) {
frag.append(t("gitDiff.enable.descMissingBoth"), " ");
frag.append(createLink(t("gitDiff.enable.pluginObsidianGit"), "https://github.com/Vinzent03/obsidian-git"));
frag.append(t("gitDiff.enable.separator"));
frag.append(createLink(t("gitDiff.enable.pluginVHD"), "https://github.com/kometenstaub/obsidian-version-history-diff"));
} else if (!gitAvailable) {
frag.append(t("gitDiff.enable.descMissingObsidianGit"), " ");
frag.append(createLink(t("gitDiff.enable.pluginObsidianGit"), "https://github.com/Vinzent03/obsidian-git"));
} else {
frag.append(t("gitDiff.enable.descMissingVHD"), " ");
frag.append(createLink(t("gitDiff.enable.pluginVHD"), "https://github.com/kometenstaub/obsidian-version-history-diff"));
}
return frag;
}
/** 重置按钮 */
renderResetButton(containerEl) {
containerEl.createEl("hr");
new import_obsidian2.Setting(containerEl).setName(t("reset.name")).addButton(
(btn) => btn.setButtonText(t("reset.button")).setWarning().onClick(async () => {
const { DEFAULT_SETTINGS: DEFAULT_SETTINGS2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
Object.assign(this.plugin.settings, DEFAULT_SETTINGS2);
await this.plugin.saveSettings();
this.display();
})
);
}
};
// src/main.ts
init_settings();
// src/dataCache.ts
var import_obsidian3 = require("obsidian");
function parseDateString(val) {
if (!val)
return null;
if (typeof val === "object" && val !== null) {
const anyVal = val;
if (typeof anyVal["toISODate"] === "function") {
return anyVal["toISODate"]();
}
if (typeof anyVal["toISOString"] === "function") {
return anyVal["toISOString"]().split("T")[0];
}
return null;
}
if (typeof val !== "string" && typeof val !== "number")
return null;
const str = String(val);
const isoMatch = str.match(/^(\d{4}-\d{2}-\d{2})/);
if (isoMatch)
return isoMatch[1];
return null;
}
var DataCache = class extends import_obsidian3.Component {
constructor(plugin) {
super();
this.cache = {};
this.isInitialized = false;
// 等待初始化的 Promise
this.initPromise = null;
// 防抖定时器
this.updateTimeout = null;
// 初始化取消标志(用于 forceRefresh 时中断正在进行的初始化)
this.initCancelled = false;
this.plugin = plugin;
this.targetField = plugin.settings.targetField;
this.targetFolder = plugin.settings.targetFolder;
}
/**
* 初始化缓存 - 全量扫描(仅在插件加载时执行一次)
*/
async initialize() {
if (this.isInitialized)
return;
if (this.initPromise)
return this.initPromise;
this.initPromise = this.doInitialize();
return this.initPromise;
}
async doInitialize() {
this.initCancelled = false;
try {
this.cache = {};
const files = this.getTargetFiles();
const batchSize = 100;
for (let i = 0; i < files.length; i += batchSize) {
if (this.initCancelled) {
console.debug("[NoteHeatmap] \u7F13\u5B58\u521D\u59CB\u5316\u88AB\u53D6\u6D88");
return;
}
const batch = files.slice(i, i + batchSize);
for (const file of batch) {
this.processFile(file);
}
if (i + batchSize < files.length) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
if (this.initCancelled) {
console.debug("[NoteHeatmap] \u7F13\u5B58\u521D\u59CB\u5316\u88AB\u53D6\u6D88");
return;
}
this.isInitialized = true;
this.registerEventListeners();
this.plugin.refreshViews();
} catch (err) {
console.error("[NoteHeatmap] \u7F13\u5B58\u521D\u59CB\u5316\u51FA\u9519:", err);
this.isInitialized = true;
throw err;
}
}
/**
* 注册文件变化事件监听
*/
registerEventListeners() {
const metadataCache = this.plugin.app.metadataCache;
const vault = this.plugin.app.vault;
this.registerEvent(
metadataCache.on("changed", (file) => {
if (file instanceof import_obsidian3.TFile && this.isTargetFile(file)) {
this.handleFileChanged(file);
}
})
);
this.registerEvent(
vault.on("create", (file) => {
if (file instanceof import_obsidian3.TFile && this.isTargetFile(file)) {
this.handleFileCreated(file);
}
})
);
this.registerEvent(
vault.on("delete", (file) => {
if (file instanceof import_obsidian3.TFile && this.isTargetFile(file)) {
this.handleFileDeleted(file);
}
})
);
this.registerEvent(
vault.on("rename", (file, oldPath) => {
if (file instanceof import_obsidian3.TFile && this.isTargetFile(file)) {
this.handleFileRenamed(file, oldPath);
}
})
);
}
/**
* 获取目标文件夹内的所有 Markdown 文件
* targetFolder 为空字符串时表示整个 vault
*/
getTargetFiles() {
if (!this.targetFolder) {
return this.plugin.app.vault.getMarkdownFiles();
}
return this.plugin.app.vault.getMarkdownFiles().filter(
(f) => f.path.startsWith(this.targetFolder + "/") || f.path === this.targetFolder
);
}
/**
* 判断文件是否在目标文件夹内
* targetFolder 为空字符串时表示整个 vault
*/
isTargetFile(file) {
if (!this.targetFolder) {
return true;
}
return file.path.startsWith(this.targetFolder + "/") || file.path === this.targetFolder;
}
/**
* 处理单个文件,提取日期数据
*/
processFile(file) {
const cache = this.plugin.app.metadataCache.getFileCache(file);
const fm = cache == null ? void 0 : cache.frontmatter;
if (!fm)
return;
const rawDates = fm[this.targetField];
if (!rawDates)
return;
let dates;
if (typeof rawDates === "string") {
dates = [rawDates];
} else if (Array.isArray(rawDates) && rawDates.every((item) => typeof item === "string")) {
dates = rawDates;
} else {
return;
}
let createdDate = null;
const createdField = this.plugin.settings.createdField;
if (fm[createdField]) {
createdDate = parseDateString(fm[createdField]);
}
if (!createdDate) {
createdDate = new Date(file.stat.ctime).toISOString().split("T")[0];
}
for (const d of dates) {
if (!d)
continue;
let dateStr;
let modifiedTime;
const isoMatch = d.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (!isoMatch)
continue;
const [, year, month, day] = isoMatch;
dateStr = `${year}-${month}-${day}`;
const dateObj = new Date(d);
if (isNaN(dateObj.getTime()))
continue;
modifiedTime = dateObj.getTime();
if (!this.cache[year]) {
this.cache[year] = {};
}
if (!this.cache[year][dateStr]) {
this.cache[year][dateStr] = [];
}
const existingIndex = this.cache[year][dateStr].findIndex(
(entry) => entry.path === file.path
);
if (existingIndex !== -1) {
if (modifiedTime > this.cache[year][dateStr][existingIndex].modifiedTime) {
this.cache[year][dateStr][existingIndex] = {
path: file.path,
name: file.basename,
createdDate,
modifiedTime
};
}
} else {
this.cache[year][dateStr].push({
path: file.path,
name: file.basename,
createdDate,
modifiedTime
});
}
}
}
/**
* 从缓存中移除文件的所有记录
*/
removeFileFromCache(filePath) {
for (const year in this.cache) {
for (const dateStr in this.cache[year]) {
this.cache[year][dateStr] = this.cache[year][dateStr].filter(
(n) => n.path !== filePath
);
if (this.cache[year][dateStr].length === 0) {
delete this.cache[year][dateStr];
}
}
if (Object.keys(this.cache[year]).length === 0) {
delete this.cache[year];
}
}
}
/**
* 处理文件修改
*/
handleFileChanged(file) {
this.removeFileFromCache(file.path);
this.processFile(file);
this.notifyUpdate();
}
handleFileCreated(file) {
this.processFile(file);
this.notifyUpdate();
}
handleFileDeleted(file) {
this.removeFileFromCache(file.path);
this.notifyUpdate();
}
handleFileRenamed(file, oldPath) {
this.removeFileFromCache(oldPath);
this.processFile(file);
this.notifyUpdate();
}
/**
* 通知所有热力图视图更新
*/
notifyUpdate() {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
}
this.updateTimeout = window.setTimeout(() => {
this.plugin.refreshViews();
}, 300);
}
/**
* 获取指定年份的数据
*/
getYearData(year) {
return this.cache[String(year)] || {};
}
/**
* 检查缓存是否已初始化
*/
isReady() {
return this.isInitialized;
}
/**
* 等待缓存初始化完成
*/
async waitForReady() {
if (this.isInitialized)
return;
await this.initialize();
}
/**
* 清理资源(在重新初始化前调用)
*/
cleanup() {
if (this.updateTimeout) {
clearTimeout(this.updateTimeout);
this.updateTimeout = null;
}
this.initCancelled = true;
if (this.isInitialized) {
this.unload();
this.load();
}
}
/**
* 更新设置(当用户在设置面板修改时调用)
*/
updateSettings(targetField, targetFolder) {
if (this.targetField !== targetField || this.targetFolder !== targetFolder) {
this.targetField = targetField;
this.targetFolder = targetFolder;
this.cleanup();
this.cache = {};
this.isInitialized = false;
this.initPromise = null;
void this.initialize();
}
}
/**
* 强制刷新缓存
*/
async forceRefresh() {
this.cleanup();
this.cache = {};
this.isInitialized = false;
this.initPromise = null;
await this.initialize();
}
};
// src/gitService.ts
var GitService = class {
constructor(plugin) {
this.plugin = plugin;
this.gitPlugin = null;
this.findGitPlugin();
}
/**
* 查找 Obsidian Git 插件
*/
findGitPlugin() {
try {
this.gitPlugin = this.plugin.app.plugins.getPlugin("obsidian-git");
} catch (e) {
}
}
/**
* 检查 Git 功能是否可用
*/
isGitPluginAvailable() {
if (!this.gitPlugin) {
this.findGitPlugin();
}
return this.gitPlugin != null;
}
/**
* 获取 gitManager 实例
* 提取公共模式:获取 + 空值检查
*/
getGitManager() {
var _a;
if (!this.isGitPluginAvailable())
return null;
return ((_a = this.gitPlugin) == null ? void 0 : _a.gitManager) || null;
}
/**
* 确定 diff 的起始 hash
* 有 previousCommit 时用它,否则取第一个提交的父提交
*/
resolveFromHash(commits, previousCommit) {
if (previousCommit) {
return previousCommit.hash;
}
const parentHash = this.findParentCommitFromKnown(commits, commits[0].hash);
return parentHash || commits[0].hash;
}
/**
* 获取某文件在某日的变更统计
* @returns DayDiffResult - 有插件时的结果(可能为空提交)
* @returns null - 没有安装 Git 插件
*/
async getDayDiff(file, date) {
const gitManager = this.getGitManager();
if (!gitManager) {
return this.isGitPluginAvailable() ? { additions: 0, deletions: 0, diffText: "", commits: [] } : null;
}
try {
const [y, m, d] = date.split("-").map(Number);
const startTime = new Date(y, m - 1, d, 0, 0, 0);
const endTime = new Date(y, m - 1, d, 23, 59, 59);
const { commits, previousCommit } = await this.getCommitsInRange(gitManager, file, startTime, endTime);
if (commits.length === 0) {
return { additions: 0, deletions: 0, diffText: "", commits: [], previousCommit };
}
const fromHash = this.resolveFromHash(commits, previousCommit);
const toHash = commits[commits.length - 1].hash;
const diffText = await this.getDiffBetweenCommits(
gitManager,
file,
fromHash,
toHash,
commits
);
const stats = this.parseDiffStats(diffText);
return {
additions: stats.additions,
deletions: stats.deletions,
diffText,
commits,
previousCommit
};
} catch (err) {
console.error("[NoteHeatmap] \u83B7\u53D6 Git diff \u5931\u8D25:", err);
return { additions: 0, deletions: 0, diffText: "", commits: [] };
}
}
/**
* 获取指定时间范围内的提交,同时返回范围之前的最后一个提交
* 合并了 getDayCommits/getMonthCommits + getPreviousCommit/getMonthPreviousCommit 的逻辑
* 一次 log 查询同时获取两个结果,避免重复 API 调用
*/
async getCommitsInRange(gitManager, file, startTime, endTime) {
try {
const logEntries = await gitManager.log(file.path);
if (!logEntries || logEntries.length === 0) {
return { commits: [], previousCommit: void 0 };
}
const startMs = startTime.getTime();
const endMs = endTime.getTime();
const commits = [];
let previousCommit;
for (const entry of logEntries) {
const commitDate = new Date(entry.date);
const commitTime = commitDate.getTime();
if (commitTime >= startMs && commitTime <= endMs) {
commits.push({
hash: entry.hash,
time: commitTime,
message: entry.message || "\u65E0\u63D0\u4EA4\u4FE1\u606F",
fileName: entry.fileName
});
} else if (commitTime < startMs && !previousCommit) {
previousCommit = {
hash: entry.hash,
time: commitTime,
message: entry.message || "\u65E0\u63D0\u4EA4\u4FE1\u606F",
fileName: entry.fileName || file.path
};
}
}
commits.sort((a, b) => a.time - b.time);
return { commits, previousCommit };
} catch (err) {
console.error("[NoteHeatmap] \u83B7\u53D6\u63D0\u4EA4\u5386\u53F2\u5931\u8D25:", err);
return { commits: [], previousCommit: void 0 };
}
}
/**
* 获取两个提交之间的 diff
* 使用 gitManager.show() 与 Version History Diff 保持一致
* 支持文件重命名/移动场景
*/
async getDiffBetweenCommits(gitManager, file, fromHash, toHash, knownCommits) {
try {
const findFileName = (hash) => {
const commit = knownCommits.find((c) => c.hash === hash);
return (commit == null ? void 0 : commit.fileName) || null;
};
const fromFileName = findFileName(fromHash) || file.path;
const toFileName = findFileName(toHash) || file.path;
let fromContent;
let toContent;
if (fromHash === toHash) {
const parentHash = this.findParentCommitFromKnown(knownCommits, fromHash);
if (parentHash) {
const parentFileName = findFileName(parentHash) || file.path;
fromContent = await this.tryShow(gitManager, parentHash, parentFileName, file.path);
} else {
fromContent = "";
}
toContent = await this.tryShow(gitManager, toHash, toFileName, file.path);
} else {
fromContent = await this.tryShow(gitManager, fromHash, fromFileName, file.path);
toContent = await this.tryShow(gitManager, toHash, toFileName, file.path);
}
return this.generateUnifiedDiff(fromContent, toContent, file.path, fromHash, toHash);
} catch (err) {
console.error("[NoteHeatmap] \u83B7\u53D6 diff \u5931\u8D25:", err);
return "";
}
}
/**
* 从已知的 commits 列表中查找指定提交的父提交 hash
* commits 按时间正序排列,父提交是列表中当前提交的前一个
*/
findParentCommitFromKnown(commits, hash) {
const index = commits.findIndex((c) => c.hash === hash);
if (index > 0) {
return commits[index - 1].hash;
}
return null;
}
/**
* 尝试用不同路径获取文件内容
*/
async tryShow(gitManager, hash, preferredPath, fallbackPath) {
const pathsToTry = preferredPath ? [preferredPath, fallbackPath] : [fallbackPath];
for (const path of pathsToTry) {
try {
return await gitManager.show(hash, path);
} catch (e) {
}
}
return "";
}
/**
* 生成统一格式的 diff
*/
generateUnifiedDiff(fromContent, toContent, filePath, fromHash, toHash) {
var _a, _b;
const fromLines = fromContent.split("\n");
const toLines = toContent.split("\n");
let diff = `diff --git a/${filePath} b/${filePath}
`;
diff += `index ${fromHash.substring(0, 7)}..${toHash.substring(0, 7)} 100644
`;
diff += `--- a/${filePath}
`;
diff += `+++ b/${filePath}
`;
const maxLines = Math.max(fromLines.length, toLines.length);
let hunkStarted = false;
let hunkFromStart = 1;
let hunkToStart = 1;
let hunkLines = [];
for (let i = 0; i < maxLines; i++) {
const fromLine = (_a = fromLines[i]) != null ? _a : null;
const toLine = (_b = toLines[i]) != null ? _b : null;
if (fromLine !== toLine) {
if (!hunkStarted) {
hunkFromStart = i + 1;
hunkToStart = i + 1;
hunkStarted = true;
}
if (fromLine !== null && toLine !== null) {
hunkLines.push("-" + fromLine);
hunkLines.push("+" + toLine);
} else if (fromLine !== null) {
hunkLines.push("-" + fromLine);
} else {
hunkLines.push("+" + toLine);
}
} else {
if (hunkStarted && hunkLines.length > 0) {
const fromCount = hunkLines.filter((l) => l.startsWith("-")).length;
const toCount = hunkLines.filter((l) => l.startsWith("+")).length;
diff += `@@ -${hunkFromStart},${fromCount} +${hunkToStart},${toCount} @@
`;
diff += hunkLines.join("\n") + "\n";
hunkLines = [];
}
hunkStarted = false;
}
}
if (hunkStarted && hunkLines.length > 0) {
const fromCount = hunkLines.filter((l) => l.startsWith("-")).length;
const toCount = hunkLines.filter((l) => l.startsWith("+")).length;
diff += `@@ -${hunkFromStart},${fromCount} +${hunkToStart},${toCount} @@
`;
diff += hunkLines.join("\n") + "\n";
}
return diff;
}
/**
* 解析 diff 统计行数
*/
parseDiffStats(diffText) {
let additions = 0;
let deletions = 0;
const lines = diffText.split("\n");
for (const line of lines) {
if (line.startsWith("+") && !line.startsWith("+++")) {
additions++;
} else if (line.startsWith("-") && !line.startsWith("---")) {
deletions++;
}
}
return { additions, deletions };
}
/**
* 获取某文件在某月的变更统计(整个月的所有提交)
* @param yearMonth - 格式 "2026-02"
* @returns DayDiffResult - 有插件时的结果(可能为空提交)
* @returns null - 没有安装 Git 插件
*/
async getMonthDiff(file, yearMonth) {
const gitManager = this.getGitManager();
if (!gitManager) {
return this.isGitPluginAvailable() ? { additions: 0, deletions: 0, diffText: "", commits: [] } : null;
}
try {
const [year, month] = yearMonth.split("-").map(Number);
const startTime = new Date(year, month - 1, 1, 0, 0, 0);
const lastDay = new Date(year, month, 0).getDate();
const endTime = new Date(year, month - 1, lastDay, 23, 59, 59);
const { commits, previousCommit } = await this.getCommitsInRange(gitManager, file, startTime, endTime);
if (commits.length === 0) {
return { additions: 0, deletions: 0, diffText: "", commits: [], previousCommit };
}
const fromHash = this.resolveFromHash(commits, previousCommit);
const toHash = commits[commits.length - 1].hash;
const diffText = await this.getDiffBetweenCommits(
gitManager,
file,
fromHash,
toHash,
commits
);
const stats = this.parseDiffStats(diffText);
return {
additions: stats.additions,
deletions: stats.deletions,
diffText,
commits,
previousCommit
};
} catch (err) {
console.error("[NoteHeatmap] \u83B7\u53D6\u6708\u5EA6 Git diff \u5931\u8D25:", err);
return { additions: 0, deletions: 0, diffText: "", commits: [] };
}
}
};
// src/main.ts
var NoteHeatmapPlugin = class extends import_obsidian4.Plugin {
async onload() {
await this.loadSettings();
this.dataCache = new DataCache(this);
this.addChild(this.dataCache);
this.gitService = new GitService(this);
this.app.workspace.onLayoutReady(() => {
void this.checkAndAutoEnableGitDiff();
void this.dataCache.initialize().catch((err) => {
console.error("[NoteHeatmap] \u7F13\u5B58\u521D\u59CB\u5316\u5931\u8D25:", err);
});
});
this.registerView(
HEATMAP_VIEW_TYPE,
(leaf) => new HeatmapView(leaf, this)
);
this.addRibbonIcon("calendar-with-checkmark", "Note heatmap", () => {
void this.activateView();
});
this.addCommand({
id: "open",
name: t("commands.openView"),
callback: () => {
void this.activateView();
}
});
this.addCommand({
id: "refresh",
name: t("commands.refreshView"),
callback: () => {
this.refreshViews();
}
});
this.addSettingTab(new HeatmapSettingTab(this.app, this));
}
onunload() {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
if (this.dataCache) {
this.dataCache.updateSettings(
this.settings.targetField,
this.settings.targetFolder
);
}
this.refreshViews();
}
async activateView() {
const { workspace } = this.app;
let leaf = null;
const leaves = workspace.getLeavesOfType(HEATMAP_VIEW_TYPE);
if (leaves.length > 0) {
leaf = leaves[0];
} else {
leaf = workspace.getLeaf("tab");
await leaf.setViewState({ type: HEATMAP_VIEW_TYPE, active: true });
}
await workspace.revealLeaf(leaf);
}
/**
* 检查指定插件是否已安装并启用
*/
isPluginInstalled(pluginId) {
try {
const plugin = this.app.plugins.plugins[pluginId];
return plugin != null;
} catch (e) {
return false;
}
}
/**
* 检测依赖插件,如果都安装了则自动开启 git diff
* 需要在 onLayoutReady 后调用,确保其他插件已加载
*/
async checkAndAutoEnableGitDiff() {
if (this.settings.enableGitDiff) {
return;
}
const hasGitPlugin = this.isPluginInstalled("obsidian-git");
const hasVHDPlugin = this.isPluginInstalled("obsidian-version-history-diff");
if (hasGitPlugin && hasVHDPlugin) {
console.debug("[NoteHeatmap] \u68C0\u6D4B\u5230\u4F9D\u8D56\u63D2\u4EF6\u5DF2\u5B89\u88C5\uFF0C\u81EA\u52A8\u5F00\u542F Git Diff \u529F\u80FD");
this.settings.enableGitDiff = true;
await this.saveSettings();
}
}
/**
* 刷新所有热力图视图
*/
refreshViews() {
const leaves = this.app.workspace.getLeavesOfType(HEATMAP_VIEW_TYPE);
for (const leaf of leaves) {
const view = leaf.view;
if (view instanceof HeatmapView) {
view.render();
}
}
}
/**
* 强制刷新缓存并更新视图
*/
async forceRefresh() {
if (this.dataCache) {
await this.dataCache.forceRefresh();
this.refreshViews();
}
}
};