first stable version

This commit is contained in:
Nick 2025-11-09 01:02:20 +01:00
parent 65dccdec74
commit ee20bb78c3
21 changed files with 4362 additions and 1 deletions

3
.eslintignore Normal file
View file

@ -0,0 +1,3 @@
node_modules/
main.js

23
.eslintrc Normal file
View file

@ -0,0 +1,23 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"env": { "node": true },
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parserOptions": {
"sourceType": "module"
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off"
}
}

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# vscode
.vscode
# Intellij
*.iml
.idea
# npm
node_modules
# Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead.
main.js
# Exclude sourcemaps
*.map
# obsidian
data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store

1
.npmrc Normal file
View file

@ -0,0 +1 @@
tag-version-prefix=""

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Nick
Copyright (c) 2025 Nick (Reifat)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

97
README.md Normal file
View file

@ -0,0 +1,97 @@
# Daily Notes Sorter
An Obsidian plugin that allows you to automatically sort daily notes by date in the file explorer.
## Description
**Daily Notes Sorter** is an Obsidian plugin that extends the file explorer functionality by adding the ability to automatically sort files by date in specified folders. The plugin analyzes file names, extracts dates according to the specified format, and sorts files in chronological order.
## Features
### 📁 Date Sorting
- Automatic sorting of files in specified folders by date
- Support for multiple date formats:
- `YYYY-MM-DD` (ISO format, e.g.: 2024-01-15)
- `DD.MM.YYYY` (European format, e.g.: 15.01.2024)
- `DD.MM.YY` (European short format, e.g.: 15.01.24)
- `MM/DD/YYYY` (US format, e.g.: 01/15/2024)
### 🔄 Sort Direction
- Ascending sort (from old to new)
- Descending sort (from new to old)
- Toggle sort direction via button in file explorer interface
### ⚙️ Flexible Configuration
- Configure individual folders with custom date formats
- Autocomplete folder paths when configuring
- Real-time path validation
- Settings persistence between sessions
### 🔔 Notifications
- Automatic notifications about files that don't match the specified date format
- Files without dates are placed at the beginning (descending) or end (ascending) of the list
## Usage
### Installation
#### Via Community Plugins (Recommended)
1. Open Obsidian Settings
2. Go to **Community plugins**
3. Make sure **Safe mode** is **off**
4. Click **Browse** and search for "Daily notes sorter"
5. Click **Install** and then **Enable**
#### Manual Installation
1. Download the latest release from the [GitHub repository](https://github.com/Reifat/daily-notes-sorter) (or clone the repository)
2. Copy the files `main.js`, `manifest.json`, and `styles.css` to `.obsidian/plugins/daily-notes-sorter/` folder in your vault
3. Reload Obsidian
4. Enable the plugin in Obsidian settings (Settings → Community plugins)
### Configuration
1. Open plugin settings (Settings → Daily notes sorter settings)
2. Click "Add item" to add a new folder
3. Enter folder path (you can use autocomplete)
4. Select date format from dropdown
5. Click "Apply settings" to apply changes
### Using in File Explorer
1. After configuring folders, sorting is applied automatically
2. A button for toggling sort direction will appear in the file explorer header
3. Click the button to toggle between ascending and descending sort
> **For developers:** Technical documentation, including plugin architecture, technical details, development instructions, and implementation details, can be found in [`src/README.md`](src/README.md).
## Known Limitations
1. **Date format at the beginning of file name:** The plugin only searches for dates at the beginning of file names. If the date is in the middle or end of the name, it will not be recognized.
2. **Files only:** The plugin sorts only files (TFile), folders remain in their places.
3. **FileExplorer dependency:** The plugin requires the built-in "Files" (FileExplorer) plugin to be enabled.
4. **One format per folder:** Each folder can have only one date format. If a folder contains files with different date formats, they may be sorted incorrectly.
## Future Improvements
- Support for additional date formats
- Search for date anywhere in file name
- Sort folders by date
- Group files by months/years
- Export/import settings
## License
MIT
## Author
Nick (GitHub: [@Reifat](https://github.com/Reifat))
## Support
If you found a bug or have a suggestion for improvement, please create an issue in the project repository.

49
esbuild.config.mjs Normal file
View file

@ -0,0 +1,49 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === "production");
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ["src/main.ts"],
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2018",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
minify: prod,
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "daily-notes-sorter",
"name": "Daily notes sorter",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Automatically sort daily notes and files by date in the file explorer. Supports multiple date formats (YYYY-MM-DD, DD.MM.YYYY, DD.MM.YY, MM/DD/YYYY) and flexible folder configuration.",
"author": "Nick",
"authorUrl": "https://github.com/Reifat",
"isDesktopOnly": false
}

2491
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

27
package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "daily-notes-sorter",
"version": "1.0.0",
"description": "Obsidian plugin for automatically sorting daily notes by date in the file explorer",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
},
"keywords": ["obsidian", "obsidian-plugin", "daily-notes", "sorting", "file-explorer"],
"author": "Reifat <https://github.com/Reifat>",
"license": "MIT",
"devDependencies": {
"@types/node": "^20.0.0",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"esbuild": "^0.25.12",
"typescript": "^5.3.0",
"monkey-around": "^3.0.0",
"obsidian": "latest",
"tslib": "2.4.0",
"moment": "^2.30.1",
"moment-timezone": "^0.5.48",
"builtin-modules": "3.3.0"
}
}

218
src/README.md Normal file
View file

@ -0,0 +1,218 @@
# Technical Documentation
This document contains technical documentation for developers of the Daily Notes Sorter plugin.
## Plugin Architecture
### Project Structure
```
src/
├── main.ts # Main plugin file, entry point
├── core/
│ └── sorter.ts # File sorting logic by date
├── file-explorer-utils.ts # Utilities for working with FileExplorer and patching
└── ui/
├── settings.ts # Plugin settings interface
├── explorer-ui.ts # UI for file explorer (sort button)
└── autocomplete-input.ts # Autocomplete component for path input
```
### Main Components
#### 1. `DailyNotesSorter` (main.ts)
Main plugin class that:
- Initializes all components on load
- Manages plugin settings (load/save)
- Coordinates work between components
- Integrates with Obsidian daily notes settings
**Key methods:**
- `onload()` — plugin initialization
- `loadSettings()` — load saved settings
- `saveSettings()` — save settings
#### 2. `Sorter` (core/sorter.ts)
Class responsible for sorting logic:
- Extracts date from file name according to specified format
- Sorts files by date (year → month → day)
- Handles files without dates
- Shows notifications about files that don't match the format
**Key methods:**
- `extractDate(fileName, dateFormat)` — extract date from file name
- `sortFolder(folder, fileItems, ascending, dateFormat)` — sort files in folder
**Supported formats:**
- Regular expressions for each date format
- Automatic conversion of two-digit year (YY) to four-digit
- Format validation before parsing
#### 3. `FileExplorerUtils` (file-explorer-utils.ts)
Utilities for working with file explorer:
- Patching FileExplorer sort methods via `monkey-around` library
- Support for different Obsidian API versions (before and after 1.6.0)
- Sort direction management
- Applying sorting to file explorer
**Key methods:**
- `patchFileExplorerFolder()` — apply patch for custom sorting
- `applySort()` — apply sorting to FileExplorer
- `setSortDirection()` — set sort direction
- `getFolderSettings()` — get settings for specific folder
**Patching features:**
- For Obsidian >= 1.6.0: patching `getSortedFolderItems()` method
- For Obsidian < 1.6.0: patching `sort()` method in Folder prototype
- Automatic API version detection
#### 4. `SorterSettings` (ui/settings.ts)
Plugin settings interface:
- Add/remove folders for sorting
- Select date format for each folder
- Path validation
- Path autocomplete on input
**Key features:**
- Folder list caching for performance
- Real-time validation
- Visual error indication (red border) and successful validation (green border)
- Automatic folder list update on vault changes
**UI elements:**
- Path input field with autocomplete
- Date format dropdown
- Delete item button
- Add new item button
- Apply settings button
#### 5. `ExplorerUI` (ui/explorer-ui.ts)
File explorer UI:
- Add sort button to FileExplorer header
- Toggle between ascending and descending sort
- Visual button styling
**Integration:**
- Uses `addSortButton()` method from FileExplorer API
- Saves sort state between sessions
#### 6. `AutocompleteInput` (ui/autocomplete-input.ts)
Autocomplete component:
- Filter folder list by entered text
- Display suggestions in dropdown
- Automatic list height adjustment to content
- Limit number of displayed suggestions (20 items)
**Features:**
- Adaptive list height considering available screen space
- Scrolling with many suggestions
- Focus and blur event handling
- Duplicate prevention in suggestion list
## Technical Details
### Dependencies
- **obsidian** — Obsidian API for plugin development
- **monkey-around** — library for method patching
- **moment** — date handling (used for getting daily notes settings)
- **typescript** — development language
### Compatibility
- **Minimum Obsidian version:** 0.15.0
- **API version support:**
- Obsidian < 1.6.0: patching via `createFolderDom()`
- Obsidian >= 1.6.0: patching via `getSortedFolderItems()`
### Settings Data Structure
```typescript
interface DailyNotesSorterSettings {
items: FolderItem[]; // List of folders for sorting
sortAscending: boolean; // Sort direction
}
interface FolderItem {
path: string; // Folder path
dateFormat: string; // Date format (YYYY-MM-DD, DD.MM.YYYY, etc.)
}
```
### Sorting Algorithm
1. **Date extraction:** Plugin analyzes the beginning of file name and attempts to extract date according to specified format
2. **File separation:** Files are separated into two groups:
- Files with date (will be sorted by date)
- Files without date (will be sorted by name)
3. **Sorting:**
- Files with date are sorted by year → month → day
- Files without date are sorted by name (alphabetically)
4. **Combining:**
- With ascending sort: files with date first, then without date
- With descending sort: files without date first, then with date
### Error Handling
- Path validation before applying settings
- Check for folder existence in vault
- Error handling when loading/saving settings
- Notifications about files that don't match date format
- Error logging to console for debugging
## Development
### Requirements
- Node.js >= 16
- npm or yarn
### Installing Dependencies
```bash
npm install
```
### Development
```bash
npm run dev
```
This command starts build in watch mode, automatically rebuilding the plugin when source files change.
### Production Build
```bash
npm run build
```
### Build Structure
- `main.js` — compiled JavaScript plugin file
- `styles.css` — styles for UI components
- `manifest.json` — plugin manifest with metadata
## Implementation Details
### FileExplorer Patching
The plugin uses the `monkey-around` library to intercept FileExplorer sort methods. This allows:
- Not modifying Obsidian source code
- Working with different API versions
- Correctly handling patch removal when plugin is unloaded
### Caching
For performance improvement, caching is used:
- Folder list in vault is cached in settings
- Cache is automatically invalidated when vault structure changes
- Cache update on file/folder create/delete/rename
### Validation
Multi-level validation:
- Check folder existence when entering path
- Visual indication (input field border color)
- Check all items before applying settings
- Prevent adding empty items

184
src/core/sorter.ts Normal file
View file

@ -0,0 +1,184 @@
import { TFolder, TFile, TAbstractFile, App, Notice } from 'obsidian';
class Data {
day: number
month: number
year: number
constructor(day: string, month: string, year: string) {
this.day = Number(day);
this.month = Number(month);
this.year = Number(year);
}
}
class SortingItem {
data: Data | undefined
path: string
basename: string
constructor(data: Data | undefined, path: string, basename: string) {
this.data = data;
this.path = path;
this.basename = basename;
}
}
export class Sorter {
private app: App | null = null;
constructor(app?: App) {
this.app = app || null;
}
/**
* Extracts date from file name according to specified format
* Supported formats: YYYY-MM-DD, DD.MM.YYYY, DD.MM.YY, MM/DD/YYYY
* Parses date from the beginning of file name
*/
private extractDate(fileName: string, dateFormat: string): Data | undefined {
let day: string, month: string, year: string;
let match: RegExpMatchArray | null = null;
switch (dateFormat) {
case "YYYY-MM-DD": {
// Format: 2024-01-15 (search at the beginning of string)
match = fileName.match(/^(\d{4})-(\d{1,2})-(\d{1,2})/);
if (match) {
year = match[1];
month = match[2];
day = match[3];
} else {
return undefined;
}
break;
}
case "DD.MM.YYYY": {
// Format: 15.01.2024 (search at the beginning of string)
match = fileName.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})/);
if (match) {
day = match[1];
month = match[2];
year = match[3];
} else {
return undefined;
}
break;
}
case "DD.MM.YY": {
// Format: 15.01.24 (search at the beginning of string)
match = fileName.match(/^(\d{1,2})\.(\d{1,2})\.(\d{2})/);
if (match) {
day = match[1];
month = match[2];
const yy = parseInt(match[3], 10);
// Convert two-digit year to four-digit
// YY < 50 -> 20YY, otherwise 19YY
year = yy < 50 ? `20${match[3]}` : `19${match[3]}`;
} else {
return undefined;
}
break;
}
case "MM/DD/YYYY": {
// Format: 01/15/2024 (search at the beginning of string)
match = fileName.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})/);
if (match) {
month = match[1];
day = match[2];
year = match[3];
} else {
return undefined;
}
break;
}
default:
console.warn(`[Sorter] Unsupported date format: ${dateFormat}`);
return undefined;
}
return new Data(day, month, year);
}
/**
* Sorts files in folder by date (year, month, day)
* Files not matching the format are placed at the top (descending) or bottom (ascending)
* @param sortedFolder - folder to sort
* @param fileItems - collection of all file explorer items
* @param ascending - sort direction: true for ascending, false for descending
* @param dateFormat - date format for parsing (YYYY-MM-DD, DD.MM.YYYY, MM/DD/YYYY)
* @returns sorted array of items
*/
sortFolder(sortedFolder: TFolder, fileItems: any, ascending: boolean = true, dateFormat: string = "YYYY-MM-DD"): any[] {
const allFileItemsCollection = fileItems;
// Separate files into matching and non-matching format
const filesWithDate: Array<SortingItem> = [];
const filesWithoutDate: Array<SortingItem> = [];
sortedFolder.children
.filter((entry): entry is TFile => {
return entry instanceof TFile;
})
.forEach((item: TFile) => {
const dateData = this.extractDate(item.basename, dateFormat);
const sortingItem = new SortingItem(dateData, item.path, item.basename);
if (dateData) {
filesWithDate.push(sortingItem);
} else {
filesWithoutDate.push(sortingItem);
}
});
// Show notification if there are files not matching the format
if (filesWithoutDate.length > 0 && this.app) {
const fileNames = filesWithoutDate.slice(0, 3).map(item => item.basename).join(", ");
const moreFiles = filesWithoutDate.length > 3 ? ` and ${filesWithoutDate.length - 3} more` : "";
const message = `Found ${filesWithoutDate.length} file(s) in folder "${sortedFolder.path}" that do not match format ${dateFormat}: ${fileNames}${moreFiles}`;
new Notice(message, 5000);
}
// Sort files with date
if (filesWithDate.length > 0) {
filesWithDate.sort((a: SortingItem, b: SortingItem) => {
if (!a.data || !b.data) {
return !a.data ? 1 : -1;
}
const diffYear = a.data.year - b.data.year;
if (diffYear === 0) {
const diffMonth = a.data.month - b.data.month;
if (diffMonth === 0) {
const diff = a.data.day - b.data.day;
return ascending ? diff : -diff;
}
return ascending ? diffMonth : -diffMonth;
}
return ascending ? diffYear : -diffYear;
});
}
// Sort files without date by name (for consistency)
filesWithoutDate.sort((a: SortingItem, b: SortingItem) => {
return a.basename.localeCompare(b.basename);
});
// Combine results: files without date at top (descending) or bottom (ascending)
let resultItems: Array<SortingItem>;
if (ascending) {
// Ascending: files with date first, then without date
resultItems = [...filesWithDate, ...filesWithoutDate];
} else {
// Descending: files without date first, then with date
resultItems = [...filesWithoutDate, ...filesWithDate];
}
// Convert to FileExplorer items
const result = resultItems
.map((item: SortingItem) => allFileItemsCollection[item.path])
.filter(item => item !== undefined);
return result;
}
}

229
src/file-explorer-utils.ts Normal file
View file

@ -0,0 +1,229 @@
import { App, Plugin, requireApiVersion, FileExplorerView, TFolder } from 'obsidian';
import { around } from 'monkey-around';
import { Sorter } from './core/sorter';
import { FolderItem } from './main';
// the monkey-around package doesn't export the below type
type MonkeyAroundUninstaller = () => void;
export class FileExplorerUtils {
private app: App;
private plugin: Plugin;
private sorter: Sorter;
private settings: { items: FolderItem[]; sortAscending?: boolean };
private pluginInstance: any; // Reference to plugin instance for saving settings
public sortAscending: boolean = true; // Public for access from closure
constructor(app: App, plugin: Plugin, sorter: Sorter, settings: { items: FolderItem[]; sortAscending?: boolean }, pluginInstance?: any) {
this.app = app;
this.plugin = plugin;
this.sorter = sorter;
this.settings = settings;
this.pluginInstance = pluginInstance;
// Restore saved sort state
if (settings.sortAscending !== undefined) {
this.sortAscending = settings.sortAscending;
}
}
/**
* Updates settings (called when plugin settings change)
*/
updateSettings(settings: { items: FolderItem[]; sortAscending?: boolean }): void {
this.settings = settings;
// Update sort state if it exists in settings
if (settings.sortAscending !== undefined) {
this.sortAscending = settings.sortAscending;
}
}
/**
* Checks if sorting is enabled for specified path
* @param path - path to folder
* @returns object with folder settings information or undefined
*/
private getFolderSettings(path: string): FolderItem | undefined {
return this.settings.items.find(item => {
// Compare paths, accounting for possible variants (with "/" and without)
const normalizedItemPath = item.path.startsWith("/") ? item.path : `/${item.path}`;
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return item.path === path || normalizedItemPath === normalizedPath;
});
}
/**
* Sets sort direction and updates display
* @param ascending - true for ascending, false for descending
* @param saveSettings - whether to save settings (default true)
*/
setSortDirection(ascending: boolean, saveSettings: boolean = true): void {
this.sortAscending = ascending;
// Update plugin settings
if (this.settings) {
this.settings.sortAscending = ascending;
}
// Save settings if required
if (saveSettings && this.pluginInstance) {
this.pluginInstance.saveSettings().catch((err: Error) => {
console.error("[FileExplorerUtils] Error saving sort direction:", err);
});
}
// Update sorting after changing direction
this.applySort();
}
/**
* Applies sorting to FileExplorer
*/
applySort(): void {
const fileExplorerView = this.getFileExplorer();
if (fileExplorerView && typeof fileExplorerView.requestSort === 'function') {
fileExplorerView.requestSort();
}
}
/**
* Gets current sort direction
*/
getSortDirection(): boolean {
return this.sortAscending;
}
/**
* Gets FileExplorerView instance from workspace
*/
getFileExplorer(): FileExplorerView | undefined {
let fileExplorer: FileExplorerView | undefined = this.app.workspace.getLeavesOfType("file-explorer")?.first()
?.view as unknown as FileExplorerView;
return fileExplorer;
}
/**
* Waits for FileExplorer to load and executes callback
* @param callback - function that will be called when FileExplorer loads
* @param interval - check interval in milliseconds (default 100)
* @param timeout - wait timeout in milliseconds (default 5000)
*/
async waitForFileExplorer(
callback: (fileExplorer: FileExplorerView) => void,
interval: number = 100,
timeout: number = 5000
): Promise<void> {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkFileExplorer = () => {
const fileExplorer = this.getFileExplorer();
if (fileExplorer) {
clearInterval(timer);
callback(fileExplorer);
resolve();
} else if (Date.now() - startTime > timeout) {
clearInterval(timer);
reject(new Error("[FileExplorerUtils] FileExplorer was not loaded, reason is time up!"));
}
};
const timer = setInterval(checkFileExplorer, interval);
});
}
/**
* Checks availability and patchability of FileExplorer
*/
checkFileExplorerIsAvailableAndPatchable(logWarning: boolean = true): FileExplorerView | undefined {
let fileExplorerView: FileExplorerView | undefined = this.getFileExplorer()
if (fileExplorerView && typeof fileExplorerView.requestSort === 'function') {
// The plugin integration points changed with Obsidian 1.6.0 hence the patchability-check should also be Obsidian version aware
if (requireApiVersion && requireApiVersion("1.6.0")) {
if (typeof fileExplorerView.getSortedFolderItems === 'function') {
return fileExplorerView
}
} else { // Obsidian versions prior to 1.6.0
if (typeof fileExplorerView.createFolderDom === 'function') {
return fileExplorerView
}
}
}
// Various scenarios when File Explorer was turned off (e.g. by some other plugin)
if (logWarning) {
const msg = `custom-sort v${this.plugin.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)
}
return undefined
}
/**
* Applies patch to FileExplorer for custom sorting
*/
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
const utils = this;
const requestStandardObsidianSortAfter = (patchUninstaller: MonkeyAroundUninstaller|undefined) => {
return () => {
if (patchUninstaller) patchUninstaller()
const fileExplorerView: FileExplorerView | undefined = utils.checkFileExplorerIsAvailableAndPatchable(false)
if (fileExplorerView) {
fileExplorerView.requestSort()
}
}
}
// 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) {
if (requireApiVersion && requireApiVersion("1.6.0")) {
// Starting from Obsidian 1.6.0 the sorting mechanics has been significantly refactored internally in Obsidian
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(patchableFileExplorer.constructor.prototype, {
getSortedFolderItems(old: any) {
return function (this: any, ...args: any[]) {
const folderPath = args[0].path;
const folderSettings = utils.getFolderSettings(folderPath);
if (folderSettings) {
const ascending = utils.sortAscending;
const dateFormat = folderSettings.dateFormat;
return utils.sorter.sortFolder(args[0], this.fileItems, ascending, dateFormat);
}
else { // default sort
return old.call(this, ...args);
}
};
}
})
this.plugin.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
return true
} else {
// Up to Obsidian 1.6.0
const tmpFolder = this.app.vault.getRoot();
let Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor;
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
sort(old: any) {
return function (this: any, ...args: any[]) {
const folderPath = args[0].path;
const folderSettings = utils.getFolderSettings(folderPath);
if (folderSettings) {
const ascending = utils.sortAscending;
const dateFormat = folderSettings.dateFormat;
return utils.sorter.sortFolder(args[0], this.fileItems, ascending, dateFormat);
}
else { // default sort
return old.call(this, ...args);
}
};
}
})
this.plugin.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
return true
}
} else {
return false
}
}
}

166
src/main.ts Normal file
View file

@ -0,0 +1,166 @@
import {
App,
Plugin,
FileExplorerView } from 'obsidian';
import { SorterSettings } from './ui/settings';
import { Sorter } from './core/sorter';
import { FileExplorerUtils } from './file-explorer-utils';
import { ExplorerUI } from './ui/explorer-ui';
import type moment from "moment";
const DEFAULT_DAILY_NOTE_FORMAT = "YYYY-MM-DD";
declare global {
interface Window {
app: App;
moment: typeof moment;
}
}
interface IPeriodicNoteSettings {
folder?: string;
format?: string;
template?: string;
}
function getDailyNoteSettings(): IPeriodicNoteSettings {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { internalPlugins, plugins } = <any>window.app;
const { folder, format, template } =
internalPlugins.getPluginById("daily-notes")?.instance?.options || {};
return {
format: format || DEFAULT_DAILY_NOTE_FORMAT,
folder: folder?.trim() || "",
template: template?.trim() || "",
};
}
export interface FolderItem {
path: string;
dateFormat: string;
}
interface DailyNotesSorterSettings {
items: FolderItem[];
sortAscending: boolean;
}
const DEFAULT_SETTINGS: DailyNotesSorterSettings = {
items: [],
sortAscending: true
};
export default class DailyNotesSorter extends Plugin {
settings!: DailyNotesSorterSettings;
private sorter!: Sorter;
private fileExplorerUtils!: FileExplorerUtils;
private explorerUI!: ExplorerUI;
async onload() {
await this.loadSettings();
// Initialize sorter with App for showing notifications
this.sorter = new Sorter(this.app);
// Initialize utilities for working with FileExplorer
this.fileExplorerUtils = new FileExplorerUtils(this.app, this, this.sorter, this.settings, this);
// Initialize UI for FileExplorer
this.explorerUI = new ExplorerUI(this.fileExplorerUtils);
this.fileExplorerUtils.waitForFileExplorer((fileExplorer) => {
this.fileExplorerUtils.patchFileExplorerFolder(fileExplorer);
this.explorerUI.initialize(fileExplorer);
// Apply sorting after plugin load
// Use a small delay to ensure the patch is applied
setTimeout(() => {
this.fileExplorerUtils.applySort();
}, 100);
})
.catch((err: Error) => {
console.error("[DailyNotesSorter] Error initializing FileExplorer:", err);
// Note: We don't show a Notice here as the plugin might still work
// if FileExplorer loads later
});
this.addSettingTab(new SorterSettings(this.app, this));
}
onunload() {
// Cleanup is handled automatically by Obsidian:
// - Patches registered via this.plugin.register() are automatically uninstalled
// - Setting tabs are automatically removed
// - Event listeners registered via this.registerEvent() are automatically removed
// Explicit cleanup if needed (currently not required as all resources
// are managed through plugin.register() which handles cleanup automatically)
}
async loadSettings() {
try {
const loadedData = await this.loadData();
// Initialize settings with default values
this.settings = Object.assign({}, DEFAULT_SETTINGS);
// If there is loaded data, apply it
if (loadedData) {
if (loadedData.items && Array.isArray(loadedData.items)) {
this.settings.items = loadedData.items.map((item: any) => ({
path: item.path || "",
dateFormat: item.dateFormat || DEFAULT_DAILY_NOTE_FORMAT,
}));
}
// Load saved sort state
if (typeof loadedData.sortAscending === 'boolean') {
this.settings.sortAscending = loadedData.sortAscending;
}
}
} catch (error) {
console.error("Error loading settings:", error);
// In case of error, use default settings
this.settings = Object.assign({}, DEFAULT_SETTINGS);
}
}
async saveSettings() {
try {
// Ensure data structure is correct before saving
if (!this.settings) {
this.settings = { items: [], sortAscending: true };
}
// Validate and normalize data before saving
if (this.settings.items) {
this.settings.items = this.settings.items.map((item) => ({
path: item.path || "",
dateFormat: item.dateFormat || DEFAULT_DAILY_NOTE_FORMAT,
}));
} else {
this.settings.items = [];
}
// Save sort state from FileExplorerUtils
if (this.fileExplorerUtils) {
this.settings.sortAscending = this.fileExplorerUtils.sortAscending;
}
await this.saveData(this.settings);
// Update settings in FileExplorerUtils after saving
if (this.fileExplorerUtils) {
this.fileExplorerUtils.updateSettings(this.settings);
// Apply sorting after updating settings
this.fileExplorerUtils.applySort();
}
} catch (error) {
console.error("Error saving settings:", error);
}
}
}

View file

@ -0,0 +1,203 @@
/**
* Class for managing autocomplete in input field
*/
export class AutocompleteInput {
private suggestionsList: HTMLElement;
private inputEl: HTMLInputElement;
private onValueChange: (value: string) => void;
private getSuggestions: () => string[];
constructor(
container: HTMLElement,
inputEl: HTMLInputElement,
getSuggestions: () => string[],
onValueChange: (value: string) => void
) {
this.inputEl = inputEl;
this.onValueChange = onValueChange;
this.getSuggestions = getSuggestions;
this.suggestionsList = this.createSuggestionsList(container);
this.attachEventListeners();
}
/**
* Updates suggestion list if it's visible
*/
public refresh(): void {
if (!this.suggestionsList.hasClass("hidden")) {
const query = this.inputEl.value.trim();
if (query) {
this.handleInput();
} else {
// If query is empty but list is visible, update height
this.adjustListHeight();
}
}
}
/**
* Checks if suggestion list is visible
*/
public isVisible(): boolean {
return !this.suggestionsList.hasClass("hidden");
}
private createSuggestionsList(container: HTMLElement): HTMLElement {
return container.createDiv({
cls: "suggestions-list_1 hidden",
});
}
private attachEventListeners(): void {
this.inputEl.addEventListener("input", () => this.handleInput());
this.inputEl.addEventListener("focus", () => this.handleFocus());
this.inputEl.addEventListener("blur", () => this.handleBlur());
// Recalculate height on window resize
window.addEventListener("resize", () => {
if (!this.suggestionsList.hasClass("hidden")) {
this.adjustListHeight();
}
});
}
private handleInput(): void {
const query = this.inputEl.value.trim();
this.suggestionsList.empty();
if (!query) {
this.hideSuggestions();
return;
}
const filtered = this.filterSuggestions(query);
if (filtered.length > 0) {
this.showSuggestions(filtered);
} else {
this.hideSuggestions();
}
}
private handleFocus(): void {
if (this.inputEl.value.trim()) {
const query = this.inputEl.value.trim();
const filtered = this.filterSuggestions(query);
if (filtered.length > 0) {
// Clear list before showing suggestions
this.suggestionsList.empty();
this.showSuggestions(filtered);
}
}
}
private handleBlur(): void {
// Delay for handling click on suggestion
setTimeout(() => this.hideSuggestions(), 100);
}
private filterSuggestions(query: string): string[] {
const lowerQuery = query.toLowerCase();
const suggestions = this.getSuggestions();
return suggestions.filter((s) =>
s.toLowerCase().includes(lowerQuery)
);
}
private showSuggestions(suggestions: string[]): void {
// Always clear list before adding new elements
this.suggestionsList.empty();
this.suggestionsList.removeClass("hidden");
// Limit number of displayed suggestions for performance
const maxSuggestions = 20;
const limitedSuggestions = suggestions.slice(0, maxSuggestions);
// Get existing element texts for duplicate checking
const existingTexts = new Set<string>();
limitedSuggestions.forEach((suggestion) => {
// Check if such element already exists
if (!existingTexts.has(suggestion)) {
this.createSuggestionItem(suggestion);
existingTexts.add(suggestion);
}
});
// Show message if there are more suggestions
if (suggestions.length > maxSuggestions) {
const moreItem = this.suggestionsList.createDiv({
cls: "suggestion-item_1 suggestion-more",
text: `... and ${suggestions.length - maxSuggestions} more`,
});
moreItem.style.fontStyle = "italic";
moreItem.style.opacity = "0.7";
}
// Adjust list size to content after adding elements
// Use requestAnimationFrame for correct calculation after DOM update
requestAnimationFrame(() => {
this.adjustListHeight();
});
}
/**
* Adjusts list height to content considering available screen space
*/
private adjustListHeight(): void {
// Check if list is visible
if (this.suggestionsList.hasClass("hidden")) {
return;
}
// Reset any fixed heights for automatic calculation
this.suggestionsList.style.height = "auto";
// Get actual content height
const contentHeight = this.suggestionsList.scrollHeight;
// Calculate available screen space
const rect = this.suggestionsList.getBoundingClientRect();
const viewportHeight = window.innerHeight;
const spaceBelow = viewportHeight - rect.top;
const maxAllowedHeight = Math.min(400, Math.max(spaceBelow - 10, 50)); // Minimum 50px, 10px margin from screen edge
// Set height: either by content or maximum allowed
if (contentHeight <= maxAllowedHeight && contentHeight > 0) {
// Content fits, use its actual height
this.suggestionsList.style.height = `${contentHeight}px`;
this.suggestionsList.style.overflowY = "hidden";
} else if (contentHeight > 0) {
// Content doesn't fit, set maximum height with scrolling
this.suggestionsList.style.height = `${maxAllowedHeight}px`;
this.suggestionsList.style.overflowY = "auto";
}
}
private createSuggestionItem(suggestion: string): void {
// Check if element with such text already exists in list
const existingItems = Array.from(this.suggestionsList.children);
const alreadyExists = existingItems.some((child) => {
return child.textContent?.trim() === suggestion;
});
if (alreadyExists) {
return; // Skip if element already exists
}
const item = this.suggestionsList.createDiv({
cls: "suggestion-item_1",
text: suggestion,
});
item.onclick = () => {
this.inputEl.value = suggestion;
this.onValueChange(suggestion);
this.hideSuggestions();
};
}
private hideSuggestions(): void {
this.suggestionsList.addClass("hidden");
}
}

44
src/ui/explorer-ui.ts Normal file
View file

@ -0,0 +1,44 @@
import { FileExplorerView } from 'obsidian';
import { FileExplorerUtils } from '../file-explorer-utils';
export class ExplorerUI {
private fileExplorerUtils: FileExplorerUtils;
private readonly SORT_OPTIONS = [
{ id: 'ascending', label: 'Ascending' },
{ id: 'descending', label: 'Descending' }
];
constructor(fileExplorerUtils: FileExplorerUtils) {
this.fileExplorerUtils = fileExplorerUtils;
}
initialize(fileExplorer: FileExplorerView): void {
if (!fileExplorer?.headerDom.navButtonsEl) {
return;
}
const sortOptions: string[][] = [this.SORT_OPTIONS.map(opt => opt.id)];
const descriptions: Record<string, () => string> = {};
this.SORT_OPTIONS.forEach(opt => {
descriptions[opt.id] = () => opt.label;
});
fileExplorer.headerDom.addSortButton(
sortOptions,
descriptions,
(value: string | number) => {
const ascending = typeof value === 'string' ? value === 'ascending' : value === 0;
this.fileExplorerUtils.setSortDirection(ascending);
},
() => this.fileExplorerUtils.sortAscending ? 'ascending' : 'descending'
);
// Style the button
setTimeout(() => {
const button = fileExplorer.headerDom.navButtonsEl?.lastElementChild as HTMLElement;
button?.querySelector("svg")?.setAttribute("style", "color: #fff;");
}, 100);
}
}

408
src/ui/settings.ts Normal file
View file

@ -0,0 +1,408 @@
import { App, PluginSettingTab, Setting, TFolder, Notice } from 'obsidian';
import DailyNotesSorter from '../main';
import { AutocompleteInput } from './autocomplete-input';
// Configuration constants
const INPUT_PLACEHOLDER = "Example: /folder/note";
const INPUT_DESCRIPTION = "Enter the path to the folder";
const SETTINGS_TITLE = "Daily notes sorter settings";
const ADD_BUTTON_TEXT = "Add item";
const DATE_FORMAT_DESCRIPTION = "Select date format";
const APPLY_BUTTON_TEXT = "Apply settings";
// Date formats
const DATE_FORMATS = [
{ value: "YYYY-MM-DD", label: "YYYY-MM-DD (ISO)" },
{ value: "DD.MM.YYYY", label: "DD.MM.YYYY (European)" },
{ value: "DD.MM.YY", label: "DD.MM.YY (European short)" },
{ value: "MM/DD/YYYY", label: "MM/DD/YYYY (US)" },
] as const;
const DEFAULT_DATE_FORMAT = "YYYY-MM-DD";
export class SorterSettings extends PluginSettingTab {
plugin: DailyNotesSorter;
private itemsContainer: HTMLElement | null = null;
private cachedFolders: string[] | null = null;
private activeAutocompleteInputs: Set<AutocompleteInput> = new Set();
private inputElements: Map<number, HTMLInputElement> = new Map();
constructor(app: App, plugin: DailyNotesSorter) {
super(app, plugin);
this.plugin = plugin;
// Update cache and open lists when vault changes
this.plugin.registerEvent(
this.plugin.app.vault.on("create", () => {
this.invalidateCache();
})
);
this.plugin.registerEvent(
this.plugin.app.vault.on("delete", () => {
this.invalidateCache();
})
);
this.plugin.registerEvent(
this.plugin.app.vault.on("rename", () => {
this.invalidateCache();
})
);
}
/**
* Invalidates cache and updates all open suggestion lists
*/
private invalidateCache(): void {
this.cachedFolders = null;
// Update all visible suggestion lists
this.activeAutocompleteInputs.forEach((input) => {
if (input.isVisible()) {
input.refresh();
}
});
}
display(): void {
const { containerEl } = this;
containerEl.empty();
// Clear list of active components on redraw
this.activeAutocompleteInputs.clear();
this.inputElements.clear();
// Reset cache when opening settings
this.cachedFolders = null;
// Ensure settings are loaded
if (!this.plugin.settings) {
this.plugin.loadSettings();
}
this.renderHeader(containerEl);
this.itemsContainer = this.createItemsContainer(containerEl);
this.renderItems();
this.renderAddButton(containerEl);
this.renderApplyButton(containerEl);
}
private renderHeader(container: HTMLElement): void {
container.createEl("h2", { text: SETTINGS_TITLE });
}
private createItemsContainer(container: HTMLElement): HTMLElement {
return container.createDiv();
}
private renderItems(): void {
if (!this.itemsContainer) return;
this.itemsContainer.empty();
if (this.plugin.settings.items.length === 0) {
this.renderEmptyState();
return;
}
this.plugin.settings.items.forEach((item, index: number) => {
this.renderItem(item, index);
});
}
private renderEmptyState(): void {
if (!this.itemsContainer) return;
this.itemsContainer.createDiv({
cls: "setting-item-description",
text: "No items. Click 'Add item' to create a new one.",
});
}
private renderItem(item: { path: string; dateFormat?: string }, index: number): void {
if (!this.itemsContainer) return;
const itemDiv = this.itemsContainer.createDiv({
cls: "setting-item-custom-plugin",
});
const inputSetting = this.createInputSetting(itemDiv, item.path || "", index);
this.addDateFormatComboBox(inputSetting, item.dateFormat || DEFAULT_DATE_FORMAT, index);
this.addDeleteButton(inputSetting, index);
}
/**
* Gets list of all folders from vault
* Uses caching to improve performance
*/
private getFoldersFromVault(): string[] {
// Return cached list if available
if (this.cachedFolders !== null) {
return this.cachedFolders;
}
try {
const allFiles = this.plugin.app.vault.getAllLoadedFiles();
const folders = allFiles
.filter((file): file is TFolder => file instanceof TFolder)
.map((folder) => folder.path)
.sort(); // Sort for user convenience
// Cache the result
this.cachedFolders = folders;
return folders;
} catch (error) {
console.error("Error getting folders from vault:", error);
return [];
}
}
/**
* Validates path - checks if it exists in vault
*/
private validatePath(path: string): boolean {
if (!path || path.trim() === "") {
return false;
}
const trimmedPath = path.trim();
const file = this.plugin.app.vault.getAbstractFileByPath(trimmedPath);
return file !== null && file instanceof TFolder;
}
/**
* Validates all settings items
*/
private validateAllItems(): boolean {
let isValid = true;
this.plugin.settings.items.forEach((item, index) => {
const isValidPath = this.validatePath(item.path);
const inputEl = this.inputElements.get(index);
if (inputEl) {
if (!isValidPath) {
inputEl.addClass("input-error");
inputEl.removeClass("input-valid");
isValid = false;
} else {
inputEl.removeClass("input-error");
inputEl.addClass("input-valid");
}
} else {
if (!isValidPath) {
isValid = false;
}
}
});
return isValid;
}
/**
* Validates a single item by index
*/
private validateItem(index: number): boolean {
const item = this.plugin.settings.items[index];
if (!item) return false;
const isValid = this.validatePath(item.path);
const inputEl = this.inputElements.get(index);
if (inputEl) {
if (!isValid) {
inputEl.addClass("input-error");
inputEl.removeClass("input-valid");
} else {
inputEl.removeClass("input-error");
inputEl.addClass("input-valid");
}
}
return isValid;
}
private createInputSetting(
container: HTMLElement,
value: string,
index: number
): Setting {
const setting = new Setting(container)
.setName(`Item ${index + 1}`)
.setDesc(INPUT_DESCRIPTION)
.addText((text) => {
text.setPlaceholder(INPUT_PLACEHOLDER);
text.setValue(value);
// Save reference to input element for validation
this.inputElements.set(index, text.inputEl);
// Validation on load
if (value) {
this.validateItem(index);
}
// Create autocomplete component with folder retrieval function
const autocompleteInput = new AutocompleteInput(
container,
text.inputEl,
() => this.getFoldersFromVault(),
(newValue: string) => {
this.plugin.settings.items[index].path = newValue;
// Real-time validation
this.validateItem(index);
}
);
// Register component for real-time updates
this.activeAutocompleteInputs.add(autocompleteInput);
// Value change handler with real-time validation
text.onChange((newValue: string) => {
this.plugin.settings.items[index].path = newValue;
// Real-time validation
this.validateItem(index);
});
// Validation on blur
text.inputEl.addEventListener("blur", () => {
this.validateItem(index);
});
});
return setting;
}
/**
* Adds dropdown for date format selection
*/
private addDateFormatComboBox(
setting: Setting,
currentFormat: string,
index: number
): void {
setting.addDropdown((dropdown) => {
// Add options to dropdown
DATE_FORMATS.forEach((format) => {
dropdown.addOption(format.value, format.label);
});
// Set current value
dropdown.setValue(currentFormat || DEFAULT_DATE_FORMAT);
// Format change handler
dropdown.onChange((value: string) => {
this.plugin.settings.items[index].dateFormat = value;
});
});
}
private addDeleteButton(setting: Setting, index: number): void {
setting.addExtraButton((button) => {
button.setIcon("trash")
.setTooltip("Delete")
.onClick(async () => {
await this.deleteItem(index);
});
});
}
private async deleteItem(index: number): Promise<void> {
this.plugin.settings.items.splice(index, 1);
this.inputElements.delete(index);
// Update indices for remaining elements
const newMap = new Map<number, HTMLInputElement>();
this.inputElements.forEach((el, oldIndex) => {
if (oldIndex > index) {
newMap.set(oldIndex - 1, el);
} else if (oldIndex < index) {
newMap.set(oldIndex, el);
}
});
this.inputElements = newMap;
this.renderItems();
}
private renderAddButton(container: HTMLElement): void {
const buttonContainer = container.createDiv({
cls: "add-button-container",
});
buttonContainer.createEl("button", {
text: ADD_BUTTON_TEXT,
}).onclick = async () => {
await this.addItem();
};
}
private async addItem(): Promise<void> {
// Check if there are empty items before adding a new one
const hasEmptyItems = this.plugin.settings.items.some(
(item) => !item.path || item.path.trim() === ""
);
if (hasEmptyItems) {
new Notice("Please fill in all existing items before adding a new one.");
return;
}
this.plugin.settings.items.push({
path: "",
dateFormat: DEFAULT_DATE_FORMAT,
});
this.renderItems();
}
/**
* Renders apply settings button
*/
private renderApplyButton(container: HTMLElement): void {
const buttonContainer = container.createDiv({
cls: "apply-button-container",
});
const applyButton = buttonContainer.createEl("button", {
cls: "apply-button",
text: APPLY_BUTTON_TEXT,
});
applyButton.onclick = async () => {
await this.applySettings();
};
}
/**
* Applies settings: validates, saves and shows result
*/
private async applySettings(): Promise<void> {
// Remove empty items before validation
this.plugin.settings.items = this.plugin.settings.items.filter(
(item) => item.path && item.path.trim() !== ""
);
// Validate all items
const isValid = this.validateAllItems();
if (!isValid) {
new Notice("Please fix all errors before applying settings.");
return;
}
// Save settings
try {
await this.plugin.saveSettings();
// Remove all validation indicators after successful application
this.inputElements.forEach((inputEl) => {
inputEl.removeClass("input-error");
inputEl.removeClass("input-valid");
});
new Notice("Settings applied successfully!");
} catch (error) {
console.error("Error applying settings:", error);
new Notice("Error applying settings. Please try again.");
}
}
}

82
styles.css Normal file
View file

@ -0,0 +1,82 @@
.setting-item-custom-plugin {
position: relative; /* Теперь дочерние элементы позиционируются относительно этого контейнера */
width: 100%; /* Убедитесь, что контейнер занимает нужную ширину */
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
border-radius: 5px;
}
.suggestions-list_1 {
position: absolute; /* Позиционируется относительно setting-item-custom-plugin */
top: 100%; /* Начинается сразу под строкой ввода */
left: 0; /* Выравнивание по левому краю родителя */
width: 100%; /* Совпадает с шириной строки ввода */
background-color: var(--background-primary);
border: 1px solid var(--background-modifier-border);
max-height: 400px; /* Максимальная высота для предотвращения выхода за пределы экрана */
overflow-y: auto; /* Прокрутка появляется только при необходимости */
z-index: 1000;
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
border-radius: 5px;
/* Размер подстраивается под контент автоматически */
height: auto;
min-height: 0;
}
.suggestion-item_1 {
padding: 10px;
cursor: pointer;
color: var(--text-normal);
font-size: 0.9em;
border-bottom: 1px solid var(--background-modifier-border);
}
.suggestion-item_1:hover {
background-color: var(--background-modifier-hover);
}
.suggestion-item_1:last-child {
border-bottom: none;
}
.hidden {
display: none;
}
/* Стили для валидации */
.input-error {
border: 2px solid var(--text-error) !important;
background-color: var(--background-modifier-error) !important;
}
.input-valid {
border: 2px solid var(--text-success) !important;
}
.apply-button-container {
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--background-modifier-border);
}
.apply-button {
padding: 8px 20px;
background-color: var(--interactive-accent);
color: var(--text-on-accent);
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.apply-button:hover {
background-color: var(--interactive-accent-hover);
}
.apply-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}

23
tsconfig.json Normal file
View file

@ -0,0 +1,23 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2020",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "bundler",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"lib": ["ES2020", "DOM"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "main.js", "backup"]
}

77
types/types.d.ts vendored Normal file
View file

@ -0,0 +1,77 @@
import {PluginInstance, TFolder, WorkspaceLeaf} from "obsidian";
// Needed to support monkey-patching of the folder sort() function
declare module 'obsidian' {
export interface ViewRegistry {
viewByType: Record<string, (leaf: WorkspaceLeaf) => unknown>;
}
// undocumented internal interface - for experimental features
export interface PluginInstance {
id: string;
}
export type CommunityPluginId = string
// undocumented internal interface - for experimental features
export interface CommunityPlugin {
manifest: {
id: CommunityPluginId
}
_loaded: boolean
}
// undocumented internal interface - for experimental features
export interface CommunityPlugins {
enabledPlugins: Set<CommunityPluginId>
plugins: {[key: CommunityPluginId]: CommunityPlugin}
}
export interface App {
plugins: CommunityPlugins;
internalPlugins: InternalPlugins; // undocumented internal API - for experimental features
viewRegistry: ViewRegistry;
}
// undocumented internal interface - for experimental features
export interface InstalledPlugin {
enabled: boolean;
instance: PluginInstance;
}
// undocumented internal interface - for experimental features
export interface InternalPlugins {
plugins: Record<string, InstalledPlugin>;
getPluginById(id: string): InstalledPlugin;
}
interface FileExplorerFolder {
}
interface EI {
navHeaderEl: HTMLElement | null;
navButtonsEl: HTMLElement | null;
addNavButton(e: string, t: string, n: (event: Event) => void, i?: string): HTMLElement;
addSortButton(
e: string[][],
t: Record<string, () => string>,
n: (value: string | number) => void,
i: () => string | number
): void;
}
export interface FileExplorerView extends View {
createFolderDom(folder: TFolder): FileExplorerFolder;
getSortedFolderItems(sortedFolder: TFolder): any[];
requestSort(): void;
sortOrder: string;
headerDom: EI
}
interface MenuItem {
setSubmenu: () => Menu;
}
}

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}