Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/inspector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { ItemView, setIcon, TFile, WorkspaceLeaf } from "obsidian";
import type SequentialNoteNavigator from "./main";

export const SEQUENCE_INSPECTOR_VIEW_TYPE = "sequencer-inspector";

export type SequenceNode = {
file: TFile;
isCurrent: boolean;
};

export class SequenceInspectorView extends ItemView {
constructor(
leaf: WorkspaceLeaf,
private plugin: SequentialNoteNavigator,
) {
super(leaf);
}

getViewType(): string {
return SEQUENCE_INSPECTOR_VIEW_TYPE;
}

getDisplayText(): string {
return "Sequence inspector";
}

getIcon(): string {
return "signpost";
}

async onOpen(): Promise<void> {
this.render();
}

render(): void {
const { contentEl } = this;
contentEl.empty();
contentEl.addClass("seq-inspector");

const currentFile = this.plugin.getCurrentMarkdownFile();

if (!currentFile) {
contentEl.createEl("p", {
cls: "seq-inspector-empty",
text: "Open a sequenced note to inspect its chain.",
});
return;
}

const nodes = this.plugin.getSequenceNodes(currentFile);
if (nodes.length === 0) {
contentEl.createEl("p", {
cls: "seq-inspector-empty",
text: "This note is not connected to a sequence.",
});
return;
}

const listEl = contentEl.createDiv({ cls: "seq-inspector-chain" });
for (const node of nodes) {
const rowEl = listEl.createDiv({
cls: `seq-inspector-node${node.isCurrent ? " is-current" : ""}`,
});
const markerEl = rowEl.createDiv({ cls: "seq-inspector-marker" });
setIcon(markerEl, node.isCurrent ? "circle-dot" : "circle");

const buttonEl = rowEl.createEl("button", {
cls: "seq-inspector-note",
text: node.file.path,
});
buttonEl.ariaLabel = `Open ${node.file.basename}`;
buttonEl.onpointerdown = async (event) => {
event.preventDefault();
event.stopPropagation();
await this.plugin.openSequenceFile(node.file);
};
}
}
}
122 changes: 112 additions & 10 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { CachedMetadata, FrontMatterCache, MarkdownView, Notice, Plugin, setIcon, TFile } from "obsidian";
import { CachedMetadata, FrontMatterCache, MarkdownView, Notice, Plugin, setIcon, TFile, WorkspaceLeaf } from "obsidian";
import { SequenceInspectorView, SEQUENCE_INSPECTOR_VIEW_TYPE } from "./inspector";
import type { SequenceNode } from "./inspector";
import { ConfirmSequenceDeleteModal, InsertSequenceNoteModal, LinkToFileModal } from "./modal";
import { DEFAULT_SETTINGS, SequencerSettings, SequencerSettingTab } from "./settings";

export default class SequentialNoteNavigator extends Plugin {
settings: SequencerSettings;
private lastFocusedMarkdownFile: TFile | null = null;
private lastFocusedMarkdownLeaf: WorkspaceLeaf | null = null;
private pluginDeletedPaths = new Set<string>();

async onload() {
Expand All @@ -12,12 +16,21 @@ export default class SequentialNoteNavigator extends Plugin {
await this.loadSettings();

this.addSettingTab(new SequencerSettingTab(this.app, this));
this.registerView(
SEQUENCE_INSPECTOR_VIEW_TYPE,
(leaf) => new SequenceInspectorView(leaf, this),
);

this.registerEvent(
this.app.workspace.on("active-leaf-change", () => this.addNavigationButtons())
this.app.workspace.on("active-leaf-change", () => {
this.rememberActiveMarkdownView();
this.addNavigationButtons();
this.refreshSequenceInspectors();
})
);

// run on startup
this.rememberActiveMarkdownView();
this.addNavigationButtons();

this.registerEvent(
Expand All @@ -26,9 +39,14 @@ export default class SequentialNoteNavigator extends Plugin {
if (file.path === activeFile?.path) {
this.addNavigationButtons();
}
this.refreshSequenceInspectors();
})
);

this.addRibbonIcon("signpost", "Open sequence inspector", () => {
void this.activateSequenceInspector();
});

this.registerEvent(
this.app.metadataCache.on("deleted", (file, prevCache) => {
void this.handleDeletedFile(file, prevCache);
Expand All @@ -47,6 +65,14 @@ export default class SequentialNoteNavigator extends Plugin {
callback: () => this.insertLink("next"),
});

this.addCommand({
id: "open-sequence-inspector",
name: "Open sequence inspector",
callback: () => {
void this.activateSequenceInspector();
},
});

this.addCommand({
id: "insert-note-before-current",
name: "Insert note before current note",
Expand Down Expand Up @@ -78,14 +104,6 @@ export default class SequentialNoteNavigator extends Plugin {
void this.removeCurrentNoteFromSequence();
},
});

this.addCommand({
id: "unlink-current-note-from-sequence",
name: "Unlink current note from sequence",
callback: () => {
void this.removeCurrentNoteFromSequence();
},
});
}

async loadSettings() {
Expand All @@ -100,6 +118,90 @@ export default class SequentialNoteNavigator extends Plugin {
console.debug("Unloading Obsidian Sequencer plugin...");
}

getCurrentMarkdownFile(): TFile | null {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (view?.file) {
this.lastFocusedMarkdownFile = view.file;
this.lastFocusedMarkdownLeaf = view.leaf;
return view.file;
}

return this.lastFocusedMarkdownFile;
}

rememberActiveMarkdownView() {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view?.file) return;

this.lastFocusedMarkdownFile = view.file;
this.lastFocusedMarkdownLeaf = view.leaf;
}

async openSequenceFile(file: TFile) {
const activeMarkdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
const leaf = activeMarkdownView?.leaf ?? this.lastFocusedMarkdownLeaf ?? this.app.workspace.getLeaf(false);

await leaf.openFile(file);
this.lastFocusedMarkdownFile = file;
this.lastFocusedMarkdownLeaf = leaf;
this.refreshSequenceInspectors();
}

async activateSequenceInspector() {
const existingLeaves = this.app.workspace.getLeavesOfType(SEQUENCE_INSPECTOR_VIEW_TYPE);
if (existingLeaves.length > 0) {
await this.app.workspace.revealLeaf(existingLeaves[0]!);
this.refreshSequenceInspectors();
return;
}

const leaf = this.app.workspace.getRightLeaf(false);
if (!leaf) return;

await leaf.setViewState({ type: SEQUENCE_INSPECTOR_VIEW_TYPE, active: true });
await this.app.workspace.revealLeaf(leaf);
}

refreshSequenceInspectors() {
for (const leaf of this.app.workspace.getLeavesOfType(SEQUENCE_INSPECTOR_VIEW_TYPE)) {
if (leaf.view instanceof SequenceInspectorView) {
leaf.view.render();
}
}
}

getSequenceNodes(currentFile: TFile): SequenceNode[] {
const currentFrontmatter = this.getFrontmatter(currentFile);
if (!currentFrontmatter?.prev && !currentFrontmatter?.next) {
return [];
}

let firstFile = currentFile;
const reverseVisited = new Set<string>([currentFile.path]);
let previousFile = this.resolveSequenceLink(currentFile, "prev");

while (previousFile && !reverseVisited.has(previousFile.path)) {
firstFile = previousFile;
reverseVisited.add(previousFile.path);
previousFile = this.resolveSequenceLink(previousFile, "prev");
}

const nodes: SequenceNode[] = [];
const forwardVisited = new Set<string>();
let nextFile: TFile | null = firstFile;

while (nextFile && !forwardVisited.has(nextFile.path)) {
nodes.push({
file: nextFile,
isCurrent: nextFile.path === currentFile.path,
});
forwardVisited.add(nextFile.path);
nextFile = this.resolveSequenceLink(nextFile, "next");
}

return nodes;
}

addNavigationButtons() {
const view = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!view) return;
Expand Down
91 changes: 91 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,94 @@ If your plugin does not need CSS, delete this file.
.seq-nav-button {
padding: 4px;
}

.seq-inspector {
align-items: center;
box-sizing: border-box;
display: flex;
justify-content: center;
min-height: 100%;
padding: 12px;
width: 100%;
}

.seq-inspector-empty {
color: var(--text-muted);
font-size: var(--font-ui-small);
margin: 0;
}

.seq-inspector-chain {
display: flex;
flex-direction: column;
gap: 0;
margin-inline: auto;
max-width: 280px;
width: 100%;
}

.seq-inspector-node {
display: grid;
grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1fr);
grid-template-rows: auto;
padding: 8px 0 14px;
position: relative;
}

.seq-inspector-node::before {
background: var(--background-modifier-border);
bottom: -2px;
content: "";
left: calc(50% - 1px);
position: absolute;
top: 28px;
width: 2px;
}

.seq-inspector-node:last-child::before {
display: none;
}

.seq-inspector-marker {
align-items: center;
color: var(--text-muted);
display: flex;
grid-column: 2;
height: 24px;
justify-content: center;
position: relative;
z-index: 1;
}

.seq-inspector-node.is-current .seq-inspector-marker {
color: var(--interactive-accent);
}

.seq-inspector-note {
background: transparent;
border: 0;
box-shadow: none;
color: var(--text-normal);
cursor: pointer;
font-size: var(--font-ui-small);
font-weight: var(--font-semibold);
grid-column: 3;
height: auto;
justify-self: start;
line-height: 1.3;
margin: 0;
max-width: 100%;
overflow: hidden;
padding: 2px 4px;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}

.seq-inspector-note:hover {
color: var(--text-accent);
}

.seq-inspector-node.is-current .seq-inspector-note {
color: var(--text-accent);
}
Loading