#74 - Integration with Bookmarks core plugin and support for indirect drag & drop arrangement
- feature code complete - not reviewed - not tested - no unit tests coverage
This commit is contained in:
parent
08ffd7db9a
commit
56348006ce
|
@ -9,6 +9,7 @@ export enum CustomSortGroupType {
|
|||
ExactHeadAndTail, // Like W...n or Un...ed, which is shorter variant of typing the entire title
|
||||
HasMetadataField, // Notes (or folder's notes) containing a specific metadata field
|
||||
StarredOnly,
|
||||
BookmarkedOnly,
|
||||
HasIcon
|
||||
}
|
||||
|
||||
|
@ -30,6 +31,8 @@ export enum CustomSortOrder {
|
|||
byMetadataFieldAlphabeticalReverse,
|
||||
byMetadataFieldTrueAlphabeticalReverse,
|
||||
standardObsidian, // Let the folder sorting be in hands of Obsidian, whatever user selected in the UI
|
||||
byBookmarkOrder,
|
||||
byBookmarkOrderReverse,
|
||||
default = alphabetical
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
determineFolderDatesIfNeeded,
|
||||
determineSortingGroup,
|
||||
FolderItemForSorting,
|
||||
matchGroupRegex,
|
||||
matchGroupRegex, sorterByBookmarkOrder, sorterByMetadataField,
|
||||
SorterFn,
|
||||
Sorters
|
||||
} from './custom-sort';
|
||||
|
@ -16,6 +16,7 @@ import {
|
|||
ObsidianIconFolder_PluginInstance,
|
||||
ObsidianIconFolderPlugin_Data
|
||||
} from "../utils/ObsidianIconFolderPluginSignature";
|
||||
import {determineBookmarkOrder} from "../utils/BookmarksCorePluginSignature";
|
||||
|
||||
const mockTFile = (basename: string, ext: string, size?: number, ctime?: number, mtime?: number): TFile => {
|
||||
return {
|
||||
|
@ -2216,7 +2217,7 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
|
|||
const itemB: Partial<FolderItemForSorting> = {
|
||||
sortString: 'n123'
|
||||
}
|
||||
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
|
||||
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabetical]
|
||||
|
||||
// when
|
||||
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||
|
@ -2226,6 +2227,25 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
|
|||
expect(result1).toBe(SORT_FIRST_GOES_EARLIER)
|
||||
expect(result2).toBe(SORT_FIRST_GOES_LATER)
|
||||
})
|
||||
it('should put the item with metadata later if the second one has no metadata (reverse order)', () => {
|
||||
// given
|
||||
const itemA: Partial<FolderItemForSorting> = {
|
||||
metadataFieldValue: '15',
|
||||
sortString: 'n123'
|
||||
}
|
||||
const itemB: Partial<FolderItemForSorting> = {
|
||||
sortString: 'n123'
|
||||
}
|
||||
const sorter: SorterFn = Sorters[CustomSortOrder.byMetadataFieldAlphabeticalReverse]
|
||||
|
||||
// when
|
||||
const result1: number = sorter(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||
const result2: number = sorter(itemB as FolderItemForSorting, itemA as FolderItemForSorting)
|
||||
|
||||
// then
|
||||
expect(result1).toBe(SORT_FIRST_GOES_LATER)
|
||||
expect(result2).toBe(SORT_FIRST_GOES_EARLIER)
|
||||
})
|
||||
it('should correctly fallback to alphabetical reverse if no metadata on both items', () => {
|
||||
// given
|
||||
const itemA: Partial<FolderItemForSorting> = {
|
||||
|
@ -2247,3 +2267,74 @@ describe('CustomSortOrder.byMetadataFieldAlphabeticalReverse', () => {
|
|||
expect(result3).toBe(SORT_ITEMS_ARE_EQUAL)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sorterByMetadataField', () => {
|
||||
it.each([
|
||||
[true,'abc','def',-1, 'a', 'a'],
|
||||
[true,'xyz','klm',1, 'b', 'b'],
|
||||
[true,'mmm','mmm',0, 'c', 'c'],
|
||||
[true,'mmm','mmm',-1, 'd', 'e'],
|
||||
[true,'mmm','mmm',1, 'e', 'd'],
|
||||
[true,'abc',undefined,-1, 'a','a'],
|
||||
[true,undefined,'klm',1, 'b','b'],
|
||||
[true,undefined,undefined,0, 'a','a'],
|
||||
[true,undefined,undefined,-1, 'a','b'],
|
||||
[true,undefined,undefined,1, 'd','c'],
|
||||
[false,'abc','def',1, 'a', 'a'],
|
||||
[false,'xyz','klm',-1, 'b', 'b'],
|
||||
[false,'mmm','mmm',0, 'c', 'c'],
|
||||
[false,'mmm','mmm',1, 'd', 'e'],
|
||||
[false,'mmm','mmm',-1, 'e', 'd'],
|
||||
[false,'abc',undefined,1, 'a','a'],
|
||||
[false,undefined,'klm',-1, 'b','b'],
|
||||
[false,undefined,undefined,0, 'a','a'],
|
||||
[false,undefined,undefined,1, 'a','b'],
|
||||
[false,undefined,undefined,-1, 'd','c'],
|
||||
|
||||
])('straight order %s, comparing %s and %s should return %s for sortStrings %s and %s',
|
||||
(straight: boolean, metadataA: string|undefined, metadataB: string|undefined, order: number, sortStringA: string, sortStringB) => {
|
||||
const sorterFn = sorterByMetadataField(!straight, false)
|
||||
const itemA: Partial<FolderItemForSorting> = {metadataFieldValue: metadataA, sortString: sortStringA}
|
||||
const itemB: Partial<FolderItemForSorting> = {metadataFieldValue: metadataB, sortString: sortStringB}
|
||||
const result = sorterFn(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||
|
||||
// then
|
||||
expect(result).toBe(order)
|
||||
})
|
||||
})
|
||||
|
||||
describe('sorterByBookmarkOrder', () => {
|
||||
it.each([
|
||||
[true,10,20,-1, 'a', 'a'],
|
||||
[true,20,10,1, 'b', 'b'],
|
||||
[true,30,30,0, 'c', 'c'], // not possible in reality - each bookmark order is unique by definition - covered for clarity
|
||||
[true,1,1,0, 'd', 'e'], // ----//----
|
||||
[true,2,2,0, 'e', 'd'], // ----//----
|
||||
[true,3,undefined,-1, 'a','a'],
|
||||
[true,undefined,4,1, 'b','b'],
|
||||
[true,undefined,undefined,0, 'a','a'],
|
||||
[true,undefined,undefined,-1, 'a','b'],
|
||||
[true,undefined,undefined,1, 'd','c'],
|
||||
[false,10,20,1, 'a', 'a'],
|
||||
[false,20,10,-1, 'b', 'b'],
|
||||
[false,30,30,0, 'c', 'c'], // not possible in reality - each bookmark order is unique by definition - covered for clarity
|
||||
[false,1,1,0, 'd', 'e'], // ------//-----
|
||||
[false,2,2,0, 'e', 'd'], // ------//-----
|
||||
[false,3,undefined,1, 'a','a'],
|
||||
[false,undefined,4,-1, 'b','b'],
|
||||
[false,undefined,undefined,0, 'a','a'],
|
||||
[false,undefined,undefined,1, 'a','b'],
|
||||
[false,undefined,undefined,-1, 'd','c'],
|
||||
|
||||
])('straight order %s, comparing %s and %s should return %s for sortStrings %s and %s',
|
||||
(straight: boolean, bookmarkA: number|undefined, bookmarkB: number|undefined, order: number, sortStringA: string, sortStringB) => {
|
||||
const sorterFn = sorterByBookmarkOrder(!straight, false)
|
||||
const itemA: Partial<FolderItemForSorting> = {bookmarkedIdx: bookmarkA, sortString: sortStringA}
|
||||
const itemB: Partial<FolderItemForSorting> = {bookmarkedIdx: bookmarkB, sortString: sortStringB}
|
||||
const result = sorterFn(itemA as FolderItemForSorting, itemB as FolderItemForSorting)
|
||||
const normalizedResult = result < 0 ? -1 : ((result > 0) ? 1 : result)
|
||||
|
||||
// then
|
||||
expect(normalizedResult).toBe(order)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,26 +1,9 @@
|
|||
import {
|
||||
App,
|
||||
CommunityPlugin,
|
||||
FrontMatterCache,
|
||||
InstalledPlugin,
|
||||
requireApiVersion,
|
||||
TAbstractFile,
|
||||
TFile,
|
||||
TFolder
|
||||
} from 'obsidian';
|
||||
import {
|
||||
determineStarredStatusOf,
|
||||
getStarredPlugin,
|
||||
Starred_PluginInstance,
|
||||
StarredPlugin_findStarredFile_methodName
|
||||
} from '../utils/StarredPluginSignature';
|
||||
import {FrontMatterCache, requireApiVersion, TAbstractFile, TFile, TFolder} from 'obsidian';
|
||||
import {determineStarredStatusOf, getStarredPlugin, Starred_PluginInstance} from '../utils/StarredPluginSignature';
|
||||
import {
|
||||
determineIconOf,
|
||||
getIconFolderPlugin,
|
||||
FolderIconObject,
|
||||
ObsidianIconFolder_PluginInstance,
|
||||
ObsidianIconFolderPlugin_Data,
|
||||
ObsidianIconFolderPlugin_getData_methodName
|
||||
ObsidianIconFolder_PluginInstance
|
||||
} from '../utils/ObsidianIconFolderPluginSignature'
|
||||
import {
|
||||
CustomSortGroup,
|
||||
|
@ -32,6 +15,11 @@ import {
|
|||
RegExpSpec
|
||||
} from "./custom-sort-types";
|
||||
import {isDefined} from "../utils/utils";
|
||||
import {
|
||||
Bookmarks_PluginInstance,
|
||||
determineBookmarkOrder,
|
||||
getBookmarksPlugin
|
||||
} from "../utils/BookmarksCorePluginSignature";
|
||||
|
||||
let CollatorCompare = new Intl.Collator(undefined, {
|
||||
usage: "sort",
|
||||
|
@ -45,6 +33,21 @@ let CollatorTrueAlphabeticalCompare = new Intl.Collator(undefined, {
|
|||
numeric: false,
|
||||
}).compare;
|
||||
|
||||
|
||||
export const SORTSPEC_FOR_AUTOMATIC_BOOKMARKS_INTEGRATION: CustomSortSpec = {
|
||||
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||
groups: [
|
||||
{
|
||||
order: CustomSortOrder.byBookmarkOrder,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}
|
||||
],
|
||||
outsidersGroupIdx: 0,
|
||||
targetFoldersPaths: [
|
||||
"Spec applied automatically to folder not having explicit spec when automatic integration with bookmarks is enabled"
|
||||
]
|
||||
}
|
||||
|
||||
export interface FolderItemForSorting {
|
||||
path: string
|
||||
groupIdx?: number // the index itself represents order for groups
|
||||
|
@ -55,6 +58,7 @@ export interface FolderItemForSorting {
|
|||
mtime: number // for a file mtime is obvious, for a folder = date of most recently modified child file
|
||||
isFolder: boolean
|
||||
folder?: TFolder
|
||||
bookmarkedIdx?: number // derived from Bookmarks core plugin position
|
||||
}
|
||||
|
||||
export type SorterFn = (a: FolderItemForSorting, b: FolderItemForSorting) => number
|
||||
|
@ -65,7 +69,7 @@ const TrueAlphabetical: boolean = true
|
|||
const ReverseOrder: boolean = true
|
||||
const StraightOrder: boolean = false
|
||||
|
||||
const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
|
||||
export const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
|
||||
const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare
|
||||
return (a: FolderItemForSorting, b: FolderItemForSorting) => {
|
||||
if (reverseOrder) {
|
||||
|
@ -81,13 +85,31 @@ const sorterByMetadataField:(reverseOrder?: boolean, trueAlphabetical?: boolean)
|
|||
}
|
||||
}
|
||||
// Item with metadata goes before the w/o metadata
|
||||
if (a.metadataFieldValue) return reverseOrder ? 1 : -1
|
||||
if (b.metadataFieldValue) return reverseOrder ? -1 : 1
|
||||
if (a.metadataFieldValue) return -1
|
||||
if (b.metadataFieldValue) return 1
|
||||
// Fallback -> requested sort by metadata, yet none of two items contain it, use alphabetical by name
|
||||
return collatorCompareFn(a.sortString, b.sortString)
|
||||
}
|
||||
}
|
||||
|
||||
export const sorterByBookmarkOrder:(reverseOrder?: boolean, trueAlphabetical?: boolean) => SorterFn = (reverseOrder: boolean, trueAlphabetical?: boolean) => {
|
||||
const collatorCompareFn: CollatorCompareFn = trueAlphabetical ? CollatorTrueAlphabeticalCompare : CollatorCompare
|
||||
return (a: FolderItemForSorting, b: FolderItemForSorting) => {
|
||||
if (reverseOrder) {
|
||||
[a, b] = [b, a]
|
||||
}
|
||||
if (a.bookmarkedIdx && b.bookmarkedIdx) {
|
||||
// By design the bookmark idx is unique per each item, so no need for secondary sorting if they are equal
|
||||
return a.bookmarkedIdx - b.bookmarkedIdx
|
||||
}
|
||||
// Item with bookmark order goes before the w/o bookmark info
|
||||
if (a.bookmarkedIdx) return -1
|
||||
if (b.bookmarkedIdx) return 1
|
||||
// Fallback -> requested sort by bookmark order, yet none of two items contain it, use alphabetical by name
|
||||
return collatorCompareFn(a.sortString, b.sortString)
|
||||
}
|
||||
}
|
||||
|
||||
export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
||||
[CustomSortOrder.alphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||
[CustomSortOrder.trueAlphabetical]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorTrueAlphabeticalCompare(a.sortString, b.sortString),
|
||||
|
@ -105,6 +127,8 @@ export let Sorters: { [key in CustomSortOrder]: SorterFn } = {
|
|||
[CustomSortOrder.byMetadataFieldTrueAlphabetical]: sorterByMetadataField(StraightOrder, TrueAlphabetical),
|
||||
[CustomSortOrder.byMetadataFieldAlphabeticalReverse]: sorterByMetadataField(ReverseOrder),
|
||||
[CustomSortOrder.byMetadataFieldTrueAlphabeticalReverse]: sorterByMetadataField(ReverseOrder, TrueAlphabetical),
|
||||
[CustomSortOrder.byBookmarkOrder]: sorterByBookmarkOrder(StraightOrder),
|
||||
[CustomSortOrder.byBookmarkOrderReverse]: sorterByBookmarkOrder(ReverseOrder),
|
||||
|
||||
// This is a fallback entry which should not be used - the plugin code should refrain from custom sorting at all
|
||||
[CustomSortOrder.standardObsidian]: (a: FolderItemForSorting, b: FolderItemForSorting) => CollatorCompare(a.sortString, b.sortString),
|
||||
|
@ -164,6 +188,7 @@ export const matchGroupRegex = (theRegex: RegExpSpec, nameForMatching: string):
|
|||
|
||||
export interface Context {
|
||||
starredPluginInstance?: Starred_PluginInstance
|
||||
bookmarksPluginInstance?: Bookmarks_PluginInstance
|
||||
iconFolderPluginInstance?: ObsidianIconFolder_PluginInstance
|
||||
}
|
||||
|
||||
|
@ -171,6 +196,7 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
let groupIdx: number
|
||||
let determined: boolean = false
|
||||
let matchedGroup: string | null | undefined
|
||||
let bookmarkedIdx: number | undefined
|
||||
let metadataValueToSortBy: string | undefined
|
||||
const aFolder: boolean = isFolder(entry)
|
||||
const aFile: boolean = !aFolder
|
||||
|
@ -264,12 +290,20 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
break
|
||||
case CustomSortGroupType.StarredOnly:
|
||||
if (ctx?.starredPluginInstance) {
|
||||
let starred: boolean = determineStarredStatusOf(entry, aFile, ctx.starredPluginInstance)
|
||||
const starred: boolean = determineStarredStatusOf(entry, aFile, ctx.starredPluginInstance)
|
||||
if (starred) {
|
||||
determined = true
|
||||
}
|
||||
}
|
||||
break
|
||||
case CustomSortGroupType.BookmarkedOnly:
|
||||
if (ctx?.bookmarksPluginInstance) {
|
||||
const bookmarkOrder: number | undefined = determineBookmarkOrder(entry.path, ctx.bookmarksPluginInstance)
|
||||
if (bookmarkOrder) { // safe ==> orders intentionally start from 1
|
||||
determined = true
|
||||
bookmarkedIdx = bookmarkOrder
|
||||
}
|
||||
}
|
||||
case CustomSortGroupType.HasIcon:
|
||||
if(ctx?.iconFolderPluginInstance) {
|
||||
let iconName: string | undefined = determineIconOf(entry, ctx.iconFolderPluginInstance)
|
||||
|
@ -363,7 +397,8 @@ export const determineSortingGroup = function (entry: TFile | TFolder, spec: Cus
|
|||
folder: aFolder ? (entry as TFolder) : undefined,
|
||||
path: entry.path,
|
||||
ctime: aFile ? entryAsTFile.stat.ctime : DEFAULT_FOLDER_CTIME,
|
||||
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME
|
||||
mtime: aFile ? entryAsTFile.stat.mtime : DEFAULT_FOLDER_MTIME,
|
||||
bookmarkedIdx: bookmarkedIdx
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,6 +415,17 @@ export const sortOrderNeedsFolderDates = (order: CustomSortOrder | undefined, se
|
|||
|| SortOrderRequiringFolderDate.has(secondary ?? CustomSortOrder.standardObsidian)
|
||||
}
|
||||
|
||||
const SortOrderRequiringBookmarksOrder = new Set<CustomSortOrder>([
|
||||
CustomSortOrder.byBookmarkOrder,
|
||||
CustomSortOrder.byBookmarkOrderReverse
|
||||
])
|
||||
|
||||
export const sortOrderNeedsBookmarksOrder = (order: CustomSortOrder | undefined, secondary?: CustomSortOrder): boolean => {
|
||||
// The CustomSortOrder.standardObsidian used as default because it doesn't require bookmarks order
|
||||
return SortOrderRequiringBookmarksOrder.has(order ?? CustomSortOrder.standardObsidian)
|
||||
|| SortOrderRequiringBookmarksOrder.has(secondary ?? CustomSortOrder.standardObsidian)
|
||||
}
|
||||
|
||||
// Syntax sugar for readability
|
||||
export type ModifiedTime = number
|
||||
export type CreatedTime = number
|
||||
|
@ -422,10 +468,32 @@ export const determineFolderDatesIfNeeded = (folderItems: Array<FolderItemForSor
|
|||
})
|
||||
}
|
||||
|
||||
// Order by bookmarks order can be applied independently of grouping by bookmarked status
|
||||
// This function determines the bookmarked order if the sorting criteria (of group or entire folder) requires it
|
||||
export const determineBookmarksOrderIfNeeded = (folderItems: Array<FolderItemForSorting>, sortingSpec: CustomSortSpec, plugin: Bookmarks_PluginInstance) => {
|
||||
if (!plugin) return
|
||||
|
||||
folderItems.forEach((item) => {
|
||||
const folderDefaultSortRequiresBookmarksOrder: boolean = !!(sortingSpec.defaultOrder && sortOrderNeedsBookmarksOrder(sortingSpec.defaultOrder))
|
||||
let groupSortRequiresBookmarksOrder: boolean = false
|
||||
if (!folderDefaultSortRequiresBookmarksOrder) {
|
||||
const groupIdx: number | undefined = item.groupIdx
|
||||
if (groupIdx !== undefined) {
|
||||
const groupOrder: CustomSortOrder | undefined = sortingSpec.groups[groupIdx].order
|
||||
groupSortRequiresBookmarksOrder = sortOrderNeedsBookmarksOrder(groupOrder)
|
||||
}
|
||||
}
|
||||
if (folderDefaultSortRequiresBookmarksOrder || groupSortRequiresBookmarksOrder) {
|
||||
item.bookmarkedIdx = determineBookmarkOrder(item.path, plugin)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]) {
|
||||
let fileExplorer = this.fileExplorer
|
||||
sortingSpec._mCache = sortingSpec.plugin?.app.metadataCache
|
||||
const starredPluginInstance: Starred_PluginInstance | undefined = getStarredPlugin(sortingSpec?.plugin?.app)
|
||||
const bookmarksPluginInstance: Bookmarks_PluginInstance | undefined = getBookmarksPlugin(sortingSpec?.plugin?.app)
|
||||
const iconFolderPluginInstance: ObsidianIconFolder_PluginInstance | undefined = getIconFolderPlugin(sortingSpec?.plugin?.app)
|
||||
|
||||
const folderItems: Array<FolderItemForSorting> = (sortingSpec.itemsToHide ?
|
||||
|
@ -437,6 +505,7 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
|||
.map((entry: TFile | TFolder) => {
|
||||
const itemForSorting: FolderItemForSorting = determineSortingGroup(entry, sortingSpec, {
|
||||
starredPluginInstance: starredPluginInstance,
|
||||
bookmarksPluginInstance: bookmarksPluginInstance,
|
||||
iconFolderPluginInstance: iconFolderPluginInstance
|
||||
})
|
||||
return itemForSorting
|
||||
|
@ -445,6 +514,10 @@ export const folderSort = function (sortingSpec: CustomSortSpec, order: string[]
|
|||
// Finally, for advanced sorting by modified date, for some folders the modified date has to be determined
|
||||
determineFolderDatesIfNeeded(folderItems, sortingSpec)
|
||||
|
||||
if (bookmarksPluginInstance) {
|
||||
determineBookmarksOrderIfNeeded(folderItems, sortingSpec, bookmarksPluginInstance)
|
||||
}
|
||||
|
||||
folderItems.sort(function (itA: FolderItemForSorting, itB: FolderItemForSorting) {
|
||||
return compareTwoItems(itA, itB, sortingSpec);
|
||||
});
|
||||
|
|
|
@ -37,6 +37,13 @@ starred:
|
|||
/:files starred:
|
||||
/folders starred:
|
||||
|
||||
:::: folder of bookmarks
|
||||
< by-bookmarks-order
|
||||
/: bookmarked:
|
||||
< by-bookmarks-order
|
||||
/ Abc
|
||||
> by-bookmarks-order
|
||||
|
||||
:::: Conceptual model
|
||||
/: Entities
|
||||
%
|
||||
|
@ -95,6 +102,13 @@ target-folder: tricky folder 2
|
|||
/:files starred:
|
||||
/folders starred:
|
||||
|
||||
target-folder: folder of bookmarks
|
||||
order-asc: by-bookmarks-order
|
||||
/:files bookmarked:
|
||||
order-asc: by-bookmarks-order
|
||||
/folders Abc
|
||||
order-desc: by-bookmarks-order
|
||||
|
||||
:::: Conceptual model
|
||||
/:files Entities
|
||||
%
|
||||
|
@ -205,6 +219,30 @@ const expectedSortSpecsExampleA: { [key: string]: CustomSortSpec } = {
|
|||
'tricky folder 2'
|
||||
]
|
||||
},
|
||||
"folder of bookmarks": {
|
||||
defaultOrder: CustomSortOrder.byBookmarkOrder,
|
||||
groups: [
|
||||
{
|
||||
filesOnly: true,
|
||||
order: CustomSortOrder.byBookmarkOrder,
|
||||
type: CustomSortGroupType.BookmarkedOnly
|
||||
},
|
||||
{
|
||||
exactText: "Abc",
|
||||
foldersOnly: true,
|
||||
order: CustomSortOrder.byBookmarkOrderReverse,
|
||||
type: CustomSortGroupType.ExactName
|
||||
},
|
||||
{
|
||||
order: CustomSortOrder.byBookmarkOrder,
|
||||
type: CustomSortGroupType.Outsiders
|
||||
}
|
||||
],
|
||||
outsidersGroupIdx: 2,
|
||||
targetFoldersPaths: [
|
||||
"folder of bookmarks"
|
||||
]
|
||||
},
|
||||
"Conceptual model": {
|
||||
groups: [{
|
||||
exactText: "Entities",
|
||||
|
|
|
@ -114,6 +114,7 @@ const OrderLiterals: { [key: string]: CustomSortOrderAscDescPair } = {
|
|||
'modified': {asc: CustomSortOrder.byModifiedTime, desc: CustomSortOrder.byModifiedTimeReverse},
|
||||
'advanced modified': {asc: CustomSortOrder.byModifiedTimeAdvanced, desc: CustomSortOrder.byModifiedTimeReverseAdvanced},
|
||||
'advanced created': {asc: CustomSortOrder.byCreatedTimeAdvanced, desc: CustomSortOrder.byCreatedTimeReverseAdvanced},
|
||||
'by-bookmarks-order': {asc: CustomSortOrder.byBookmarkOrder, desc: CustomSortOrder.byBookmarkOrderReverse},
|
||||
|
||||
// Advanced, for edge cases of secondary sorting, when if regexp match is the same, override the alphabetical sorting by full name
|
||||
'a-z, created': {
|
||||
|
@ -207,6 +208,8 @@ const HideItemVerboseLexeme: string = '/--hide:'
|
|||
|
||||
const MetadataFieldIndicatorLexeme: string = 'with-metadata:'
|
||||
|
||||
const BookmarkedItemIndicatorLexeme: string = 'bookmarked:'
|
||||
|
||||
const StarredItemsIndicatorLexeme: string = 'starred:'
|
||||
|
||||
const IconIndicatorLexeme: string = 'with-icon:'
|
||||
|
@ -1514,6 +1517,13 @@ export class SortingSpecProcessor {
|
|||
foldersOnly: spec.foldersOnly,
|
||||
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||
}
|
||||
} else if (theOnly.startsWith(BookmarkedItemIndicatorLexeme)) {
|
||||
return {
|
||||
type: CustomSortGroupType.BookmarkedOnly,
|
||||
filesOnly: spec.filesOnly,
|
||||
foldersOnly: spec.foldersOnly,
|
||||
matchFilenameWithExt: spec.matchFilenameWithExt
|
||||
}
|
||||
} else if (theOnly.startsWith(IconIndicatorLexeme)) {
|
||||
const iconName: string | undefined = extractIdentifier(theOnly.substring(IconIndicatorLexeme.length))
|
||||
return {
|
||||
|
|
22
src/main.ts
22
src/main.ts
|
@ -16,7 +16,7 @@ import {
|
|||
Vault
|
||||
} from 'obsidian';
|
||||
import {around} from 'monkey-around';
|
||||
import {folderSort} from './custom-sort/custom-sort';
|
||||
import {folderSort, SORTSPEC_FOR_AUTOMATIC_BOOKMARKS_INTEGRATION} from './custom-sort/custom-sort';
|
||||
import {SortingSpecProcessor, SortSpecsCollection} from './custom-sort/sorting-spec-processor';
|
||||
import {CustomSortOrder, CustomSortSpec} from './custom-sort/custom-sort-types';
|
||||
|
||||
|
@ -36,6 +36,7 @@ interface CustomSortPluginSettings {
|
|||
statusBarEntryEnabled: boolean
|
||||
notificationsEnabled: boolean
|
||||
mobileNotificationsEnabled: boolean
|
||||
enableAutomaticBookmarksOrderIntegration: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: CustomSortPluginSettings = {
|
||||
|
@ -43,7 +44,8 @@ const DEFAULT_SETTINGS: CustomSortPluginSettings = {
|
|||
suspended: true, // if false by default, it would be hard to handle the auto-parse after plugin install
|
||||
statusBarEntryEnabled: true,
|
||||
notificationsEnabled: true,
|
||||
mobileNotificationsEnabled: false
|
||||
mobileNotificationsEnabled: false,
|
||||
enableAutomaticBookmarksOrderIntegration: false
|
||||
}
|
||||
|
||||
const SORTSPEC_FILE_NAME: string = 'sortspec.md'
|
||||
|
@ -347,6 +349,9 @@ export default class CustomSortPlugin extends Plugin {
|
|||
sortSpec = null // A folder is explicitly excluded from custom sorting plugin
|
||||
}
|
||||
}
|
||||
if (!sortSpec && plugin.settings.enableAutomaticBookmarksOrderIntegration) {
|
||||
sortSpec = SORTSPEC_FOR_AUTOMATIC_BOOKMARKS_INTEGRATION
|
||||
}
|
||||
if (sortSpec) {
|
||||
sortSpec.plugin = plugin
|
||||
return folderSort.call(this, sortSpec, ...args);
|
||||
|
@ -476,5 +481,18 @@ class CustomSortSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.mobileNotificationsEnabled = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName('Enable automatic integration with core Bookmarks plugin')
|
||||
// TODO: add a nice description here
|
||||
.setDesc('Details TBD. TODO: add a nice description here')
|
||||
.addToggle(toggle => toggle
|
||||
.setValue(this.plugin.settings.enableAutomaticBookmarksOrderIntegration)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.enableAutomaticBookmarksOrderIntegration = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
// TODO: expose additional configuration setting to specify group path in Bookmarks, if auto-integration with bookmarks is enabled
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
import {App, InstalledPlugin, PluginInstance} from "obsidian";
|
||||
|
||||
const BookmarksPlugin_getBookmarks_methodName = 'getBookmarks'
|
||||
|
||||
type Path = string
|
||||
|
||||
// Only relevant types of bookmarked items considered here
|
||||
// The full set of types also includes 'search', canvas, graph, maybe more to come
|
||||
type BookmarkedItem = BookmarkedFile | BookmarkedFolder | BookmarkedGroup
|
||||
|
||||
// Either a file, a folder or header/block inside a file
|
||||
interface BookmarkWithPath {
|
||||
path: Path
|
||||
}
|
||||
|
||||
interface BookmarkedFile {
|
||||
type: 'file'
|
||||
path: Path
|
||||
subpath?: string // Anchor within the file
|
||||
title?: string
|
||||
}
|
||||
|
||||
interface BookmarkedFolder {
|
||||
type: 'folder'
|
||||
path: Path
|
||||
title?: string
|
||||
}
|
||||
|
||||
interface BookmarkedGroup {
|
||||
type: 'group'
|
||||
items: Array<BookmarkedItem>
|
||||
title?: string
|
||||
}
|
||||
|
||||
export type BookmarkedItemPath = string
|
||||
|
||||
export interface OrderedBookmarkedItem {
|
||||
file: boolean
|
||||
folder: boolean
|
||||
path: BookmarkedItemPath
|
||||
order: number
|
||||
}
|
||||
|
||||
interface OrderedBookmarks {
|
||||
[key: BookmarkedItemPath]: OrderedBookmarkedItem
|
||||
}
|
||||
|
||||
export interface Bookmarks_PluginInstance extends PluginInstance {
|
||||
[BookmarksPlugin_getBookmarks_methodName]: () => Array<BookmarkedItem> | undefined
|
||||
}
|
||||
|
||||
let bookmarksCache: OrderedBookmarks | undefined = undefined
|
||||
let bookmarksCacheTimestamp: number | undefined = undefined
|
||||
|
||||
const CacheExpirationMilis = 1000 // One second seems to be reasonable
|
||||
|
||||
export const invalidateExpiredBookmarksCache = (force?: boolean): void => {
|
||||
if (bookmarksCache) {
|
||||
let flush: boolean = true
|
||||
if (!force && !!bookmarksCacheTimestamp) {
|
||||
if (Date.now() - CacheExpirationMilis <= bookmarksCacheTimestamp) {
|
||||
flush = false
|
||||
}
|
||||
}
|
||||
if (flush) {
|
||||
bookmarksCache = undefined
|
||||
bookmarksCacheTimestamp = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const BookmarksCorePluginId: string = 'bookmarks'
|
||||
|
||||
export const getBookmarksPlugin = (app?: App): Bookmarks_PluginInstance | undefined => {
|
||||
invalidateExpiredBookmarksCache()
|
||||
const bookmarksPlugin: InstalledPlugin | undefined = app?.internalPlugins?.getPluginById(BookmarksCorePluginId)
|
||||
console.log(bookmarksPlugin)
|
||||
const bookmarks = (bookmarksPlugin?.instance as any) ?.['getBookmarks']()
|
||||
console.log(bookmarks)
|
||||
if (bookmarksPlugin && bookmarksPlugin.enabled && bookmarksPlugin.instance) {
|
||||
const bookmarksPluginInstance: Bookmarks_PluginInstance = bookmarksPlugin.instance as Bookmarks_PluginInstance
|
||||
// defensive programming, in case Obsidian changes its internal APIs
|
||||
if (typeof bookmarksPluginInstance?.[BookmarksPlugin_getBookmarks_methodName] === 'function') {
|
||||
return bookmarksPluginInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type TraverseCallback = (item: BookmarkedItem) => boolean | void
|
||||
|
||||
const traverseBookmarksCollection = (items: Array<BookmarkedItem>, callback: TraverseCallback) => {
|
||||
const recursiveTraversal = (collection: Array<BookmarkedItem>) => {
|
||||
for (let idx = 0, collectionRef = collection; idx < collectionRef.length; idx++) {
|
||||
const item = collectionRef[idx];
|
||||
if (callback(item)) return;
|
||||
if ('group' === item.type) recursiveTraversal(item.items);
|
||||
}
|
||||
};
|
||||
recursiveTraversal(items);
|
||||
}
|
||||
|
||||
// TODO: extend this function to take a scope as parameter: a path to Bookmarks group to start from
|
||||
// Initially consuming all bookmarks is ok - finally the starting point (group) should be configurable
|
||||
const getOrderedBookmarks = (plugin: Bookmarks_PluginInstance): OrderedBookmarks | undefined => {
|
||||
const bookmarks: Array<BookmarkedItem> | undefined = plugin?.[BookmarksPlugin_getBookmarks_methodName]()
|
||||
if (bookmarks) {
|
||||
const orderedBookmarks: OrderedBookmarks = {}
|
||||
let order: number = 0
|
||||
const consumeItem = (item: BookmarkedItem) => {
|
||||
const isFile: boolean = item.type === 'file'
|
||||
const isAnchor: boolean = isFile && !!(item as BookmarkedFile).subpath
|
||||
const isFolder: boolean = item.type === 'folder'
|
||||
if ((isFile && !isAnchor) || isFolder) {
|
||||
const path = (item as BookmarkWithPath).path
|
||||
// Consume only the first occurrence of a path in bookmarks, even if many duplicates can exist
|
||||
const alreadyConsumed = orderedBookmarks[path]
|
||||
if (!alreadyConsumed) {
|
||||
orderedBookmarks[path] = {
|
||||
path: path,
|
||||
order: order++,
|
||||
file: isFile,
|
||||
folder: isFile
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
traverseBookmarksCollection(bookmarks, consumeItem)
|
||||
return orderedBookmarks
|
||||
}
|
||||
}
|
||||
|
||||
// Result:
|
||||
// undefined ==> item not found in bookmarks
|
||||
// > 0 ==> item found in bookmarks at returned position
|
||||
// Intentionally not returning 0 to allow simple syntax of processing the result
|
||||
export const determineBookmarkOrder = (path: string, plugin: Bookmarks_PluginInstance): number | undefined => {
|
||||
if (!bookmarksCache) {
|
||||
bookmarksCache = getOrderedBookmarks(plugin)
|
||||
bookmarksCacheTimestamp = Date.now()
|
||||
}
|
||||
|
||||
const bookmarkedItemPosition: number | undefined = bookmarksCache?.[path]?.order
|
||||
|
||||
return (bookmarkedItemPosition !== undefined && bookmarkedItemPosition >= 0) ? (bookmarkedItemPosition + 1) : undefined
|
||||
}
|
Loading…
Reference in New Issue