fix: address all required linter issues from Obsidian community review

- Replace localStorage with app.saveLocalStorage/loadLocalStorage
- Replace window.confirm/prompt with custom Modal dialogs
- Remove inline <style> element; move all CSS to styles.css
- Replace all element.style.cssText with CSS class additions
- Replace element.style.* with CSS classes and setCssProps for dynamic values
- Fix command IDs to exclude plugin ID prefix
- Remove detachLeavesOfType from onunload
- Fix async methods/callbacks returning Promise where void expected
- Add void operator to unhandled promise calls
- Fix empty catch block
- Remove unused variable
- Fix type coercion in template literals
This commit is contained in:
Chenyan Wang 2026-05-03 19:49:14 +08:00
parent 3a0fd4a35a
commit eca4f285b1
3 changed files with 666 additions and 509 deletions

504
main.js
View file

@ -281,7 +281,7 @@ function detectLanguage() {
try {
const locale = window.moment?.locale?.() || "";
if (locale.toLowerCase().startsWith("zh")) return "zh";
} catch {
} catch (_e) {
}
return "en";
}
@ -372,23 +372,23 @@ function makeSelectSearchable(selectEl, placeholder) {
const host = selectEl?.parentElement;
if (!host || selectEl.dataset.searchable === "1") return;
selectEl.dataset.searchable = "1";
selectEl.style.display = "none";
selectEl.addClass("sfv-hidden");
const box = host.createEl("details");
box.style.cssText = "position:relative;display:block;z-index:1;";
box.addClass("sfv-sel-host");
const summary = box.createEl("summary");
summary.style.cssText = "list-style:none;cursor:pointer;min-height:30px;padding:5px 10px;border-radius:8px;border:1px solid var(--background-modifier-border);";
summary.addClass("sfv-sel-summary");
const summaryText = summary.createEl("span");
const panel = box.createEl("div");
panel.style.cssText = "position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:60;max-height:220px;overflow:auto;padding:8px;border-radius:10px;border:1px solid var(--background-modifier-border);background:var(--background-primary);";
panel.addClass("sfv-sel-panel");
const input = panel.createEl("input");
input.type = "search";
input.placeholder = placeholder;
input.style.cssText = "width:100%;height:28px;padding:2px 8px;border-radius:8px;border:1px solid var(--background-modifier-border);margin-bottom:6px;";
input.addClass("sfv-sel-input");
const list = panel.createEl("div");
const rows = [];
const optionTexts = Array.from(selectEl.options).map((opt) => opt.textContent || "");
const meter = document.createElement("span");
meter.style.cssText = "position:fixed;visibility:hidden;white-space:nowrap;font:var(--font-ui-medium);";
meter.classList.add("sfv-sel-meter");
document.body.appendChild(meter);
let maxTextWidth = 0;
for (const text of optionTexts) {
@ -398,16 +398,12 @@ function makeSelectSearchable(selectEl, placeholder) {
meter.remove();
const fixedWidth = Math.max(170, Math.ceil(maxTextWidth) + 44);
const clampedWidth = Math.min(fixedWidth, 340);
box.style.width = "100%";
box.style.maxWidth = `${clampedWidth}px`;
panel.style.minWidth = `${Math.min(clampedWidth, 260)}px`;
panel.style.maxWidth = `${clampedWidth}px`;
obsidian.setCssProps(box, { "--sfv-sel-max-width": `${clampedWidth}px` });
obsidian.setCssProps(panel, { "--sfv-sel-min-width": `${Math.min(clampedWidth, 260)}px`, "--sfv-sel-max-width": `${clampedWidth}px` });
for (const opt of Array.from(selectEl.options)) {
const row = list.createEl("button", { text: opt.textContent || "" });
row.type = "button";
row.style.cssText = "all:unset;display:block;width:100%;box-sizing:border-box;text-align:left;padding:5px 8px;cursor:pointer;border-radius:6px;";
row.addEventListener("mouseenter", () => row.style.background = "var(--background-modifier-hover)");
row.addEventListener("mouseleave", () => row.style.background = "transparent");
row.addClass("sfv-sel-row");
row.addEventListener("click", () => {
selectEl.value = opt.value;
selectEl.dispatchEvent(new Event("change"));
@ -418,22 +414,23 @@ function makeSelectSearchable(selectEl, placeholder) {
const refreshSummary = () => {
const selected = selectEl.selectedOptions[0];
summaryText.setText(selected?.textContent || "");
summaryText.style.cssText = "display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;";
summaryText.addClass("sfv-sel-summary-text");
};
input.addEventListener("input", () => {
const q = input.value.trim().toLowerCase();
for (const { text, row } of rows) {
row.style.display = !q || text.includes(q) ? "block" : "none";
if (!q || text.includes(q)) row.removeClass("sfv-hidden");
else row.addClass("sfv-hidden");
}
});
box.addEventListener("toggle", () => {
if (box.open) {
box.style.zIndex = "160";
box.addClass("sfv-elevated");
input.value = "";
for (const { row } of rows) row.style.display = "block";
for (const { row } of rows) row.removeClass("sfv-hidden");
window.setTimeout(() => input.focus(), 0);
} else {
box.style.zIndex = "1";
box.removeClass("sfv-elevated");
}
});
selectEl.addEventListener("change", refreshSummary);
@ -552,31 +549,35 @@ var SetupModal = class extends obsidian.Modal {
});
});
const action = contentEl.createEl("div");
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:10px;";
action.createEl("button", { text: this.plugin.t("preview") }).addEventListener("click", async () => {
action.addClass("sfv-setup-action");
action.createEl("button", { text: this.plugin.t("preview") }).addEventListener("click", () => {
this.plugin.setDraftProfile(this.draft);
await this.plugin.activateView();
void this.plugin.activateView();
this.hasPreviewedDraft = true;
});
const saveBtn = action.createEl("button", { text: this.plugin.t("saveAndOpen") });
saveBtn.addClass("mod-cta");
saveBtn.addEventListener("click", async () => {
await this.plugin.saveProfile(this.draft, true, false);
await this.plugin.activateView({ profile: this.draft });
this.skipCloseSavePrompt = true;
this.close();
saveBtn.addEventListener("click", () => {
void (async () => {
await this.plugin.saveProfile(this.draft, true, false);
await this.plugin.activateView({ profile: this.draft });
this.skipCloseSavePrompt = true;
this.close();
})();
});
}
onOpen() {
this.render();
}
async onClose() {
onClose() {
this.contentEl.empty();
if (this.skipCloseSavePrompt) return;
if (!this.hasPreviewedDraft) return;
if (!window.confirm(this.plugin.t("savePreviewOnClose"))) return;
await this.plugin.saveProfile(this.draft, true, false);
await this.plugin.activateView({ profile: this.draft });
if (this.skipCloseSavePrompt || !this.hasPreviewedDraft) return;
void askConfirm(this.app, this.plugin.t("savePreviewOnClose")).then((ok) => {
if (!ok) return;
void this.plugin.saveProfile(this.draft, true, false).then(
() => void this.plugin.activateView({ profile: this.draft })
);
});
}
};
var TextInputModal = class extends obsidian.Modal {
@ -592,14 +593,14 @@ var TextInputModal = class extends obsidian.Modal {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h3", { text: this.titleText });
if (this.hintText) contentEl.createEl("p", { text: this.hintText }).style.cssText = "margin:6px 0;color:var(--text-muted);font-size:0.9em;";
if (this.hintText) contentEl.createEl("p", { text: this.hintText }).addClass("sfv-modal-hint");
const input = contentEl.createEl("input");
input.type = "text";
input.placeholder = this.placeholderText;
input.value = this.initialValue;
input.style.cssText = "width:100%;margin-top:6px;";
input.addClass("sfv-modal-input");
const action = contentEl.createEl("div");
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:12px;";
action.addClass("sfv-modal-action");
action.createEl("button", { text: "Cancel" }).addEventListener("click", () => {
this.onSubmitValue(null);
this.close();
@ -640,9 +641,9 @@ var ThreeChoiceModal = class extends obsidian.Modal {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h3", { text: this.titleText });
contentEl.createEl("p", { text: this.messageText }).style.cssText = "margin:6px 0 10px;color:var(--text-muted);font-size:0.9em;";
contentEl.createEl("p", { text: this.messageText }).addClass("sfv-modal-hint");
const action = contentEl.createEl("div");
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;flex-wrap:wrap;";
action.addClass("sfv-modal-action");
const addChoice = (key, label, isCta = false) => {
if (!label) return;
const btn = action.createEl("button", { text: label });
@ -675,9 +676,9 @@ var StartActionModal = class extends obsidian.Modal {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h3", { text: this.plugin.t("startActionTitle") });
contentEl.createEl("p", { text: this.plugin.t("startActionDesc") }).style.cssText = "margin:6px 0 10px;color:var(--text-muted);font-size:0.9em;";
contentEl.createEl("p", { text: this.plugin.t("startActionDesc") }).addClass("sfv-modal-hint");
const action = contentEl.createEl("div");
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;";
action.addClass("sfv-modal-action");
action.createEl("button", { text: this.plugin.t("startOpenPage") }).addEventListener("click", () => {
this.onSubmitValue("open");
this.close();
@ -710,16 +711,16 @@ var FolderSelectModal = class extends obsidian.Modal {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("h3", { text: this.plugin.t("exportFolderTitle") });
contentEl.createEl("p", { text: this.plugin.t("exportFolderDesc") }).style.cssText = "margin:6px 0 10px;color:var(--text-muted);font-size:0.9em;";
contentEl.createEl("p", { text: this.plugin.t("exportFolderDesc") }).addClass("sfv-modal-hint");
const select = contentEl.createEl("select");
select.style.cssText = "width:100%;";
select.addClass("sfv-modal-select");
for (const f of this.folders) {
select.createEl("option", { value: f, text: f || this.plugin.t("rootFolder") });
}
select.value = this.initialValue;
makeSelectSearchable(select, this.plugin.t("search"));
const action = contentEl.createEl("div");
action.style.cssText = "display:flex;justify-content:flex-end;gap:8px;margin-top:12px;";
action.addClass("sfv-modal-action");
action.createEl("button", { text: "Cancel" }).addEventListener("click", () => {
this.onSubmitValue(null);
this.close();
@ -740,6 +741,38 @@ function askFolderSelect(app, plugin, folders, initialValue) {
new FolderSelectModal(app, plugin, folders, initialValue, resolve).open();
});
}
var ConfirmModal = class extends obsidian.Modal {
constructor(app, message, onResult) {
super(app);
this.message = message;
this.onResultValue = onResult;
}
onOpen() {
const { contentEl } = this;
contentEl.empty();
contentEl.createEl("p", { text: this.message });
const action = contentEl.createEl("div");
action.addClass("sfv-modal-action");
action.createEl("button", { text: "Cancel" }).addEventListener("click", () => {
this.onResultValue(false);
this.close();
});
const ok = action.createEl("button", { text: "OK" });
ok.addClass("mod-cta");
ok.addEventListener("click", () => {
this.onResultValue(true);
this.close();
});
}
onClose() {
this.contentEl.empty();
}
};
function askConfirm(app, message) {
return new Promise((resolve) => {
new ConfirmModal(app, message, resolve).open();
});
}
var ProfileManagerModal = class extends obsidian.Modal {
constructor(app, plugin) {
super(app);
@ -752,21 +785,25 @@ var ProfileManagerModal = class extends obsidian.Modal {
for (const profile of this.plugin.data.profiles) {
const s = new obsidian.Setting(contentEl).setName(profile.name || profile.sourceFolder).setDesc(profile.sourceFolder);
s.addButton((b) => b.setButtonText(this.plugin.t("open")).onClick(async () => {
this.plugin.clearDraftProfile();
this.plugin.data.lastActiveProfileId = profile.id;
await this.plugin.persist();
await this.plugin.activateView({ profile, newLeaf: true });
this.close();
void (async () => {
this.plugin.clearDraftProfile();
this.plugin.data.lastActiveProfileId = profile.id;
await this.plugin.persist();
await this.plugin.activateView({ profile, newLeaf: true });
this.close();
})();
}));
s.addButton((b) => b.setButtonText(this.plugin.t("edit")).onClick(() => {
new SetupModal(this.app, this.plugin, profile).open();
}));
s.addButton((b) => b.setButtonText(this.plugin.t("delete")).onClick(async () => {
if (!window.confirm(this.plugin.t("deleteConfirm", { name: profile.name || profile.sourceFolder }))) return;
this.plugin.data.profiles = this.plugin.data.profiles.filter((p) => p.id !== profile.id);
if (this.plugin.data.lastActiveProfileId === profile.id) this.plugin.data.lastActiveProfileId = "";
await this.plugin.persist();
this.onOpen();
s.addButton((b) => b.setButtonText(this.plugin.t("delete")).onClick(() => {
void askConfirm(this.app, this.plugin.t("deleteConfirm", { name: profile.name || profile.sourceFolder })).then(async (confirmed) => {
if (!confirmed) return;
this.plugin.data.profiles = this.plugin.data.profiles.filter((p) => p.id !== profile.id);
if (this.plugin.data.lastActiveProfileId === profile.id) this.plugin.data.lastActiveProfileId = "";
await this.plugin.persist();
this.onOpen();
});
}));
}
}
@ -825,7 +862,7 @@ var SmartFolderView = class extends obsidian.ItemView {
this.stateOwnerId = ownerId;
this.profileDefaultsKey = nextDefaultsKey;
try {
const saved = JSON.parse(localStorage.getItem(FILTER_STORAGE_PREFIX + ownerId) || "{}");
const saved = this.app.loadLocalStorage(FILTER_STORAGE_PREFIX + ownerId) || {};
this.isFreshFilterState = !saved || Object.keys(saved).length === 0;
const savedDefaultsKey = typeof saved.profileDefaultsKey === "string" ? saved.profileDefaultsKey : "";
const defaultsChanged = !!savedDefaultsKey && savedDefaultsKey !== this.profileDefaultsKey;
@ -842,14 +879,14 @@ var SmartFolderView = class extends obsidian.ItemView {
}
}
saveState() {
localStorage.setItem(FILTER_STORAGE_PREFIX + this.stateOwnerId, JSON.stringify({
this.app.saveLocalStorage(FILTER_STORAGE_PREFIX + this.stateOwnerId, {
...this.filterState,
profileDefaultsKey: this.profileDefaultsKey
}));
});
}
loadPresets() {
try {
const parsed = JSON.parse(localStorage.getItem(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId) || "[]");
const parsed = this.app.loadLocalStorage(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId);
if (!Array.isArray(parsed)) return [];
return clonePresetList(parsed);
} catch {
@ -857,7 +894,7 @@ var SmartFolderView = class extends obsidian.ItemView {
}
}
savePresets(presets) {
localStorage.setItem(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId, JSON.stringify(clonePresetList(presets)));
this.app.saveLocalStorage(FILTER_PRESET_STORAGE_PREFIX + this.stateOwnerId, clonePresetList(presets));
}
contextKey(profile) {
return JSON.stringify({ profileId: profile.id, selectedByAttr: this.filterState.selectedByAttr, enabled: this.filterState.enabledFilters, sort: this.filterState.sortOrder, viewMode: this.filterState.viewMode, boardField: profile.boardField });
@ -867,14 +904,14 @@ var SmartFolderView = class extends obsidian.ItemView {
}
loadOrderStore(ownerId) {
try {
const parsed = JSON.parse(localStorage.getItem(MANUAL_ORDER_STORAGE_PREFIX + ownerId) || "{}");
return parsed && typeof parsed === "object" ? parsed : {};
const parsed = this.app.loadLocalStorage(MANUAL_ORDER_STORAGE_PREFIX + ownerId);
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
} catch {
return {};
}
}
saveOrderStore(ownerId, store) {
localStorage.setItem(MANUAL_ORDER_STORAGE_PREFIX + ownerId, JSON.stringify(store));
this.app.saveLocalStorage(MANUAL_ORDER_STORAGE_PREFIX + ownerId, store);
}
applyOrder(ownerId, key, items) {
const store = this.loadOrderStore(ownerId);
@ -912,8 +949,8 @@ var SmartFolderView = class extends obsidian.ItemView {
await this.rerenderPreserveScroll();
new obsidian.Notice(this.plugin.t("undoDone"));
}
async onOpen() {
await this.renderView();
onOpen() {
this.renderView();
}
async rerenderPreserveScroll() {
const container = this.containerEl.children[1];
@ -925,8 +962,8 @@ var SmartFolderView = class extends obsidian.ItemView {
}
async askAndMaybeUpdateField(file, field, targetColumn, unsetLabel) {
const suggested = targetColumn === unsetLabel ? "" : targetColumn;
const useSuggested = window.confirm(this.plugin.t("moveAsk", { field, value: suggested || this.plugin.t("emptyValue") }));
const finalValue = useSuggested ? suggested : window.prompt(this.plugin.t("moveManual", { field }), suggested);
const useSuggested = await askConfirm(this.app, this.plugin.t("moveAsk", { field, value: suggested || this.plugin.t("emptyValue") }));
const finalValue = useSuggested ? suggested : await askTextInput(this.app, this.plugin.t("moveManual", { field }), this.plugin.t("moveManual", { field }), suggested, "");
if (finalValue == null) return void 0;
const value = finalValue.trim();
try {
@ -961,21 +998,22 @@ var SmartFolderView = class extends obsidian.ItemView {
}
renderCard(parent, file, cache, profile, color, showDot) {
const card = parent.createEl("section");
card.style.cssText = `position:relative;margin:0 0 12px;padding:12px 14px;border:1px solid var(--background-modifier-border);border-left:4px solid ${color};border-radius:12px;background:color-mix(in srgb, ${color} 9%, var(--background-primary) 91%);`;
card.addClass("sfv-card");
obsidian.setCssProps(card, { "--sfv-color": color });
if (showDot) {
card.createEl("span").style.cssText = `position:absolute;left:-26px;top:17px;width:12px;height:12px;border-radius:50%;background:${color};border:2px solid var(--background-primary);`;
card.createEl("span").addClass("sfv-card-dot");
}
const top = card.createEl("div");
top.style.cssText = "display:flex;align-items:center;gap:10px;overflow:hidden;margin-bottom:5px;";
top.addClass("sfv-card-top");
const meta = top.createEl("span");
meta.setText(profile.displayFields.map((k) => `${k}: ${getFrontmatterValues(cache, k).join("\u3001") || "-"}`).join(" | "));
meta.style.cssText = "font-size:0.85em;color:var(--text-muted);white-space:nowrap;overflow-x:auto;flex:1 1 auto;min-width:100px;";
meta.addClass("sfv-card-meta");
const link = top.createEl("a");
link.setText(file.basename);
link.className = "internal-link";
link.setAttribute("data-href", file.path);
link.setAttribute("href", file.path);
link.style.cssText = "font-size:1em;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 1 220px;min-width:80px;";
link.addClass("sfv-card-link");
const triggerHoverPreview = (e) => {
this.app.workspace?.trigger?.("hover-link", {
event: e,
@ -1012,7 +1050,6 @@ var SmartFolderView = class extends obsidian.ItemView {
};
const bodyEl = card.createEl("div");
bodyEl.addClass("sfv-card-body");
bodyEl.style.cssText = "max-height:calc(var(--line-height-normal) * 1em * 3);overflow-y:auto;overflow-x:hidden;padding-right:4px;";
bodyEl.addEventListener("mouseover", (e) => {
const el = getInternalLinkElement(e.target);
if (!el) return;
@ -1048,16 +1085,16 @@ var SmartFolderView = class extends obsidian.ItemView {
}
buildDropdown(container, label, options, selectedValues, onChange, onCommit) {
const wrap = container.createEl("div");
wrap.style.cssText = "display:flex;align-items:flex-start;gap:8px;flex:1 1 420px;min-width:320px;max-width:420px;position:relative;z-index:1;";
wrap.createEl("span", { text: label }).style.cssText = "font-size:1em;color:var(--text-normal);min-width:72px;padding-top:8px;white-space:nowrap;";
wrap.addClass("sfv-dd-wrap");
wrap.createEl("span", { text: label }).addClass("sfv-dd-label");
const box = wrap.createEl("details");
box.style.cssText = "flex:1;position:relative;z-index:1;min-width:0;";
box.addClass("sfv-dd-box");
const summary = box.createEl("summary");
summary.style.cssText = "list-style:none;cursor:pointer;height:36px;padding:7px 34px 7px 12px;border-radius:10px;border:1px solid var(--background-modifier-border);font-size:1em;box-sizing:border-box;overflow:hidden;";
summary.addClass("sfv-dd-summary");
const initialValues = [...selectedValues];
let currentValues = [...initialValues];
const summarySpan = summary.createEl("span");
summarySpan.style.cssText = "color:var(--text-normal);display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";
summarySpan.addClass("sfv-dd-summary-text");
const toDisplayText = (v) => v === EMPTY_OPTION_VALUE ? this.plugin.t("emptyValue") : v;
const refreshSummary = () => {
const isAll = currentValues.length === options.length && options.length > 0;
@ -1068,11 +1105,11 @@ var SmartFolderView = class extends obsidian.ItemView {
};
refreshSummary();
const panel = box.createEl("div");
panel.style.cssText = "position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;max-height:260px;overflow:auto;padding:8px;border-radius:10px;border:1px solid var(--background-modifier-border);background:var(--background-primary);";
panel.addClass("sfv-dd-panel");
const actionBar = panel.createEl("div");
actionBar.style.cssText = "display:flex;gap:10px;align-items:center;margin-bottom:4px;";
actionBar.addClass("sfv-dd-action-bar");
const allBtn = actionBar.createEl("button", { text: this.plugin.t("selectAll") });
allBtn.style.cssText = "border:none;background:transparent;color:var(--interactive-accent);cursor:pointer;";
allBtn.addClass("sfv-dd-action-btn");
allBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
@ -1085,7 +1122,7 @@ var SmartFolderView = class extends obsidian.ItemView {
refreshSummary();
});
const clearBtn = actionBar.createEl("button", { text: this.plugin.t("selectNone") });
clearBtn.style.cssText = "border:none;background:transparent;color:var(--interactive-accent);cursor:pointer;";
clearBtn.addClass("sfv-dd-action-btn");
clearBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
@ -1100,12 +1137,12 @@ var SmartFolderView = class extends obsidian.ItemView {
const search = panel.createEl("input");
search.type = "search";
search.placeholder = this.plugin.t("search");
search.style.cssText = "width:100%;height:32px;padding:4px 8px;border-radius:8px;border:1px solid var(--background-modifier-border);margin:6px 0;";
search.addClass("sfv-dd-search");
const list = panel.createEl("div");
const rows = [];
for (const option of options) {
const row = list.createEl("label");
row.style.cssText = "display:flex;align-items:center;gap:6px;padding:4px;font-size:1em;";
row.addClass("sfv-dd-row");
const cb = row.createEl("input");
cb.type = "checkbox";
cb.checked = currentValues.includes(option);
@ -1120,34 +1157,35 @@ var SmartFolderView = class extends obsidian.ItemView {
}
search.addEventListener("input", () => {
const q = search.value.toLowerCase();
for (const { option, row } of rows) row.style.display = !q || option.toLowerCase().includes(q) ? "flex" : "none";
for (const { option, row } of rows) {
if (!q || option.toLowerCase().includes(q)) row.removeClass("sfv-hidden");
else row.addClass("sfv-hidden");
}
});
box.addEventListener("toggle", () => {
if (box.open) {
wrap.style.zIndex = "170";
box.style.zIndex = "170";
wrap.addClass("sfv-elevated");
box.addClass("sfv-elevated");
} else {
wrap.style.zIndex = "1";
box.style.zIndex = "1";
wrap.removeClass("sfv-elevated");
box.removeClass("sfv-elevated");
if (onCommit) onCommit();
}
});
}
async renderView() {
renderView() {
const profile = this.getRenderProfile();
const container = this.containerEl.children[1];
container.empty();
container.classList.add("smart-folder-view-root");
this.updateLeafTitle();
container.style.padding = "16px 18px";
container.style.overflowY = "auto";
if (!profile) {
container.createEl("p", { text: this.plugin.t("noProfile") });
return;
}
this.ensureState(profile.id, profile.sortOrder, profile.viewMode);
const topBar = container.createEl("div");
topBar.style.cssText = "display:flex;justify-content:flex-start;gap:8px;margin-bottom:10px;";
topBar.addClass("sfv-top-bar");
const saveBtn = topBar.createEl("button", { text: this.plugin.isDraft(profile.id) ? this.plugin.t("savePage") : this.plugin.t("updatePage") });
saveBtn.addClass("mod-cta");
saveBtn.addEventListener("click", async () => {
@ -1160,11 +1198,11 @@ var SmartFolderView = class extends obsidian.ItemView {
});
topBar.createEl("button", { text: this.plugin.t("openBuilderBtn") }).addEventListener("click", () => new SetupModal(this.app, this.plugin).open());
topBar.createEl("button", { text: this.plugin.t("openManagerBtn") }).addEventListener("click", () => new ProfileManagerModal(this.app, this.plugin).open());
topBar.createEl("button", { text: this.plugin.t("exportPage") }).addEventListener("click", async () => {
await this.plugin.exportProfileToMarkdown(profile);
topBar.createEl("button", { text: this.plugin.t("exportPage") }).addEventListener("click", () => {
void this.plugin.exportProfileToMarkdown(profile);
});
topBar.createEl("button", { text: this.plugin.t("refreshPreview") }).addEventListener("click", async () => {
await this.renderView();
topBar.createEl("button", { text: this.plugin.t("refreshPreview") }).addEventListener("click", () => {
this.renderView();
new obsidian.Notice(this.plugin.t("refreshPreviewDone"));
});
const files = this.plugin.getFilesByFolder(profile.sourceFolder);
@ -1193,11 +1231,11 @@ var SmartFolderView = class extends obsidian.ItemView {
}
if (defaultSelectionInitialized) this.saveState();
const switchesEl = container.createEl("div");
switchesEl.style.cssText = "display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;";
switchesEl.addClass("sfv-switches");
for (const key of attrKeys) {
const enabled = this.filterState.enabledFilters.includes(key);
const chip = switchesEl.createEl("label");
chip.style.cssText = "display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;border:1px solid var(--background-modifier-border);font-size:1em;color:var(--text-normal);";
chip.addClass("sfv-chip");
const cb = chip.createEl("input");
cb.type = "checkbox";
cb.checked = enabled;
@ -1208,16 +1246,16 @@ var SmartFolderView = class extends obsidian.ItemView {
else s.delete(key);
this.filterState.enabledFilters = attrKeys.filter((k) => s.has(k));
this.saveState();
this.renderView();
void this.renderView();
});
}
const controls = container.createEl("div");
controls.style.cssText = "display:flex;flex-direction:column;gap:8px;margin-bottom:12px;position:relative;z-index:120;overflow:visible;";
controls.createEl("p", { text: this.plugin.t("presetScopeDesc") }).style.cssText = "margin:0;color:var(--text-muted);font-size:0.85em;";
controls.addClass("sfv-controls");
controls.createEl("p", { text: this.plugin.t("presetScopeDesc") }).addClass("sfv-controls-hint");
const presets = this.loadPresets();
const presetWrap = controls.createEl("div");
presetWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
presetWrap.createEl("span", { text: this.plugin.t("preset") }).style.cssText = "min-width:72px;color:var(--text-normal);";
presetWrap.addClass("sfv-preset-wrap");
presetWrap.createEl("span", { text: this.plugin.t("preset") }).addClass("sfv-dd-label");
const presetSel = presetWrap.createEl("select");
presetSel.createEl("option", { text: this.plugin.t("noPreset"), value: "" });
for (const p of presets) {
@ -1237,22 +1275,24 @@ var SmartFolderView = class extends obsidian.ItemView {
this.filterState.presetName = p.name;
}
this.saveState();
this.renderView();
void this.renderView();
});
makeSelectSearchable(presetSel, this.plugin.t("search"));
presetWrap.createEl("button", { text: this.plugin.t("savePreset") }).addEventListener("click", async () => {
const name = (await askTextInput(this.app, this.plugin.t("savePreset"), this.plugin.t("presetPrompt"), this.filterState.presetName || "", this.plugin.t("presetSaveHint")) || "").trim();
if (!name) {
new obsidian.Notice(this.plugin.t("presetNameRequired"));
return;
}
const currentPresets = this.loadPresets();
const next = currentPresets.filter((p) => p.name !== name);
next.push(clonePreset({ name, enabledFilters: [...this.filterState.enabledFilters], selectedByAttr: cloneSelectedByAttr(this.filterState.selectedByAttr), sortOrder: this.filterState.sortOrder, viewMode: this.filterState.viewMode }));
this.savePresets(next);
this.filterState.presetName = name;
this.saveState();
await this.renderView();
presetWrap.createEl("button", { text: this.plugin.t("savePreset") }).addEventListener("click", () => {
void (async () => {
const name = (await askTextInput(this.app, this.plugin.t("savePreset"), this.plugin.t("presetPrompt"), this.filterState.presetName || "", this.plugin.t("presetSaveHint")) || "").trim();
if (!name) {
new obsidian.Notice(this.plugin.t("presetNameRequired"));
return;
}
const currentPresets = this.loadPresets();
const next = currentPresets.filter((p) => p.name !== name);
next.push(clonePreset({ name, enabledFilters: [...this.filterState.enabledFilters], selectedByAttr: cloneSelectedByAttr(this.filterState.selectedByAttr), sortOrder: this.filterState.sortOrder, viewMode: this.filterState.viewMode }));
this.savePresets(next);
this.filterState.presetName = name;
this.saveState();
this.renderView();
})();
});
presetWrap.createEl("button", { text: this.plugin.t("delPreset") }).addEventListener("click", () => {
if (!this.filterState.presetName) return;
@ -1260,10 +1300,10 @@ var SmartFolderView = class extends obsidian.ItemView {
this.savePresets(currentPresets.filter((p) => p.name !== this.filterState.presetName));
this.filterState.presetName = "";
this.saveState();
this.renderView();
void this.renderView();
});
const filterGrid = controls.createEl("div");
filterGrid.style.cssText = "display:flex;flex-wrap:wrap;gap:8px 10px;align-items:flex-start;";
filterGrid.addClass("sfv-filter-grid");
for (const key of this.filterState.enabledFilters) {
this.buildDropdown(filterGrid, key, optionsByAttr[key] || [], this.filterState.selectedByAttr[key] || [], (vals) => {
this.filterState.selectedByAttr[key] = [...vals];
@ -1274,8 +1314,8 @@ var SmartFolderView = class extends obsidian.ItemView {
}
this.isFreshFilterState = false;
const sortWrap = controls.createEl("label");
sortWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
sortWrap.createEl("span", { text: this.plugin.t("sortOrder") }).style.cssText = "min-width:72px;color:var(--text-normal);";
sortWrap.addClass("sfv-sort-wrap");
sortWrap.createEl("span", { text: this.plugin.t("sortOrder") }).addClass("sfv-dd-label");
const sortSel = sortWrap.createEl("select");
sortSel.createEl("option", { value: "desc", text: this.plugin.t("desc") });
sortSel.createEl("option", { value: "asc", text: this.plugin.t("asc") });
@ -1285,39 +1325,41 @@ var SmartFolderView = class extends obsidian.ItemView {
sortSel.addEventListener("change", () => {
this.filterState.sortOrder = sortSel.value;
this.saveState();
this.renderView();
void this.renderView();
});
const modeWrap = controls.createEl("label");
modeWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
modeWrap.createEl("span", { text: this.plugin.t("viewMode") }).style.cssText = "min-width:72px;color:var(--text-normal);";
modeWrap.addClass("sfv-sort-wrap");
modeWrap.createEl("span", { text: this.plugin.t("viewMode") }).addClass("sfv-dd-label");
const modeSel = modeWrap.createEl("select");
modeSel.createEl("option", { value: "timeline", text: this.plugin.t("timeline") });
modeSel.createEl("option", { value: "board", text: this.plugin.t("board") });
modeSel.value = this.filterState.viewMode;
makeSelectSearchable(modeSel, this.plugin.t("search"));
modeSel.dispatchEvent(new Event("change"));
modeSel.addEventListener("change", async () => {
modeSel.addEventListener("change", () => {
this.filterState.viewMode = modeSel.value === "board" ? "board" : "timeline";
this.saveState();
this.renderView();
void this.renderView();
});
if (this.filterState.viewMode === "board") {
const boardFieldWrap = controls.createEl("label");
boardFieldWrap.style.cssText = "display:flex;gap:8px;align-items:center;";
boardFieldWrap.createEl("span", { text: this.plugin.t("boardField") }).style.cssText = "min-width:72px;color:var(--text-normal);";
boardFieldWrap.addClass("sfv-sort-wrap");
boardFieldWrap.createEl("span", { text: this.plugin.t("boardField") }).addClass("sfv-dd-label");
const boardFieldSel = boardFieldWrap.createEl("select");
boardFieldSel.createEl("option", { value: "", text: this.plugin.t("choose") });
for (const k of attrKeys) boardFieldSel.createEl("option", { value: k, text: k });
boardFieldSel.value = profile.boardField || "";
makeSelectSearchable(boardFieldSel, this.plugin.t("search"));
boardFieldSel.addEventListener("change", async () => {
const next = boardFieldSel.value;
profile.boardField = next;
const idx = this.plugin.data.profiles.findIndex((p) => p.id === profile.id);
if (idx >= 0) this.plugin.data.profiles[idx].boardField = next;
if (this.plugin.draftProfile?.id === profile.id) this.plugin.draftProfile.boardField = next;
await this.plugin.persist();
await this.renderView();
boardFieldSel.addEventListener("change", () => {
void (async () => {
const next = boardFieldSel.value;
profile.boardField = next;
const idx = this.plugin.data.profiles.findIndex((p) => p.id === profile.id);
if (idx >= 0) this.plugin.data.profiles[idx].boardField = next;
if (this.plugin.draftProfile?.id === profile.id) this.plugin.draftProfile.boardField = next;
await this.plugin.persist();
this.renderView();
})();
});
}
const filtered = entries.filter(({ file, cache }) => {
@ -1340,7 +1382,7 @@ var SmartFolderView = class extends obsidian.ItemView {
const cmp = pa.kind === "number" && pb.kind === "number" ? Number(pa.value) - Number(pb.value) : String(pa.raw).localeCompare(String(pb.raw), "zh-Hans-CN", { numeric: true, sensitivity: "base" });
return this.filterState.sortOrder === "asc" ? cmp : -cmp;
});
container.createEl("p", { text: this.plugin.t("matched", { count: filtered.length }) }).style.cssText = "color:var(--text-muted);margin:0 0 10px;";
container.createEl("p", { text: this.plugin.t("matched", { count: filtered.length }) }).addClass("sfv-matched");
if (!filtered.length) {
container.createEl("p", { text: this.plugin.t("noMatch") });
return;
@ -1348,7 +1390,7 @@ var SmartFolderView = class extends obsidian.ItemView {
if (this.filterState.viewMode === "timeline") {
const ordered = this.applyOrder(profile.id, this.orderKey(profile), filtered);
const orderActions2 = container.createEl("div");
orderActions2.style.cssText = "display:flex;gap:8px;align-items:center;margin:0 0 10px;";
orderActions2.addClass("sfv-order-actions");
orderActions2.createEl("button", { text: this.plugin.t("saveOrder") }).addEventListener("click", () => {
this.saveOrder(profile.id, this.orderKey(profile), ordered.map((e) => e.file));
new obsidian.Notice(this.plugin.t("saveOrderDone"));
@ -1357,27 +1399,27 @@ var SmartFolderView = class extends obsidian.ItemView {
this.recordUndoSnapshot(profile);
this.clearOrder(profile.id, this.orderKey(profile));
new obsidian.Notice(this.plugin.t("resetOrderDone"));
await this.renderView();
await this.rerenderPreserveScroll();
});
orderActions2.createEl("button", { text: this.plugin.t("undoLastAction") }).addEventListener("click", async () => {
await this.undoLastAction(profile);
});
const timelineEl = container.createEl("div");
timelineEl.style.cssText = "position:relative;padding-left:34px;margin-top:8px;";
timelineEl.addClass("sfv-timeline");
const lineEl = timelineEl.createEl("div");
lineEl.style.cssText = "position:absolute;left:16px;top:0;bottom:0;width:2px;background:var(--background-modifier-border);";
lineEl.addClass("sfv-timeline-line");
const indicator = timelineEl.createEl("div");
indicator.style.cssText = "position:absolute;left:22px;right:0;height:4px;border-radius:6px;background:var(--interactive-accent);opacity:0;pointer-events:none;z-index:40;transition:opacity 80ms ease;";
indicator.addClass("sfv-timeline-indicator");
let dropBeforePath = null;
const hideIndicator = () => {
indicator.style.opacity = "0";
indicator.removeClass("sfv-visible");
};
const updateDropIndicator = (clientY) => {
const cards = Array.from(timelineEl.querySelectorAll("section[data-sfv-path]")).filter((el) => !el.classList.contains("sfv-dragging"));
if (!cards.length) {
dropBeforePath = null;
indicator.style.top = "0px";
indicator.style.opacity = "1";
obsidian.setCssProps(indicator, { "--sfv-top": "0px" });
indicator.addClass("sfv-visible");
return;
}
let beforeCard = null;
@ -1396,8 +1438,8 @@ var SmartFolderView = class extends obsidian.ItemView {
const last = cards[cards.length - 1];
top = last.offsetTop + last.offsetHeight - 2;
}
indicator.style.top = `${Math.max(0, top)}px`;
indicator.style.opacity = "1";
obsidian.setCssProps(indicator, { "--sfv-top": `${Math.max(0, top)}px` });
indicator.addClass("sfv-visible");
};
timelineEl.addEventListener("dragover", (e) => {
e.preventDefault();
@ -1462,7 +1504,7 @@ var SmartFolderView = class extends obsidian.ItemView {
this.timelineDropBusy = false;
}
};
timelineEl.addEventListener("drop", async (e) => {
timelineEl.addEventListener("drop", (e) => {
e.preventDefault();
hideIndicator();
const payload = e.dataTransfer?.getData("text/plain") || "";
@ -1471,7 +1513,7 @@ var SmartFolderView = class extends obsidian.ItemView {
if (obj.token && this.timelineDragToken && obj.token !== this.timelineDragToken) return;
if (!obj.path || obj.from !== "timeline") return;
const beforePath = dropBeforePath;
await handleTimelineDrop(obj.path, beforePath);
void handleTimelineDrop(obj.path, beforePath);
});
for (const { file, cache } of ordered) {
const color = colorForValue(getFrontmatterValues(cache, profile.colorField)[0] || null, this.plugin.data.colorConfig) || this.plugin.data.colorConfig.fallbackColor;
@ -1509,7 +1551,7 @@ var SmartFolderView = class extends obsidian.ItemView {
}
const columns = uniqueSorted([...grouped.keys()]);
const orderActions = container.createEl("div");
orderActions.style.cssText = "display:flex;gap:8px;align-items:center;margin:0 0 10px;";
orderActions.addClass("sfv-order-actions");
orderActions.createEl("button", { text: this.plugin.t("saveOrder") }).addEventListener("click", () => {
for (const col of columns) {
const arr = grouped.get(col) || [];
@ -1527,13 +1569,12 @@ var SmartFolderView = class extends obsidian.ItemView {
await this.undoLastAction(profile);
});
const boardEl = container.createEl("div");
boardEl.style.cssText = "display:flex;gap:12px;align-items:flex-start;overflow-x:auto;margin-top:2px;padding-top:2px;";
boardEl.addClass("sfv-board");
const handleDrop = async (payload, to, beforePath) => {
const fromArr = grouped.get(payload.from) || [];
const toArr = grouped.get(to) || [];
const idx = fromArr.findIndex((e) => e.file.path === payload.path);
if (idx < 0) return;
const moved = fromArr[idx];
let action = "order";
let nextBoardValue = void 0;
if (payload.from !== to && profile.boardField) {
@ -1574,15 +1615,15 @@ var SmartFolderView = class extends obsidian.ItemView {
const ordered = this.applyOrder(profile.id, this.orderKey(profile, col), grouped.get(col) || []);
grouped.set(col, ordered);
const colWrap = boardEl.createEl("section");
colWrap.style.cssText = "min-width:280px;max-width:320px;flex:0 0 300px;border:1px solid var(--background-modifier-border);border-radius:12px;background:var(--background-primary-alt);padding:8px;";
colWrap.createEl("strong", { text: `${col} (${ordered.length})` });
colWrap.addClass("sfv-board-col");
colWrap.createEl("strong", { text: `${String(col)} (${ordered.length})` });
const cardsEl = colWrap.createEl("div");
cardsEl.style.cssText = "margin-top:8px;min-height:40px;position:relative;";
cardsEl.addClass("sfv-board-cards");
const dropIndicator = cardsEl.createEl("div");
dropIndicator.style.cssText = "height:10px;border:2px dashed var(--interactive-accent);border-radius:8px;background:color-mix(in srgb, var(--interactive-accent) 12%, transparent);margin:4px 0;opacity:0;pointer-events:none;transition:opacity 80ms ease;";
dropIndicator.addClass("sfv-drop-indicator");
let dropBeforePath = null;
const hideDropIndicator = () => {
dropIndicator.style.opacity = "0";
dropIndicator.removeClass("sfv-visible");
dropBeforePath = null;
};
const getNextCardPath = (cardEl) => {
@ -1603,7 +1644,7 @@ var SmartFolderView = class extends obsidian.ItemView {
if (!cards.length) {
dropBeforePath = null;
cardsEl.appendChild(dropIndicator);
dropIndicator.style.opacity = "1";
dropIndicator.addClass("sfv-visible");
return;
}
let beforeCard = null;
@ -1617,7 +1658,7 @@ var SmartFolderView = class extends obsidian.ItemView {
dropBeforePath = beforeCard?.getAttribute("data-sfv-path") || null;
if (beforeCard) cardsEl.insertBefore(dropIndicator, beforeCard);
else cardsEl.appendChild(dropIndicator);
dropIndicator.style.opacity = "1";
dropIndicator.addClass("sfv-visible");
};
cardsEl.addEventListener("dragover", (e) => {
e.preventDefault();
@ -1629,13 +1670,13 @@ var SmartFolderView = class extends obsidian.ItemView {
if (related instanceof Node && cardsEl.contains(related)) return;
hideDropIndicator();
});
cardsEl.addEventListener("drop", async (e) => {
cardsEl.addEventListener("drop", (e) => {
e.preventDefault();
const beforePath = dropBeforePath;
hideDropIndicator();
const payload = e.dataTransfer?.getData("text/plain") || "";
if (!payload) return;
await handleDrop(JSON.parse(payload), col, beforePath);
void handleDrop(JSON.parse(payload), col, beforePath);
});
for (const { file, cache } of ordered) {
const color = colorForValue(getFrontmatterValues(cache, profile.colorField)[0] || null, this.plugin.data.colorConfig) || this.plugin.data.colorConfig.fallbackColor;
@ -1655,19 +1696,19 @@ var SmartFolderView = class extends obsidian.ItemView {
e.preventDefault();
updateDropIndicator(e.clientY);
});
card.addEventListener("drop", async (e) => {
card.addEventListener("drop", (e) => {
e.preventDefault();
e.stopPropagation();
const beforePath = getCardDropBeforePath(card, e.clientY);
hideDropIndicator();
const payload = e.dataTransfer?.getData("text/plain") || "";
if (!payload) return;
await handleDrop(JSON.parse(payload), col, beforePath);
void handleDrop(JSON.parse(payload), col, beforePath);
});
}
}
}
async onClose() {
onClose() {
}
};
var SmartFolderSettingTab = class extends obsidian.PluginSettingTab {
@ -1710,7 +1751,7 @@ var SmartFolderSettingTab = class extends obsidian.PluginSettingTab {
this.plugin.data.colorConfig.lightnessRange = v;
await this.plugin.persist();
}));
new obsidian.Setting(containerEl).setName(this.plugin.t("fallbackColor")).setDesc("CSS color value, e.g. #3b82f6 or var(--interactive-accent)").addText((t) => t.setPlaceholder("var(--interactive-accent)").setValue(this.plugin.data.colorConfig.fallbackColor).onChange(async (v) => {
new obsidian.Setting(containerEl).setName(this.plugin.t("fallbackColor")).setDesc("CSS color value, e.g. #3b82f6 or var(--interactive-accent)").addText((t) => t.setPlaceholder("e.g. var(--interactive-accent)").setValue(this.plugin.data.colorConfig.fallbackColor).onChange(async (v) => {
this.plugin.data.colorConfig.fallbackColor = v.trim() || DEFAULT_COLOR_CONFIG.fallbackColor;
await this.plugin.persist();
})).addButton((b) => b.setButtonText(this.plugin.t("resetDefault")).onClick(async () => {
@ -1766,7 +1807,7 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
}
async saveProfile(profile, setActive, askNameIfEmpty) {
const next = { ...profile, displayFields: [...profile.displayFields || []], updatedAt: Date.now() };
if (!next.name && askNameIfEmpty) next.name = (window.prompt(this.t("pageNamePrompt"), next.sourceFolder || "") || "").trim();
if (!next.name && askNameIfEmpty) next.name = (await askTextInput(this.app, this.t("pageNamePrompt"), this.t("pageNamePrompt"), next.sourceFolder || "", "") || "").trim();
if (!next.name) next.name = next.sourceFolder || `Page-${(/* @__PURE__ */ new Date()).toISOString()}`;
const idx = this.data.profiles.findIndex((p) => p.id === next.id);
if (idx >= 0) {
@ -1828,7 +1869,7 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
const chosenFolder = await askFolderSelect(this.app, this, folders, defaultFolder);
if (chosenFolder == null) return;
const safeBase = (profile.name || profile.sourceFolder || "smart-folder-page").replace(/[\\/:*?"<>|]/g, "-").trim() || "smart-folder-page";
const folderPrefix = chosenFolder ? `${chosenFolder}/` : "";
const folderPrefix = chosenFolder ? `${String(chosenFolder)}/` : "";
let path = `${folderPrefix}${safeBase}.smart-folder-page.md`;
let i = 1;
while (this.app.vault.getAbstractFileByPath(path)) {
@ -1867,62 +1908,6 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
}
async onload() {
await this.loadPluginData();
this.styleEl = document.createElement("style");
this.styleEl.textContent = `
.smart-folder-view-root {
position: relative;
font-size: 12px;
line-height: 1.45;
border-radius: 12px;
overflow: hidden;
background: rgba(252, 253, 255, 0.8);
backdrop-filter: blur(8px) saturate(1.03);
box-shadow: inset 0 0 0 9999px rgba(255, 255, 255, 0.28);
}
.theme-dark .smart-folder-view-root {
background: rgba(5, 8, 12, 0.8);
backdrop-filter: blur(8px) saturate(0.95);
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.28);
}
.smart-folder-view-root details {
position: relative;
}
.smart-folder-view-root details[open] {
z-index: 200;
}
.smart-folder-view-root .sfv-dragging {
opacity: 0.72;
}
.smart-folder-view-root .sfv-card-body,
.smart-folder-view-root .sfv-card-body p,
.smart-folder-view-root .sfv-card-body li,
.smart-folder-view-root .sfv-card-body ul,
.smart-folder-view-root .sfv-card-body ol,
.smart-folder-view-root .sfv-card-body a,
.smart-folder-view-root .sfv-card-body strong,
.smart-folder-view-root .sfv-card-body em,
.smart-folder-view-root .sfv-card-body code,
.smart-folder-view-root .sfv-card-body span,
.smart-folder-view-root .sfv-card-body h1,
.smart-folder-view-root .sfv-card-body h2,
.smart-folder-view-root .sfv-card-body h3,
.smart-folder-view-root .sfv-card-body h4,
.smart-folder-view-root .sfv-card-body h5,
.smart-folder-view-root .sfv-card-body h6 {
font-size: calc(var(--font-text-size) - 1px) !important;
line-height: var(--line-height-normal) !important;
}
.smart-folder-view-root button,
.smart-folder-view-root input,
.smart-folder-view-root select,
.smart-folder-view-root summary,
.smart-folder-view-root label,
.smart-folder-view-root span,
.smart-folder-view-root p {
font-size: 12px;
}
`;
document.head.appendChild(this.styleEl);
this.registerView(VIEW_TYPE, (leaf) => new SmartFolderView(leaf, this));
this.registerMarkdownCodeBlockProcessor("smart-folder-page", async (source, el) => {
el.empty();
@ -1940,26 +1925,28 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
}
if (!el.dataset.sfvAutoOpened) {
el.dataset.sfvAutoOpened = "1";
window.setTimeout(async () => {
await this.openProfileFromExport(raw);
window.setTimeout(() => {
void this.openProfileFromExport(raw);
}, 0);
}
const wrap = el.createEl("div");
wrap.style.cssText = "border:1px solid var(--background-modifier-border);border-radius:10px;padding:10px;background:var(--background-primary-alt);";
wrap.addClass("sfv-export-block");
wrap.createEl("strong", { text: this.t("exportBlockTitle") });
wrap.createEl("p", { text: `${this.t("exportBlockDesc")} ${profile.name || profile.sourceFolder}` }).style.cssText = "margin:6px 0;color:var(--text-muted);";
wrap.createEl("p", { text: `${this.t("exportBlockDesc")} ${profile.name || profile.sourceFolder}` }).addClass("sfv-export-desc");
const row = wrap.createEl("div");
row.style.cssText = "display:flex;gap:8px;flex-wrap:wrap;";
row.addClass("sfv-export-row");
const openBtn = row.createEl("button", { text: this.t("exportBlockOpen") });
openBtn.addClass("mod-cta");
openBtn.addEventListener("click", async () => {
const ok = await this.openProfileFromExport(raw);
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
openBtn.addEventListener("click", () => {
void this.openProfileFromExport(raw).then((ok) => {
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
});
});
const importBtn = row.createEl("button", { text: this.t("exportBlockImport") });
importBtn.addEventListener("click", async () => {
const ok = await this.importProfileFromExport(raw);
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
importBtn.addEventListener("click", () => {
void this.importProfileFromExport(raw).then((ok) => {
if (!ok) new obsidian.Notice(this.t("exportBlockInvalid"));
});
});
});
this.registerDomEvent(document, "pointerdown", (ev) => {
@ -1976,29 +1963,34 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
const openBoxes = document.querySelectorAll(".smart-folder-view-root details[open]");
for (const box of openBoxes) box.removeAttribute("open");
});
this.addRibbonIcon("layout-grid", this.t("title"), async () => {
const action = await askStartAction(this.app, this);
if (action === "open") new ProfileManagerModal(this.app, this).open();
else if (action === "create") new SetupModal(this.app, this).open();
this.addRibbonIcon("layout-grid", this.t("title"), () => {
void askStartAction(this.app, this).then((action) => {
if (action === "open") new ProfileManagerModal(this.app, this).open();
else if (action === "create") new SetupModal(this.app, this).open();
});
});
this.addCommand({ id: "open-smart-folder-view", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
this.addCommand({ id: "open-smart-folder-view-builder", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
this.addCommand({ id: "open-smart-folder-view-manager", name: this.t("openManager"), callback: () => new ProfileManagerModal(this.app, this).open() });
this.addCommand({ id: "open-smart-folder-view-last", name: this.t("openLast"), callback: async () => {
if (!this.getCurrentProfile()) {
new obsidian.Notice(this.t("noProfile"));
new SetupModal(this.app, this).open();
return;
}
await this.activateView({ profile: this.getCurrentProfile(), newLeaf: true });
this.addCommand({ id: "create-page", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
this.addCommand({ id: "create-page-2", name: this.t("openBuilder"), callback: () => new SetupModal(this.app, this).open() });
this.addCommand({ id: "open-manager", name: this.t("openManager"), callback: () => new ProfileManagerModal(this.app, this).open() });
this.addCommand({ id: "open-last", name: this.t("openLast"), callback: () => {
void (async () => {
if (!this.getCurrentProfile()) {
new obsidian.Notice(this.t("noProfile"));
new SetupModal(this.app, this).open();
return;
}
await this.activateView({ profile: this.getCurrentProfile(), newLeaf: true });
})();
} });
this.addCommand({ id: "export-smart-folder-page", name: this.t("exportPage"), callback: async () => {
const profile = this.getCurrentProfile();
if (!profile) {
new obsidian.Notice(this.t("noProfile"));
return;
}
await this.exportProfileToMarkdown(profile);
this.addCommand({ id: "export-page", name: this.t("exportPage"), callback: () => {
void (async () => {
const profile = this.getCurrentProfile();
if (!profile) {
new obsidian.Notice(this.t("noProfile"));
return;
}
await this.exportProfileToMarkdown(profile);
})();
} });
this.addSettingTab(new SmartFolderSettingTab(this.app, this));
}
@ -2023,8 +2015,6 @@ var SmartFolderPlugin = class extends obsidian.Plugin {
}
}
onunload() {
if (this.styleEl) this.styleEl.remove();
this.app.workspace.detachLeavesOfType(VIEW_TYPE);
}
async loadPluginData() {
const raw = await this.loadData();

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,189 @@
/*
* Smart Folder View currently ships its view styling from main.js.
* This file is intentionally kept as a release asset placeholder so
* GitHub releases can include the standard Obsidian plugin file set.
*/
/* Smart Folder View — plugin styles */
/* ── Root container ─────────────────────────────────────────────── */
.smart-folder-view-root {
position: relative;
font-size: 12px;
line-height: 1.45;
border-radius: 12px;
overflow: hidden;
background: rgba(252, 253, 255, 0.8);
backdrop-filter: blur(8px) saturate(1.03);
box-shadow: inset 0 0 0 9999px rgba(255, 255, 255, 0.28);
padding: 16px 18px;
overflow-y: auto;
}
.theme-dark .smart-folder-view-root {
background: rgba(5, 8, 12, 0.8);
backdrop-filter: blur(8px) saturate(0.95);
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.28);
}
.smart-folder-view-root details { position: relative; }
.smart-folder-view-root details[open] { z-index: 200; }
/* ── Utility states ──────────────────────────────────────────────── */
.sfv-dragging { opacity: 0.72; }
.sfv-hidden { display: none !important; }
.sfv-visible { opacity: 1 !important; }
.sfv-elevated { z-index: 170 !important; }
/* ── Font-size normalization ─────────────────────────────────────── */
.smart-folder-view-root .sfv-card-body,
.smart-folder-view-root .sfv-card-body p,
.smart-folder-view-root .sfv-card-body li,
.smart-folder-view-root .sfv-card-body ul,
.smart-folder-view-root .sfv-card-body ol,
.smart-folder-view-root .sfv-card-body a,
.smart-folder-view-root .sfv-card-body strong,
.smart-folder-view-root .sfv-card-body em,
.smart-folder-view-root .sfv-card-body code,
.smart-folder-view-root .sfv-card-body span,
.smart-folder-view-root .sfv-card-body h1,
.smart-folder-view-root .sfv-card-body h2,
.smart-folder-view-root .sfv-card-body h3,
.smart-folder-view-root .sfv-card-body h4,
.smart-folder-view-root .sfv-card-body h5,
.smart-folder-view-root .sfv-card-body h6 {
font-size: calc(var(--font-text-size) - 1px) !important;
line-height: var(--line-height-normal) !important;
}
.smart-folder-view-root button,
.smart-folder-view-root input,
.smart-folder-view-root select,
.smart-folder-view-root summary,
.smart-folder-view-root label,
.smart-folder-view-root span,
.smart-folder-view-root p { font-size: 12px; }
/* ── Searchable single-select ────────────────────────────────────── */
.sfv-sel-host { position: relative; display: block; z-index: 1; width: 100%; }
.sfv-sel-host.sfv-elevated { z-index: 160; }
.sfv-sel-summary {
list-style: none; cursor: pointer; min-height: 30px;
padding: 5px 10px; border-radius: 8px;
border: 1px solid var(--background-modifier-border);
}
.sfv-sel-panel {
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
z-index: 60; max-height: 220px; overflow: auto; padding: 8px;
border-radius: 10px; border: 1px solid var(--background-modifier-border);
background: var(--background-primary);
min-width: var(--sfv-sel-min-width, 0px);
max-width: var(--sfv-sel-max-width, none);
}
.sfv-sel-input {
width: 100%; height: 28px; padding: 2px 8px; border-radius: 8px;
border: 1px solid var(--background-modifier-border); margin-bottom: 6px;
}
.sfv-sel-meter { position: fixed; visibility: hidden; white-space: nowrap; font: var(--font-ui-medium); }
.sfv-sel-row {
all: unset; display: block; width: 100%; box-sizing: border-box;
text-align: left; padding: 5px 8px; cursor: pointer; border-radius: 6px;
}
.sfv-sel-row:hover { background: var(--background-modifier-hover); }
.sfv-sel-summary-text { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
/* ── Multi-select dropdown filter ────────────────────────────────── */
.sfv-dd-wrap {
display: flex; align-items: flex-start; gap: 8px;
flex: 1 1 420px; min-width: 320px; max-width: 420px;
position: relative; z-index: 1;
}
.sfv-dd-wrap.sfv-elevated { z-index: 170; }
.sfv-dd-label { font-size: 1em; color: var(--text-normal); min-width: 72px; padding-top: 8px; white-space: nowrap; }
.sfv-dd-box { flex: 1; position: relative; z-index: 1; min-width: 0; }
.sfv-dd-box.sfv-elevated { z-index: 170; }
.sfv-dd-summary {
list-style: none; cursor: pointer; height: 36px;
padding: 7px 34px 7px 12px; border-radius: 10px;
border: 1px solid var(--background-modifier-border);
font-size: 1em; box-sizing: border-box; overflow: hidden;
}
.sfv-dd-summary-text { color: var(--text-normal); display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sfv-dd-panel {
position: absolute; top: calc(100% + 4px); left: 0; right: 0;
z-index: 30; max-height: 260px; overflow: auto; padding: 8px;
border-radius: 10px; border: 1px solid var(--background-modifier-border);
background: var(--background-primary);
}
.sfv-dd-action-bar { display: flex; gap: 10px; align-items: center; margin-bottom: 4px; }
.sfv-dd-action-btn { border: none; background: transparent; color: var(--interactive-accent); cursor: pointer; }
.sfv-dd-search {
width: 100%; height: 32px; padding: 4px 8px; border-radius: 8px;
border: 1px solid var(--background-modifier-border); margin: 6px 0;
}
.sfv-dd-row { display: flex; align-items: center; gap: 6px; padding: 4px; font-size: 1em; }
/* ── Cards ───────────────────────────────────────────────────────── */
.sfv-card {
position: relative; margin: 0 0 12px; padding: 12px 14px;
border: 1px solid var(--background-modifier-border);
border-left: 4px solid var(--sfv-color, var(--interactive-accent));
border-radius: 12px;
background: color-mix(in srgb, var(--sfv-color, var(--interactive-accent)) 9%, var(--background-primary) 91%);
}
.sfv-card-dot {
position: absolute; left: -26px; top: 17px; width: 12px; height: 12px;
border-radius: 50%; background: var(--sfv-color, var(--interactive-accent));
border: 2px solid var(--background-primary);
}
.sfv-card-top { display: flex; align-items: center; gap: 10px; overflow: hidden; margin-bottom: 5px; }
.sfv-card-meta { font-size: 0.85em; color: var(--text-muted); white-space: nowrap; overflow-x: auto; flex: 1 1 auto; min-width: 100px; }
.sfv-card-link { font-size: 1em; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 0 1 220px; min-width: 80px; }
.sfv-card-body { max-height: calc(var(--line-height-normal) * 1em * 3); overflow-y: auto; overflow-x: hidden; padding-right: 4px; }
/* ── Modal shared ────────────────────────────────────────────────── */
.sfv-modal-action { display: flex; justify-content: flex-end; gap: 8px; margin-top: 12px; }
.sfv-setup-action { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; }
.sfv-modal-hint { margin: 6px 0 10px; color: var(--text-muted); font-size: 0.9em; }
.sfv-modal-input { width: 100%; margin-top: 6px; }
.sfv-modal-select { width: 100%; }
/* ── View controls ───────────────────────────────────────────────── */
.sfv-top-bar { display: flex; justify-content: flex-start; gap: 8px; margin-bottom: 10px; }
.sfv-switches { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 10px; }
.sfv-chip {
display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px;
border-radius: 999px; border: 1px solid var(--background-modifier-border);
font-size: 1em; color: var(--text-normal);
}
.sfv-controls {
display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px;
position: relative; z-index: 120; overflow: visible;
}
.sfv-controls-hint { margin: 0; color: var(--text-muted); font-size: 0.85em; }
.sfv-preset-wrap { display: flex; gap: 8px; align-items: center; }
.sfv-filter-grid { display: flex; flex-wrap: wrap; gap: 8px 10px; align-items: flex-start; }
.sfv-sort-wrap { display: flex; gap: 8px; align-items: center; }
.sfv-matched { color: var(--text-muted); margin: 0 0 10px; }
.sfv-order-actions { display: flex; gap: 8px; align-items: center; margin: 0 0 10px; }
/* ── Timeline ────────────────────────────────────────────────────── */
.sfv-timeline { position: relative; padding-left: 34px; margin-top: 8px; }
.sfv-timeline-line { position: absolute; left: 16px; top: 0; bottom: 0; width: 2px; background: var(--background-modifier-border); }
.sfv-timeline-indicator {
position: absolute; left: 22px; right: 0; height: 4px; border-radius: 6px;
background: var(--interactive-accent); opacity: 0; pointer-events: none;
z-index: 40; transition: opacity 80ms ease; top: var(--sfv-top, 0px);
}
.sfv-timeline-indicator.sfv-visible { opacity: 1; }
/* ── Board ───────────────────────────────────────────────────────── */
.sfv-board { display: flex; gap: 12px; align-items: flex-start; overflow-x: auto; margin-top: 2px; padding-top: 2px; }
.sfv-board-col {
min-width: 280px; max-width: 320px; flex: 0 0 300px;
border: 1px solid var(--background-modifier-border); border-radius: 12px;
background: var(--background-primary-alt); padding: 8px;
}
.sfv-board-cards { margin-top: 8px; min-height: 40px; position: relative; }
.sfv-drop-indicator {
height: 10px; border: 2px dashed var(--interactive-accent); border-radius: 8px;
background: color-mix(in srgb, var(--interactive-accent) 12%, transparent);
margin: 4px 0; opacity: 0; pointer-events: none; transition: opacity 80ms ease;
}
.sfv-drop-indicator.sfv-visible { opacity: 1; }
/* ── Export block ────────────────────────────────────────────────── */
.sfv-export-block { border: 1px solid var(--background-modifier-border); border-radius: 10px; padding: 10px; background: var(--background-primary-alt); }
.sfv-export-desc { margin: 6px 0; color: var(--text-muted); }
.sfv-export-row { display: flex; gap: 8px; flex-wrap: wrap; }