mirror of
https://github.com/reifat/daily-notes-sorter.git
synced 2026-07-22 06:42:08 +00:00
first stable version
This commit is contained in:
parent
65dccdec74
commit
ee20bb78c3
21 changed files with 4362 additions and 1 deletions
3
.eslintignore
Normal file
3
.eslintignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
|
||||
main.js
|
||||
23
.eslintrc
Normal file
23
.eslintrc
Normal 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
22
.gitignore
vendored
Normal 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
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
tag-version-prefix=""
|
||||
3
LICENSE
3
LICENSE
|
|
@ -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
97
README.md
Normal 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
49
esbuild.config.mjs
Normal 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
10
manifest.json
Normal 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
2491
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
27
package.json
Normal file
27
package.json
Normal 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
218
src/README.md
Normal 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
184
src/core/sorter.ts
Normal 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
229
src/file-explorer-utils.ts
Normal 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
166
src/main.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
203
src/ui/autocomplete-input.ts
Normal file
203
src/ui/autocomplete-input.ts
Normal 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
44
src/ui/explorer-ui.ts
Normal 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
408
src/ui/settings.ts
Normal 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
82
styles.css
Normal 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
23
tsconfig.json
Normal 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
77
types/types.d.ts
vendored
Normal 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
3
versions.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
}
|
||||
Loading…
Reference in a new issue