obsidian-sample-plugin/src/main.ts

848 lines
32 KiB
TypeScript

import {
apiVersion,
App,
FileExplorerView,
Menu,
MenuItem,
MetadataCache,
normalizePath,
Notice,
Platform,
Plugin,
PluginSettingTab,
requireApiVersion,
sanitizeHTMLToDom,
setIcon,
Setting,
TAbstractFile,
TFile,
TFolder,
Vault, WorkspaceLeaf
} from 'obsidian';
import {around} from 'monkey-around';
import {
folderSort,
ObsidianStandardDefaultSortingName,
ProcessingContext,
sortFolderItemsForBookmarking
} from './custom-sort/custom-sort';
import {
SortingSpecProcessor,
SortSpecsCollection
} from './custom-sort/sorting-spec-processor';
import {
CustomSortSpec
} from './custom-sort/custom-sort-types';
import {
addIcons,
ICON_SORT_ENABLED_ACTIVE,
ICON_SORT_ENABLED_NOT_APPLIED,
ICON_SORT_MOBILE_INITIAL,
ICON_SORT_SUSPENDED,
ICON_SORT_SUSPENDED_GENERAL_ERROR,
ICON_SORT_SUSPENDED_SYNTAX_ERROR
} from "./custom-sort/icons";
import {getStarredPlugin} from "./utils/StarredPluginSignature";
import {
BookmarksPluginInterface,
getBookmarksPlugin,
groupNameForPath
} from "./utils/BookmarksCorePluginSignature";
import {getIconFolderPlugin} from "./utils/ObsidianIconFolderPluginSignature";
import {lastPathComponent} from "./utils/utils";
import {
collectSortingAndGroupingTypes,
hasOnlyByBookmarkOrStandardObsidian,
HasSortingOrGrouping,
ImplicitSortspecForBookmarksIntegration
} from "./custom-sort/custom-sort-utils";
interface CustomSortPluginSettings {
additionalSortspecFile: string
suspended: boolean
statusBarEntryEnabled: boolean
notificationsEnabled: boolean
mobileNotificationsEnabled: boolean
automaticBookmarksIntegration: boolean
customSortContextSubmenu: boolean
bookmarksContextMenus: boolean
bookmarksGroupToConsumeAsOrderingReference: string
}
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
additionalSortspecFile: '',
suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install
statusBarEntryEnabled: true,
notificationsEnabled: true,
mobileNotificationsEnabled: false,
customSortContextSubmenu: true,
automaticBookmarksIntegration: false,
bookmarksContextMenus: false,
bookmarksGroupToConsumeAsOrderingReference: 'sortspec'
}
// On API 1.2.x+ enable the bookmarks integration by default
const DEFAULT_SETTING_FOR_1_2_0_UP: Partial<CustomSortPluginSettings> = {
automaticBookmarksIntegration: true,
bookmarksContextMenus: true
}
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
const SORTINGSPEC_YAML_KEY: string = 'sorting-spec'
const ERROR_NOTICE_TIMEOUT: number = 10000
// the monkey-around package doesn't export the below type
type MonkeyAroundUninstaller = () => void
type ContextMenuProvider = (item: MenuItem) => void
export default class CustomSortPlugin extends Plugin {
settings: CustomSortPluginSettings
statusBarItemEl: HTMLElement
ribbonIconEl: HTMLElement // On small-screen mobile devices this is useless (ribbon is re-created on-the-fly)
ribbonIconStateInaccurate: boolean // each time when displayed
sortSpecCache?: SortSpecsCollection | null
initialAutoOrManualSortingTriggered: boolean
fileExplorerFolderPatched: boolean
showNotice(message: string, timeout?: number) {
if (this.settings.notificationsEnabled || (Platform.isMobile && this.settings.mobileNotificationsEnabled)) {
new Notice(message, timeout)
}
}
readAndParseSortingSpec() {
const mCache: MetadataCache = app.metadataCache
let failed: boolean = false
let anySortingSpecFound: boolean = false
let errorMessage: string | null = null
// reset cache
this.sortSpecCache = null
const processor: SortingSpecProcessor = new SortingSpecProcessor()
if (this.settings.automaticBookmarksIntegration) {
this.sortSpecCache = processor.parseSortSpecFromText(
ImplicitSortspecForBookmarksIntegration.split('\n'),
'System internal path', // Dummy unused value, there are no errors in the internal spec
'System internal file', // Dummy unused value, there are no errors in the internal spec
this.sortSpecCache,
true // Implicit sorting spec generation
)
}
Vault.recurseChildren(app.vault.getRoot(), (file: TAbstractFile) => {
if (failed) return
if (file instanceof TFile) {
const aFile: TFile = file as TFile
const parent: TFolder = aFile.parent
// Read sorting spec from three sources of equal priority:
// - files with designated predefined name
// - files with the same name as parent folders (aka folder notes), e.g.: References/References.md
// - the file(s) explicitly configured by user in plugin settings
// Be human-friendly and accept both .md and .md.md file extensions
// (the latter representing a typical confusion between note name vs underlying file name)
if (aFile.name === SORTSPEC_FILE_NAME || // file name == sortspec.md ?
aFile.name === `${SORTSPEC_FILE_NAME}.md` || // file name == sortspec.md.md ?
aFile.basename === parent.name || // Folder Note mode: inside folder, same name
aFile.basename === this.settings.additionalSortspecFile || // when user configured _about_
aFile.name === this.settings.additionalSortspecFile || // when user configured _about_.md
aFile.path === this.settings.additionalSortspecFile || // when user configured Inbox/sort.md
aFile.path === `${this.settings.additionalSortspecFile}.md` // when user configured Inbox/sort
) {
const sortingSpecTxt: string = mCache.getCache(aFile.path)?.frontmatter?.[SORTINGSPEC_YAML_KEY]
// Warning: newer Obsidian versions can return objects as well, hence the explicit check for string value
if (typeof sortingSpecTxt === 'string') {
anySortingSpecFound = true
this.sortSpecCache = processor.parseSortSpecFromText(
sortingSpecTxt.split('\n'),
parent.path,
aFile.name,
this.sortSpecCache
)
if (this.sortSpecCache === null) {
failed = true
errorMessage = processor.recentErrorMessage ?? ''
}
}
}
}
})
if (this.sortSpecCache) {
this.showNotice(`Parsing custom sorting specification SUCCEEDED!`)
} else {
if (anySortingSpecFound) {
errorMessage = errorMessage ? errorMessage : `No valid '${SORTINGSPEC_YAML_KEY}:' key(s) in YAML front matter or multiline YAML indentation error or general YAML syntax error`
} else {
errorMessage = `No custom sorting specification found or only empty specification(s)`
}
this.showNotice(`Parsing custom sorting specification FAILED. Suspending the plugin.\n${errorMessage}`, ERROR_NOTICE_TIMEOUT)
this.settings.suspended = true
this.saveSettings()
}
}
checkFileExplorerIsAvailableAndPatchable(logWarning: boolean = true): FileExplorerView | undefined {
let fileExplorerView: FileExplorerView | undefined = this.getFileExplorer()
if (fileExplorerView
&& typeof fileExplorerView.createFolderDom === 'function'
&& typeof fileExplorerView.requestSort === 'function') {
return fileExplorerView
} else {
// Various scenarios when File Explorer was turned off (e.g. by some other plugin)
if (logWarning) {
this.logWarningFileExplorerNotAvailable()
}
return undefined
}
}
logWarningFileExplorerNotAvailable() {
const msg = `custom-sort v${this.manifest.version}: failed to locate File Explorer. The 'Files' core plugin can be disabled.\n`
+ `Some community plugins can also disable it.\n`
+ `See the example of MAKE.md plugin: https://github.com/Make-md/makemd/issues/25\n`
+ `You can find there instructions on how to re-enable the File Explorer in MAKE.md plugin`
console.warn(msg)
}
// Safe to suspend when suspended and re-enable when enabled
switchPluginStateTo(enabled: boolean, updateRibbonBtnIcon: boolean = true) {
let fileExplorerView: FileExplorerView | undefined = this.checkFileExplorerIsAvailableAndPatchable()
if (fileExplorerView && !this.fileExplorerFolderPatched) {
this.fileExplorerFolderPatched = this.patchFileExplorerFolder(fileExplorerView);
if (!this.fileExplorerFolderPatched) {
fileExplorerView = undefined
}
}
this.settings.suspended = !enabled;
this.saveSettings()
let iconToSet: string
if (this.settings.suspended) {
this.showNotice('Custom sort OFF');
this.sortSpecCache = null
iconToSet = ICON_SORT_SUSPENDED
} else {
this.readAndParseSortingSpec();
if (this.sortSpecCache) {
if (fileExplorerView) {
this.showNotice('Custom sort ON');
this.initialAutoOrManualSortingTriggered = true
iconToSet = ICON_SORT_ENABLED_ACTIVE
} else {
this.showNotice('Custom sort GENERAL PROBLEM. See console for detailed message.');
iconToSet = ICON_SORT_SUSPENDED_GENERAL_ERROR
this.settings.suspended = true
this.saveSettings()
}
} else {
iconToSet = ICON_SORT_SUSPENDED_SYNTAX_ERROR
this.settings.suspended = true
this.saveSettings()
}
}
// Syntax sugar
const ForceFlushCache = true
if (!this.settings.suspended) {
getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, ForceFlushCache)
}
if (fileExplorerView) {
if (this.fileExplorerFolderPatched) {
fileExplorerView.requestSort();
}
} else {
if (iconToSet === ICON_SORT_ENABLED_ACTIVE) {
iconToSet = ICON_SORT_ENABLED_NOT_APPLIED
if (updateRibbonBtnIcon) {
this.ribbonIconStateInaccurate = true
}
}
}
if (updateRibbonBtnIcon) {
// REMARK: on small-screen mobile devices this is void, the handle to ribbon <div> Element is useless,
// as the ribbon (and its icons) get re-created each time when re-displayed (expanded)
setIcon(this.ribbonIconEl, iconToSet)
}
this.updateStatusBar();
}
async onload() {
console.log(`loading custom-sort v${this.manifest.version}`);
await this.loadSettings();
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
if (this.settings.statusBarEntryEnabled) {
this.statusBarItemEl = this.addStatusBarItem();
this.updateStatusBar()
}
addIcons();
// Create an icon button in the left ribbon.
// REMARK: on small-screen mobile devices, the ribbon is dynamically re-created each time when displayed
// in result, the handle to the ribbon <div> Element is useless
this.ribbonIconEl = this.addRibbonIcon(
Platform.isDesktop ?
(this.settings.suspended ? ICON_SORT_SUSPENDED : ICON_SORT_ENABLED_NOT_APPLIED)
:
ICON_SORT_MOBILE_INITIAL // REMARK: on small-screen mobile devices this icon stays permanent
,
'Toggle custom sorting', (evt: MouseEvent) => {
// Clicking the icon toggles between the states of custom sort plugin
this.switchPluginStateTo(this.settings.suspended)
});
if (!this.settings.suspended) {
this.ribbonIconStateInaccurate = true
}
this.addSettingTab(new CustomSortSettingTab(app, this));
this.registerEventHandlers()
this.registerCommands()
this.initialize();
}
registerEventHandlers() {
const plugin: CustomSortPlugin = this
const m: boolean = Platform.isMobile
this.registerEvent(
// Keep in mind: this event is triggered once after app starts and then after each modification of _any_ metadata
app.metadataCache.on("resolved", () => {
if (!this.settings.suspended) {
if (!this.initialAutoOrManualSortingTriggered) {
this.readAndParseSortingSpec()
this.initialAutoOrManualSortingTriggered = true
if (this.sortSpecCache) { // successful read of sorting specifications?
this.showNotice('Custom sort ON')
const fileExplorerView: FileExplorerView | undefined = this.checkFileExplorerIsAvailableAndPatchable(false)
if (fileExplorerView) {
setIcon(this.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE)
fileExplorerView.requestSort()
} else {
setIcon(this.ribbonIconEl, ICON_SORT_ENABLED_NOT_APPLIED)
plugin.ribbonIconStateInaccurate = true
}
this.updateStatusBar()
} else {
this.settings.suspended = true
setIcon(this.ribbonIconEl, ICON_SORT_SUSPENDED_SYNTAX_ERROR)
this.saveSettings()
}
}
}
})
);
const applyCustomSortMenuItem = (item: MenuItem) => {
item.setTitle(m ? 'Custom sort: apply custom sorting' : 'Apply custom sorting');
item.onClick(() => {
plugin.switchPluginStateTo(true, true)
})
};
const suspendCustomSortMenuItem = (item: MenuItem) => {
item.setTitle(m ? 'Custom sort: suspend custom sorting' : 'Suspend custom sorting');
item.onClick(() => {
plugin.switchPluginStateTo(false, true)
})
};
const getBookmarkThisMenuItemForFile = (file: TAbstractFile): ContextMenuProvider =>
(item: MenuItem) => {
item.setTitle(m ? 'Bookmark it for custom sorting' : 'Bookmark it for sorting');
item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
bookmarksPlugin.bookmarkFolderItem(file)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
});
};
const getUnbookmarkThisMenuItemForFile = (file: TAbstractFile): ContextMenuProvider =>
(item: MenuItem) => {
item.setTitle(m ? 'UNbookmark it from custom sorting' : 'UNbookmark it from sorting');
item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
bookmarksPlugin.unbookmarkFolderItem(file)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
});
};
const getBookmarkAllMenuItemForFile = (file: TAbstractFile): ContextMenuProvider =>
(item: MenuItem) => {
item.setTitle(m ? 'Bookmark it+siblings for custom sorting' : 'Bookmark it+siblings for sorting');
item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
const orderedChildren: Array<TAbstractFile> = plugin.orderedFolderItemsForBookmarking(file.parent, bookmarksPlugin)
bookmarksPlugin.bookmarkSiblings(orderedChildren)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
});
};
const getUnbookmarkAllMenuItemForFile = (file: TAbstractFile): ContextMenuProvider =>
(item: MenuItem) => {
item.setTitle(m ? 'UNbookmark it+siblings from custom sorting' : 'UNbookmark it+siblings from sorting');
item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
const orderedChildren: Array<TAbstractFile> = file.parent.children.map((entry: TFile | TFolder) => entry)
bookmarksPlugin.unbookmarkSiblings(orderedChildren)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
});
};
const getBookmarkSelectedMenuItemForFiles = (files: TAbstractFile[]): ContextMenuProvider =>
(item: MenuItem) => {
item.setTitle(m ? 'Bookmark selected for custom sorting' : 'Custom sort: bookmark selected for sorting');
item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
files.forEach((file) => {
bookmarksPlugin.bookmarkFolderItem(file)
})
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
});
};
const getUnbookmarkSelectedMenuItemForFiles = (files: TAbstractFile[]): ContextMenuProvider =>
(item: MenuItem) => {
item.setTitle(m ? 'UNbookmark selected from custom sorting' : 'Custom sort: UNbookmark selected from sorting');
item.onClick(() => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
files.forEach((file) => {
bookmarksPlugin.unbookmarkFolderItem(file)
})
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
});
};
this.registerEvent(
app.workspace.on("file-menu", (menu: Menu, file: TAbstractFile, source: string, leaf?: WorkspaceLeaf) => {
if (!this.settings.customSortContextSubmenu) return; // Don't show the context menus at all
const customSortMenuItem = (item?: MenuItem) => {
// if parameter is empty it means mobile invocation, where submenus are not supported.
// In that case flatten the menu.
let submenu: Menu|undefined
if (item) {
item.setTitle('Custom sort:');
item.setIcon('hashtag');
submenu = item.setSubmenu()
}
if (!submenu) menu.addSeparator();
(submenu ?? menu).addItem(applyCustomSortMenuItem)
if (submenu) submenu.addSeparator();
if (this.settings.bookmarksContextMenus) {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
const itemAlreadyBookmarkedForSorting: boolean = bookmarksPlugin.isBookmarkedForSorting(file)
if (!itemAlreadyBookmarkedForSorting) {
(submenu ?? menu).addItem(getBookmarkThisMenuItemForFile(file))
} else {
(submenu ?? menu).addItem(getUnbookmarkThisMenuItemForFile(file))
}
(submenu ?? menu).addItem(getBookmarkAllMenuItemForFile(file));
(submenu ?? menu).addItem(getUnbookmarkAllMenuItemForFile(file));
}
}
(submenu ?? menu).addItem(suspendCustomSortMenuItem)
}
if (m) {
customSortMenuItem(undefined)
} else {
menu.addItem(customSortMenuItem)
}
})
)
if (requireApiVersion('1.4.11')) {
this.registerEvent(
// "files-menu" event was exposed in 1.4.11
// @ts-ignore
app.workspace.on("files-menu", (menu: Menu, files: TAbstractFile[], source: string, leaf?: WorkspaceLeaf) => {
if (!this.settings.customSortContextSubmenu) return; // Don't show the context menus at all
const customSortMenuItem = (item?: MenuItem) => {
// if parameter is empty it means mobile invocation, where submenus are not supported.
// In that case flatten the menu.
let submenu: Menu|undefined
if (item) {
item.setTitle('Custom sort:');
item.setIcon('hashtag');
submenu = item.setSubmenu()
}
if (!submenu) menu.addSeparator();
(submenu ?? menu).addItem(applyCustomSortMenuItem)
if (submenu) submenu.addSeparator();
if (this.settings.bookmarksContextMenus) {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
(submenu ?? menu).addItem(getBookmarkSelectedMenuItemForFiles(files));
(submenu ?? menu).addItem(getUnbookmarkSelectedMenuItemForFiles(files));
}
}
(submenu ?? menu).addItem(suspendCustomSortMenuItem);
};
if (m) {
customSortMenuItem(undefined)
} else {
menu.addItem(customSortMenuItem)
}
})
)
}
this.registerEvent(
app.vault.on("rename", (file: TAbstractFile, oldPath: string) => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
bookmarksPlugin.updateSortingBookmarksAfterItemRenamed(file, oldPath)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
})
)
app.vault.on("delete", (file: TAbstractFile) => {
const bookmarksPlugin = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
if (bookmarksPlugin) {
bookmarksPlugin.updateSortingBookmarksAfterItemDeleted(file)
bookmarksPlugin.saveDataAndUpdateBookmarkViews(true)
}
})
}
registerCommands() {
const plugin: CustomSortPlugin = this
this.addCommand({
id: 'enable-custom-sorting',
name: 'Enable and apply the custom sorting, (re)parsing the sorting configuration first. Sort-on.',
callback: () => {
plugin.switchPluginStateTo(true, true)
}
});
this.addCommand({
id: 'suspend-custom-sorting',
name: 'Suspend the custom sorting. Sort-off.',
callback: () => {
plugin.switchPluginStateTo(false, true)
}
});
}
initialize() {
app.workspace.onLayoutReady(() => {
this.fileExplorerFolderPatched = this.patchFileExplorerFolder();
})
}
determineSortSpecForFolder(folderPath: string, folderName?: string): CustomSortSpec|null|undefined {
folderName = folderName ?? lastPathComponent(folderPath)
let sortSpec: CustomSortSpec | null | undefined = this.sortSpecCache?.sortSpecByPath?.[folderPath]
sortSpec = sortSpec ?? this.sortSpecCache?.sortSpecByName?.[folderName]
if (!sortSpec && this.sortSpecCache?.sortSpecByWildcard) {
// when no sorting spec found directly by folder path, check for wildcard-based match
sortSpec = this.sortSpecCache?.sortSpecByWildcard.folderMatch(folderPath, folderName)
}
return sortSpec
}
createProcessingContextForSorting(has: HasSortingOrGrouping): ProcessingContext {
const ctx: ProcessingContext = {
_mCache: app.metadataCache,
starredPluginInstance: has.grouping.byStarred ? getStarredPlugin() : undefined,
bookmarksPluginInstance: has.grouping.byBookmarks || has.sorting.byBookmarks ? getBookmarksPlugin(this.settings.bookmarksGroupToConsumeAsOrderingReference, false, true) : undefined,
iconFolderPluginInstance: has.grouping.byIcon ? getIconFolderPlugin() : undefined,
plugin: this
}
return ctx
}
// For the idea of monkey-patching credits go to https://github.com/nothingislost/obsidian-bartender
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
let plugin = this;
// patching file explorer might fail here because of various non-error reasons.
// That's why not showing and not logging error message here
patchableFileExplorer = patchableFileExplorer ?? this.checkFileExplorerIsAvailableAndPatchable(false)
if (patchableFileExplorer) {
// @ts-ignore
let tmpFolder = new TFolder(Vault, "");
let Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor;
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) {
return function (...args: any[]) {
// quick check for plugin status
if (plugin.settings.suspended) {
return old.call(this, ...args);
}
if (plugin.ribbonIconStateInaccurate && plugin.ribbonIconEl) {
plugin.ribbonIconStateInaccurate = false
setIcon(plugin.ribbonIconEl, ICON_SORT_ENABLED_ACTIVE)
}
const folder: TFolder = this.file
let sortSpec: CustomSortSpec | null | undefined = plugin.determineSortSpecForFolder(folder.path, folder.name)
// Performance optimization
// Primary intention: when the implicit bookmarks integration is enabled, remain on std Obsidian, if no need to involve bookmarks
let has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
if (hasOnlyByBookmarkOrStandardObsidian(has)) {
const bookmarksPlugin: BookmarksPluginInterface|undefined = getBookmarksPlugin(plugin.settings.bookmarksGroupToConsumeAsOrderingReference, false, true)
if ( !bookmarksPlugin?.bookmarksIncludeItemsInFolder(folder.path)) {
sortSpec = null
}
}
if (sortSpec) {
return folderSort.call(this, sortSpec, plugin.createProcessingContextForSorting(has));
} else {
return old.call(this, ...args);
}
};
}
})
this.register(uninstallerOfFolderSortFunctionWrapper)
return true
} else {
return false
}
}
orderedFolderItemsForBookmarking(folder: TFolder, bookmarksPlugin: BookmarksPluginInterface): Array<TAbstractFile> {
let sortSpec: CustomSortSpec | null | undefined = undefined
if (!this.settings.suspended) {
sortSpec = this.determineSortSpecForFolder(folder.path, folder.name)
}
let uiSortOrder: string = this.getFileExplorer()?.sortOrder || ObsidianStandardDefaultSortingName
const has: HasSortingOrGrouping = collectSortingAndGroupingTypes(sortSpec)
return sortFolderItemsForBookmarking(
folder,
folder.children,
sortSpec,
this.createProcessingContextForSorting(has),
uiSortOrder
)
}
// Credits go to https://github.com/nothingislost/obsidian-bartender
getFileExplorer(): FileExplorerView | undefined {
let fileExplorer: FileExplorerView | undefined = app.workspace.getLeavesOfType("file-explorer")?.first()
?.view as unknown as FileExplorerView;
return fileExplorer;
}
onunload() {
}
updateStatusBar() {
if (this.statusBarItemEl) {
this.statusBarItemEl.setText(`Custom sort:${this.settings.suspended ? 'OFF' : 'ON'}`)
}
}
async loadSettings() {
const data: any = await this.loadData() || {}
const isFreshInstall: boolean = Object.keys(data).length === 0
this.settings = Object.assign({}, DEFAULT_SETTINGS, data);
if (requireApiVersion('1.2.0')) {
this.settings = Object.assign(this.settings, DEFAULT_SETTING_FOR_1_2_0_UP)
}
}
async saveSettings() {
await this.saveData(this.settings);
}
}
const pathToFlatString = (path: string): string => {
return path.replace(/\//g,'_').replace(/\\/g, '_')
}
class CustomSortSettingTab extends PluginSettingTab {
plugin: CustomSortPlugin;
constructor(app: App, plugin: CustomSortPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
// containerEl.createEl('h2', {text: 'Settings for Custom File Explorer Sorting Plugin'});
const additionalSortspecFileDescr: DocumentFragment = sanitizeHTMLToDom(
'A note name or note path to scan (YAML frontmatter) for sorting specification in addition to the `sortspec` notes and Folder Notes<sup><b>*</b></sup>.'
+ '<br>'
+ ' The `.md` filename suffix is optional.'
+ '<p><b>(*)</b>&nbsp;if you employ the <i>Index-File based</i> approach to folder notes (as documented in '
+ '<a href="https://github.com/aidenlx/alx-folder-note/wiki/folder-note-pref"'
+ '>Aidenlx Folder Note preferences</a>'
+ ') you can enter here the index note name, e.g. <b>_about_</b>'
+ '<br>'
+ 'The <i>Inside Folder, with Same Name Recommended</i> mode of Folder Notes is handled automatically, no additional configuration needed.'
+ '</p>'
+ '<p>NOTE: After updating this setting remember to refresh the custom sorting via clicking on the ribbon icon or via the <b>sort-on</b> command'
+ ' or by restarting Obsidian or reloading the vault</p>'
)
new Setting(containerEl)
.setName('Path or name of additional note(s) containing sorting specification')
.setDesc(additionalSortspecFileDescr)
.addText(text => text
.setPlaceholder('e.g. _about_')
.setValue(this.plugin.settings.additionalSortspecFile)
.onChange(async (value) => {
this.plugin.settings.additionalSortspecFile = value.trim() ? normalizePath(value) : '';
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable the status bar entry')
.setDesc('The status bar entry shows the label `Custom sort:ON` or `Custom sort:OFF`, representing the current state of the plugin.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.statusBarEntryEnabled)
.onChange(async (value) => {
this.plugin.settings.statusBarEntryEnabled = value;
if (value) {
// Enabling
if (this.plugin.statusBarItemEl) {
// for sanity
this.plugin.statusBarItemEl.detach()
}
this.plugin.statusBarItemEl = this.plugin.addStatusBarItem();
this.plugin.updateStatusBar()
} else { // disabling
if (this.plugin.statusBarItemEl) {
this.plugin.statusBarItemEl.detach()
}
}
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable notifications of plugin state changes')
.setDesc('The plugin can show notifications about its state changes: e.g. when successfully parsed and applied'
+ ' the custom sorting specification, or, when the parsing failed. If the notifications are disabled,'
+ ' the only indicator of plugin state is the ribbon button icon. The developer console presents the parsing'
+ ' error messages regardless if the notifications are enabled or not.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.notificationsEnabled)
.onChange(async (value) => {
this.plugin.settings.notificationsEnabled = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable notifications of plugin state changes for mobile devices only')
.setDesc('See above.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.mobileNotificationsEnabled)
.onChange(async (value) => {
this.plugin.settings.mobileNotificationsEnabled = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable File Explorer context submenu`Custom sort:`')
.setDesc('Gives access to operations relevant for custom sorting, e.g. applying custom sorting.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.customSortContextSubmenu)
.onChange(async (value) => {
this.plugin.settings.customSortContextSubmenu = value;
await this.plugin.saveSettings();
}));
containerEl.createEl('h2', {text: 'Bookmarks integration'});
const bookmarksIntegrationDescription: DocumentFragment = sanitizeHTMLToDom(
'If enabled, order of files and folders in File Explorer will reflect the order '
+ 'of bookmarked items in the bookmarks (core plugin) view. Automatically, without any '
+ 'need for sorting configuration. At the same time, it integrates seamlessly with'
+ ' <pre style="display: inline;">sorting-spec:</pre> configurations and they can nicely cooperate.'
+ '<br>'
+ '<p>To separate regular bookmarks from the bookmarks created for sorting, you can put '
+ 'the latter in a separate dedicated bookmarks group. The default name of the group is '
+ "'<i>" + DEFAULT_SETTINGS.bookmarksGroupToConsumeAsOrderingReference + "</i>' "
+ 'and you can change the group name in the configuration field below.'
+ '<br>'
+ 'If left empty, all the bookmarked items will be used to impose the order in File Explorer.</p>'
+ '<p>More information on this functionality in the '
+ '<a href="https://github.com/SebastianMC/obsidian-custom-sort/blob/master/docs/manual.md#bookmarks-plugin-integration">'
+ 'manual</a> of this custom-sort plugin.'
+ '</p>'
)
new Setting(containerEl)
.setName('Automatic integration with core Bookmarks plugin (for indirect drag & drop ordering)')
.setDesc(bookmarksIntegrationDescription)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.automaticBookmarksIntegration)
.onChange(async (value) => {
this.plugin.settings.automaticBookmarksIntegration = value;
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Name of the group in Bookmarks from which to read the order of items')
.setDesc('See above.')
.addText(text => text
.setPlaceholder('e.g. Group for sorting')
.setValue(this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference)
.onChange(async (value) => {
value = groupNameForPath(value.trim()).trim()
this.plugin.settings.bookmarksGroupToConsumeAsOrderingReference = value ? pathToFlatString(normalizePath(value)) : '';
await this.plugin.saveSettings();
}));
const bookmarksIntegrationContextMenusDescription: DocumentFragment = sanitizeHTMLToDom(
'Enable <i>Custom-sort: bookmark for sorting</i> and <i>Custom-sort: bookmark+siblings for sorting</i> (and related) entries '
+ 'in context menu in File Explorer'
)
new Setting(containerEl)
.setName('Context menus for Bookmarks integration')
.setDesc(bookmarksIntegrationContextMenusDescription)
.addToggle(toggle => toggle
.setValue(this.plugin.settings.bookmarksContextMenus)
.onChange(async (value) => {
this.plugin.settings.bookmarksContextMenus = value;
if (value) {
this.plugin.settings.customSortContextSubmenu = true; // automatically enable custom sort context submenu
}
await this.plugin.saveSettings();
}))
}
}