Skip to content

Commit

Permalink
feat: support icons & colors for bookmark groups
Browse files Browse the repository at this point in the history
  • Loading branch information
gfxholo committed Jun 17, 2024
1 parent d4898b6 commit d9467c4
Show file tree
Hide file tree
Showing 2 changed files with 90 additions and 30 deletions.
53 changes: 35 additions & 18 deletions src/BookmarkIconManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import IconPicker from './IconPicker';
*/
export default class BookmarkIconManager extends IconManager {
private containerEl: HTMLElement;
private readonly selectionLookup = new Map<HTMLElement, string>();
private readonly selectionLookup = new Map<HTMLElement, BookmarkItem>();

constructor(plugin: IconicPlugin) {
super(plugin);
Expand Down Expand Up @@ -38,8 +38,23 @@ export default class BookmarkIconManager extends IconManager {
this.stopMutationObserver(this.containerEl);
}
this.containerEl = leaf.view.containerEl.find(':scope > .view-content > div');
if (this.containerEl) this.setMutationObserver(this.containerEl, { subtree: true, childList: true }, mutations => {
if (this.containerEl) this.setMutationObserver(this.containerEl, {
subtree: true,
childList: true,
attributeFilter: ['class'],
attributeOldValue: true
}, mutations => {
for (const mutation of mutations) {
// Refresh when bookmarks are renamed
if (mutation.attributeName === 'class'
&& mutation.target instanceof HTMLElement
&& mutation.oldValue?.includes('is-being-renamed')
&& !mutation.target.hasClass('is-being-renamed')
) {
this.refreshIcons();
return;
}
// Refresh when bookmarks are added or moved
for (const addedNode of mutation.addedNodes) {
if (addedNode instanceof HTMLElement && addedNode.hasClass('tree-item')) {
this.refreshIcons();
Expand Down Expand Up @@ -73,7 +88,7 @@ export default class BookmarkIconManager extends IconManager {
const bmark = bmarks[itemEls.indexOf(itemEl)]
if (!bmark) continue;
const selfEl = itemEl.find(':scope > .tree-item-self');
if (selfEl) this.selectionLookup.set(selfEl, bmark.id);
if (selfEl) this.selectionLookup.set(selfEl, bmark);

if (bmark.items) {
if (!itemEl.hasClass('is-collapsed')) {
Expand All @@ -90,36 +105,38 @@ export default class BookmarkIconManager extends IconManager {
}
}
});

continue;
}

const iconEl = itemEl.find(':scope > .tree-item-self > .tree-item-icon');
if (itemEl) this.refreshIcon(bmark, iconEl, event => {
IconPicker.openSingle(this.plugin, bmark, (newIcon, newColor) => {
this.plugin.saveBookmarkIcon(bmark, newIcon, newColor);
this.refreshIcons();
this.plugin.tabIconManager?.refreshIcons();
this.plugin.fileIconManager?.refreshIcons();
if (iconEl.hasClass('collapse-icon') && !bmark.icon) {
this.refreshIcon(bmark, iconEl); // Skip click listener if icon will be a collapse arrow
} else {
this.refreshIcon(bmark, iconEl, event => {
IconPicker.openSingle(this.plugin, bmark, (newIcon, newColor) => {
this.plugin.saveBookmarkIcon(bmark, newIcon, newColor);
this.refreshIcons();
this.plugin.tabIconManager?.refreshIcons();
this.plugin.fileIconManager?.refreshIcons();
});
event.stopPropagation();
});
event.stopPropagation();
});
}

this.setEventListener(itemEl, 'contextmenu', () => this.onContextMenu(bmark.id), { capture: true });
this.setEventListener(itemEl, 'contextmenu', () => this.onContextMenu(bmark.id, bmark.isFile), { capture: true });
};
}

/**
* When user context-clicks a bookmark, add custom items to the menu.
*/
private async onContextMenu(clickedBmarkId: string): Promise<void> {
private async onContextMenu(clickedBmarkId: string, isFile: boolean): Promise<void> {
this.plugin.menuManager.close();
const clickedBmark: BookmarkItem = this.plugin.getBookmarkItem(clickedBmarkId);
const clickedBmark: BookmarkItem = this.plugin.getBookmarkItem(clickedBmarkId, isFile);
const selectedBmarks: BookmarkItem[] = [];

for (const [selectableEl, bmarkId] of this.selectionLookup) {
for (const [selectableEl, bmark] of this.selectionLookup) {
if (selectableEl.hasClass('is-selected')) {
selectedBmarks.push(this.plugin.getBookmarkItem(bmarkId));
selectedBmarks.push(this.plugin.getBookmarkItem(bmark.id, bmark.isFile));
}
}

Expand Down
67 changes: 55 additions & 12 deletions src/IconicPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface FileItem extends Item {
items: FileItem[] | null;
}
export interface BookmarkItem extends Item {
isFile: boolean;
items: BookmarkItem[] | null;
}
export interface PropertyItem extends Item {
Expand Down Expand Up @@ -70,6 +71,7 @@ interface IconicSettings {
appIcons: { [appItemId: string]: { icon?: string, color?: string } };
tabIcons: { [tabId: string]: { icon?: string, color?: string } };
fileIcons: { [fileId: string]: { icon?: string, color?: string } };
groupIcons: { [groupId: string]: { icon?: string, color?: string } };
propertyIcons: { [propId: string]: { icon?: string, color?: string } };
ribbonIcons: { [ribbonItemId: string]: { icon?: string, color?: string } };
}
Expand All @@ -89,6 +91,7 @@ const DEFAULT_SETTINGS: IconicSettings = {
appIcons: {},
tabIcons: {},
fileIcons: {},
groupIcons: {},
propertyIcons: {},
ribbonIcons: {},
}
Expand Down Expand Up @@ -429,28 +432,58 @@ export default class IconicPlugin extends Plugin {
}

/**
* Get array of bookF definitions.
* Get array of bookmark definitions.
*/
getBookmarkItems(unloading?: boolean): BookmarkItem[] {
function flattenBookmarks(bmarkBases: any[]): any[] {
const flatArray = [];
for (const bmarkBase of bmarkBases) {
flatArray.push(bmarkBase);
if (bmarkBase.items) flatArray.concat(flattenBookmarks(bmarkBase.items));
}
return flatArray;
}
// @ts-expect-error (Private API)
const bmarkBases: any[] = this.app.internalPlugins?.plugins?.bookmarks?.instance?.items ?? [];
const bmarkBases: any[] = flattenBookmarks(this.app.internalPlugins?.plugins?.bookmarks?.instance?.items ?? []);
return bmarkBases.map(bmark => this.defineBookmarkItem(bmark, unloading));
}

/**
* Get bookmark definition.
*/
getBookmarkItem(bmarkId: string, unloading?: boolean): BookmarkItem {
getBookmarkItem(bmarkId: string, isFile: boolean, unloading?: boolean): BookmarkItem {
function findBookmark(bmarkBases: any[]): any {
for (const bmarkBase of bmarkBases) {
if (isFile) {
if (bmarkBase.path === bmarkId) return bmarkBase;
} else {
if (bmarkBase.type === 'group' && bmarkBase.ctime === bmarkId) return bmarkBase;
}
if (bmarkBase.items) {
const childBase = findBookmark(bmarkBase.items);
if (childBase) return childBase;
};
}
}
// @ts-expect-error (Private API)
const bmarkBase = this.app.internalPlugins?.plugins?.bookmarks?.instance?.bookmarkLookup?.[bmarkId];
const bmarkBase = findBookmark(this.app.internalPlugins?.plugins?.bookmarks?.instance?.items ?? []) ?? {};
return this.defineBookmarkItem(bmarkBase, unloading);
}

/**
* Create bookmark definition.
*/
private defineBookmarkItem(bmarkBase: any, unloading?: boolean): BookmarkItem {
const bmarkIcon = this.settings.fileIcons[bmarkBase.path] ?? {};
let id, name, bmarkIcon;
if (bmarkBase.type === 'file' || bmarkBase.type === 'folder') {
id = bmarkBase.path;
name = bmarkBase.path?.replace(/\.md$/, '');
bmarkIcon = this.settings.fileIcons[id] ?? {};
} else if (bmarkBase.type === 'group') {
id = bmarkBase.ctime;
name = bmarkBase.title;
bmarkIcon = this.settings.groupIcons[id] ?? {};
}
let iconDefault = 'lucide-file';
if (bmarkBase.subpath) {
iconDefault = 'lucide-heading';
Expand All @@ -466,12 +499,13 @@ export default class IconicPlugin extends Plugin {
}
}
return {
id: bmarkBase.path,
name: bmarkBase.path?.replace(/\.md$/, '') ?? null,
id: id,
name: name ?? null,
category: bmarkBase.type ?? 'file',
iconDefault: iconDefault,
icon: unloading ? null : bmarkIcon.icon ?? null,
color: unloading ? null : bmarkIcon.color ?? null,
icon: unloading ? null : bmarkIcon?.icon ?? null,
color: unloading ? null : bmarkIcon?.color ?? null,
isFile: bmarkBase.type === 'file' || bmarkBase.type === 'folder',
items: bmarkBase.items?.map((bmark: any) => this.defineBookmarkItem(bmark, unloading)) ?? null,
}
}
Expand Down Expand Up @@ -599,7 +633,11 @@ export default class IconicPlugin extends Plugin {
* Save bookmark icon changes to settings.
*/
saveBookmarkIcon(bmark: BookmarkItem, icon: string | null, color: string | null): void {
this.updateIconSetting(this.settings.fileIcons, bmark.id, icon, color);
if (bmark.category === 'file' || bmark.category === 'folder') {
this.updateIconSetting(this.settings.fileIcons, bmark.id, icon, color);
} else if (bmark.category === 'group') {
this.updateIconSetting(this.settings.groupIcons, bmark.id, icon, color);
}
this.saveSettings();
}

Expand All @@ -612,7 +650,11 @@ export default class IconicPlugin extends Plugin {
for (const bmark of bmarks) {
if (icon !== undefined) bmark.icon = icon;
if (color !== undefined) bmark.color = color;
this.updateIconSetting(this.settings.fileIcons, bmark.id, bmark.icon, bmark.color);
if (bmark.category === 'file' || bmark.category === 'folder') {
this.updateIconSetting(this.settings.fileIcons, bmark.id, bmark.icon, bmark.color);
} else if (bmark.category === 'group') {
this.updateIconSetting(this.settings.groupIcons, bmark.id, bmark.icon, bmark.color);
}
}
this.saveSettings();
}
Expand Down Expand Up @@ -672,12 +714,13 @@ export default class IconicPlugin extends Plugin {

/**
* Save settings to storage.
* Item IDs are sorted for human-readability.
*/
async saveSettings(): Promise<void> {
// Sort item IDs for more human-readable JSON
this.settings.appIcons = Object.fromEntries(Object.entries(this.settings.appIcons).sort());
this.settings.tabIcons = Object.fromEntries(Object.entries(this.settings.tabIcons).sort());
this.settings.fileIcons = Object.fromEntries(Object.entries(this.settings.fileIcons).sort());
this.settings.groupIcons = Object.fromEntries(Object.entries(this.settings.groupIcons).sort());
this.settings.propertyIcons = Object.fromEntries(Object.entries(this.settings.propertyIcons).sort());
this.settings.ribbonIcons = Object.fromEntries(Object.entries(this.settings.ribbonIcons).sort());
await this.saveData(this.settings);
Expand Down

0 comments on commit d9467c4

Please sign in to comment.