tazihad_obsidian-easy-keep-.../main.ts
2025-04-11 00:39:09 +06:00

425 lines
12 KiB
TypeScript
Raw 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.

import {
App,
Plugin,
WorkspaceLeaf,
ItemView,
TFile,
PluginSettingTab,
Setting,
} from "obsidian";
import "./styles.css";
const VIEW_TYPE_EASY_KEEP = "easy-keep-view";
interface NoteEntry {
path: string;
title: string;
excerpt: string;
time: number;
// Store the raw image embed (e.g. "Antora-Pramanik.jpg" or "testfolder/Antora-Pramanik.jpg")
imageLink?: string;
}
interface MyPluginSettings {
mySetting: string;
notesDB: NoteEntry[];
themeMode: "system" | "light" | "dark";
}
const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: "default",
notesDB: [],
themeMode: "system",
};
// Fallback helper: search vault for an image file by matching the basename (ignoring extension)
function resolveImageByName(app: App, imageName: string): TFile | null {
const target = imageName.replace(/\.(jpg|jpeg|png|webp)$/i, "").toLowerCase();
const candidates = app.vault.getFiles().filter(f => /\.(jpg|jpeg|png|webp)$/i.test(f.name));
for (const file of candidates) {
if (file.basename.toLowerCase() === target) {
return file;
}
}
return null;
}
// Simple debounce helper
function debounce(fn: (...args: any[]) => void, delay = 300): (...args: any[]) => void {
let timer: NodeJS.Timeout;
return (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
class EasyKeepView extends ItemView {
plugin: MyPlugin;
private mainContainer: HTMLElement;
constructor(leaf: WorkspaceLeaf, plugin: MyPlugin) {
super(leaf);
this.plugin = plugin;
}
getViewType(): string {
return VIEW_TYPE_EASY_KEEP;
}
getDisplayText(): string {
return "Easy Keep View";
}
async buildContent() {
this.mainContainer.empty();
this.mainContainer.style.overflowY = "auto";
this.mainContainer.style.height = "100%";
const cardContainer = this.mainContainer.createDiv("easy-keep-cards-container");
// New Note Card
const newNoteCard = cardContainer.createDiv("easy-keep-card new-note-card");
newNoteCard.createEl("h3", { text: "+" });
newNoteCard.createEl("p", { text: "Add New Note" });
newNoteCard.onclick = () => this.plugin.createNewNote();
// Before building cards, refresh thumbnails from note content.
await this.plugin.refreshThumbnails();
let notes = this.plugin.settings.notesDB.slice()
.sort((a, b) => b.time - a.time)
.slice(0, 100);
if (notes.length) {
notes.forEach(note => {
const card = cardContainer.createDiv("easy-keep-card");
card.createEl("h3", { text: note.title });
if (note.imageLink) {
// Try resolving the image using the metadata cache relative to the note.
let file = this.app.metadataCache.getFirstLinkpathDest(note.imageLink, note.path);
if (!(file instanceof TFile)) {
// Fallback: search by name.
file = resolveImageByName(this.app, note.imageLink);
}
if (file instanceof TFile) {
const resourcePath = this.app.vault.getResourcePath(file);
const img = card.createEl("img", { cls: "easy-keep-thumbnail" });
img.src = resourcePath;
img.onerror = () => {
console.warn("[Easy Keep View] Failed to load thumbnail:", resourcePath);
img.remove();
if (note.excerpt) card.createEl("p", { text: note.excerpt });
};
img.onload = () => {
console.log("[Easy Keep View] Thumbnail loaded:", resourcePath);
};
} else if (note.excerpt) {
card.createEl("p", { text: note.excerpt });
}
} else {
if (note.excerpt) {
card.createEl("p", { text: note.excerpt });
}
}
card.onclick = () => this.plugin.openNoteInNewTab(note.path);
});
} else {
const noHistory = cardContainer.createDiv("no-history-message");
noHistory.setText("No notes, create a new one!");
}
}
async refreshContent() {
await this.buildContent();
}
async onOpen() {
this.mainContainer = this.containerEl;
// Wait until plugin settings are loaded
await this.plugin.loadSettings();
// Build content after settings are ready
await this.buildContent();
this.loadCSS();
}
loadCSS() {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `${this.plugin.manifest.dir}/assets/styles.css`;
document.head.appendChild(link);
}
async onClose() {}
}
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl("h2", { text: "Easy Keep View Plugin Settings" });
new Setting(containerEl)
.setName("Theme Mode")
.setDesc("Select a theme mode for the Easy Keep View")
.addDropdown(dropdown => {
dropdown.addOption("system", "System (adapt)");
dropdown.addOption("light", "Light");
dropdown.addOption("dark", "Dark");
dropdown.setValue(this.plugin.settings.themeMode);
dropdown.onChange(async (value: "system" | "light" | "dark") => {
this.plugin.settings.themeMode = value;
await this.plugin.saveSettings();
this.plugin.refreshEasyKeepViewIfOpen();
});
});
}
}
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
private refreshDebounced: () => void;
async onload() {
await this.loadSettings();
// Defer cleanDatabase until vault is ready
this.app.workspace.onLayoutReady(async () => {
await this.cleanDatabase();
this.refreshEasyKeepViewIfOpen();
});
this.registerView(VIEW_TYPE_EASY_KEEP, (leaf) => new EasyKeepView(leaf, this));
const ribbonIconEl = this.addRibbonIcon("dice", "Easy Keep View", () => {
this.activateEasyKeepView();
});
ribbonIconEl.addClass("my-plugin-ribbon-class");
// Refresh on file-open events.
this.registerEvent(this.app.workspace.on("file-open", async (file) => {
if (file) {
await this.addToDatabase(file);
this.refreshDebounced();
}
}));
// Refresh on file deletion.
this.registerEvent(this.app.vault.on("delete", async (file) => {
if (file instanceof TFile) {
await this.removeFromDatabase(file.path);
this.refreshDebounced();
}
}));
// Refresh on file rename for notes.
this.registerEvent(this.app.vault.on("rename", async (file, oldPath) => {
if (file instanceof TFile) {
const entryIndex = this.settings.notesDB.findIndex(e => e.path === oldPath);
if (entryIndex !== -1) {
this.settings.notesDB[entryIndex].path = file.path;
this.settings.notesDB[entryIndex].title = file.basename;
await this.saveSettings();
this.refreshDebounced();
}
}
}));
// Refresh on file rename for image files.
this.registerEvent(this.app.vault.on("rename", async (file, oldPath) => {
if (file instanceof TFile && /\.(jpg|jpeg|png|webp)$/i.test(file.path)) {
let updated = false;
this.settings.notesDB.forEach(entry => {
if (entry.imageLink === oldPath) {
entry.imageLink = file.path;
updated = true;
}
});
if (updated) {
await this.saveSettings();
this.refreshDebounced();
}
}
}));
// New: Listen for modify events to update thumbnail info when note content changes.
this.registerEvent(this.app.vault.on("modify", async (file) => {
if (file instanceof TFile) {
await this.addToDatabase(file);
this.refreshDebounced();
}
}));
// Initialize debounce for refresh calls.
this.refreshDebounced = debounce(() => this.refreshEasyKeepViewIfOpen(), 300);
this.addSettingTab(new SampleSettingTab(this.app, this));
}
onunload() {
this.app.workspace.detachLeavesOfType(VIEW_TYPE_EASY_KEEP);
}
async activateEasyKeepView() {
const existingLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EASY_KEEP);
if (existingLeaves.length > 0) {
this.app.workspace.revealLeaf(existingLeaves[0]);
return;
}
const leaf = this.app.workspace.getLeaf(true);
await leaf.setViewState({ type: VIEW_TYPE_EASY_KEEP, active: true });
this.app.workspace.revealLeaf(leaf);
}
async refreshEasyKeepViewIfOpen() {
const existingLeaves = this.app.workspace.getLeavesOfType(VIEW_TYPE_EASY_KEEP);
if (existingLeaves.length > 0) {
const view = existingLeaves[0].view as EasyKeepView;
await view.refreshContent();
}
}
/**
* Checks each notes first nonempty line for an image embed pattern.
* If found, update the note's imageLink in the database.
*/
async refreshThumbnails() {
for (const note of this.settings.notesDB) {
const file = this.app.vault.getAbstractFileByPath(note.path);
if (!(file instanceof TFile)) continue;
const content = await this.app.vault.cachedRead(file);
const firstNonEmptyLine = content.split("\n").find(line => line.trim() !== "");
if (firstNonEmptyLine) {
const match = firstNonEmptyLine.match(/!\[\[([^\]]+)\]\]/);
if (match) {
const newLink = match[1].trim();
if (note.imageLink !== newLink) {
note.imageLink = newLink;
}
}
}
}
await this.saveSettings();
}
/**
* Save note info (including raw image embed) to the database.
*/
async addToDatabase(file: TFile) {
const content = await this.app.vault.cachedRead(file);
const lines = content.trim().split("\n").filter(line => line.trim() !== "");
let excerpt = "";
let imageLink: string | undefined;
if (/\.(jpg|jpeg|png|webp)$/i.test(file.path)) {
imageLink = file.path;
} else {
const obsidianEmbedMatch = content.match(/!\[\[([^\]]+)\]\]/);
const markdownImageMatch = content.match(/!\[.*?\]\((.*?)\)/);
if (obsidianEmbedMatch || markdownImageMatch) {
imageLink = (obsidianEmbedMatch?.[1] || markdownImageMatch?.[1])?.trim();
}
}
if (!imageLink && lines.length > 0) {
excerpt = lines.slice(0, 2).join(" ");
if (lines.length > 2) excerpt = excerpt.trim() + " …";
}
const titleWithoutExt = file.name.replace(/\.(md|png|jpg|jpeg|webp)$/i, "");
const newEntry: NoteEntry = {
path: file.path,
title: titleWithoutExt,
excerpt,
time: Date.now(),
imageLink,
};
this.settings.notesDB = this.settings.notesDB.filter(entry => entry.path !== file.path);
this.settings.notesDB.unshift(newEntry);
await this.saveSettings();
}
async removeFromDatabase(filePath: string) {
const initialLength = this.settings.notesDB.length;
this.settings.notesDB = this.settings.notesDB.filter(entry => entry.path !== filePath);
if (this.settings.notesDB.length !== initialLength) {
await this.saveSettings();
}
}
async cleanDatabase() {
const files = this.app.vault.getMarkdownFiles();
const existingPaths = new Set(files.map(file => file.path));
this.settings.notesDB = this.settings.notesDB.filter(entry => existingPaths.has(entry.path));
await this.saveSettings();
}
async openNoteInNewTab(filePath: string) {
const file = this.app.vault.getAbstractFileByPath(filePath);
if (!(file instanceof TFile)) return;
let existingLeaf: WorkspaceLeaf | null = null;
this.app.workspace.iterateAllLeaves(leaf => {
if (leaf.view.getViewType() === "markdown" && (leaf.view as any).file?.path === filePath) {
existingLeaf = leaf;
}
});
if (existingLeaf) {
this.app.workspace.setActiveLeaf(existingLeaf, { focus: true });
} else {
const newLeaf = this.app.workspace.getLeaf(true);
await newLeaf.openFile(file);
this.app.workspace.setActiveLeaf(newLeaf, { focus: true });
}
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
generateUniqueUntitledName(): string {
const baseName = "Untitled";
const files = this.app.vault.getMarkdownFiles();
const existingNames = new Set(files.map(f => f.basename));
if (!existingNames.has(baseName)) {
return baseName;
}
let counter = 1;
while (existingNames.has(`${baseName} ${counter}`)) {
counter++;
}
return `${baseName} ${counter}`;
}
async createNewNote() {
const title = this.generateUniqueUntitledName();
const filePath = `${title}.md`;
// Create an empty file instead of including the title in the content
const file = await this.app.vault.create(filePath, "");
await this.addToDatabase(file);
this.refreshEasyKeepViewIfOpen();
const newLeaf = this.app.workspace.getLeaf(true);
await newLeaf.openFile(file);
this.app.workspace.setActiveLeaf(newLeaf, { focus: true });
}
}