mirror of
https://github.com/hzlume/obsidian-note-heatmap.git
synced 2026-07-22 06:06:30 +00:00
2072 lines
72 KiB
JavaScript
2072 lines
72 KiB
JavaScript
/*
|
||
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();
|
||
}
|
||
}
|
||
};
|