mirror of
https://github.com/reifat/daily-notes-sorter.git
synced 2026-07-22 06:42:08 +00:00
code style(format and naming convetion) have been added
This commit is contained in:
parent
1db557519c
commit
32e689bbb1
17 changed files with 2121 additions and 1576 deletions
11
.prettierignore
Normal file
11
.prettierignore
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
node_modules
|
||||
release
|
||||
dist
|
||||
build
|
||||
coverage
|
||||
.obsidian
|
||||
*.min.js
|
||||
*.min.css
|
||||
package-lock.json
|
||||
versions.json
|
||||
*.md
|
||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
|
|
@ -1,49 +1,49 @@
|
|||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import esbuild from 'esbuild';
|
||||
import process from 'process';
|
||||
import builtins from 'builtin-modules';
|
||||
|
||||
const banner =
|
||||
`/*
|
||||
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 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,
|
||||
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);
|
||||
await context.rebuild();
|
||||
process.exit(0);
|
||||
} else {
|
||||
await context.watch();
|
||||
await context.watch();
|
||||
}
|
||||
|
|
|
|||
222
eslint.config.js
222
eslint.config.js
|
|
@ -149,98 +149,162 @@ RULE CATALOG (what and why) — with tiny examples
|
|||
import z from "z";
|
||||
// blank line between builtin/external/internal, etc.
|
||||
*/
|
||||
import js from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import obsidianmd from "eslint-plugin-obsidianmd";
|
||||
import importPlugin from "eslint-plugin-import";
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import obsidianmd from 'eslint-plugin-obsidianmd';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import unicorn from 'eslint-plugin-unicorn';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
|
||||
// Scope type-checked rules to src/**/*.ts(x)
|
||||
const tsTypeChecked = tseslint.configs.recommendedTypeChecked.map((cfg) => ({
|
||||
...cfg,
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
...cfg,
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
}));
|
||||
const tsStrictTypeChecked = tseslint.configs.strictTypeChecked.map((cfg) => ({
|
||||
...cfg,
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
...cfg,
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
}));
|
||||
|
||||
export default [
|
||||
// Ignore build artifacts and non-source configs
|
||||
{ ignores: ["node_modules/**", "release/**", "main.js", "eslint.config.js", "scripts/**"] },
|
||||
// Ignore build artifacts and non-source configs
|
||||
{ ignores: ['node_modules/**', 'release/**', 'main.js', 'eslint.config.js', 'scripts/**'] },
|
||||
|
||||
// Base JS rules
|
||||
js.configs.recommended,
|
||||
// Base JS rules
|
||||
js.configs.recommended,
|
||||
|
||||
// Base TS rules (untyped) — syntax-level safety
|
||||
...tseslint.configs.recommended,
|
||||
// Base TS rules (untyped) — syntax-level safety
|
||||
...tseslint.configs.recommended,
|
||||
|
||||
// Enable typed linting for src TS files (required by many rules below)
|
||||
{
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: process.cwd(),
|
||||
},
|
||||
},
|
||||
},
|
||||
// Enable typed linting for src TS files (required by many rules below)
|
||||
{
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: process.cwd(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// TS recommendedTypeChecked scoped to src
|
||||
...tsTypeChecked,
|
||||
// TS strictTypeChecked scoped to src
|
||||
...tsStrictTypeChecked,
|
||||
// TS recommendedTypeChecked scoped to src
|
||||
...tsTypeChecked,
|
||||
// TS strictTypeChecked scoped to src
|
||||
...tsStrictTypeChecked,
|
||||
|
||||
// Obsidian rules for src TS
|
||||
{
|
||||
files: ["src/**/*.ts", "src/**/*.tsx"],
|
||||
plugins: { obsidianmd, import: importPlugin },
|
||||
rules: {
|
||||
"obsidianmd/sample-names": "off",
|
||||
"obsidianmd/prefer-file-manager-trash-file": "error",
|
||||
"obsidianmd/commands/no-command-in-command-id": "error",
|
||||
"obsidianmd/commands/no-command-in-command-name": "error",
|
||||
"obsidianmd/commands/no-default-hotkeys": "error",
|
||||
"obsidianmd/commands/no-plugin-id-in-command-id": "error",
|
||||
"obsidianmd/commands/no-plugin-name-in-command-name": "error",
|
||||
"obsidianmd/settings-tab/no-manual-html-headings": "error",
|
||||
"obsidianmd/settings-tab/no-problematic-settings-headings": "error",
|
||||
"obsidianmd/vault/iterate": "error",
|
||||
"obsidianmd/detach-leaves": "error",
|
||||
"obsidianmd/hardcoded-config-path": "error",
|
||||
"obsidianmd/no-forbidden-elements": "error",
|
||||
"obsidianmd/no-plugin-as-component": "error",
|
||||
"obsidianmd/no-sample-code": "error",
|
||||
"obsidianmd/no-tfile-tfolder-cast": "error",
|
||||
"obsidianmd/no-view-references-in-plugin": "error",
|
||||
"obsidianmd/no-static-styles-assignment": "error",
|
||||
"obsidianmd/object-assign": "error",
|
||||
"obsidianmd/platform": "error",
|
||||
"obsidianmd/regex-lookbehind": "error",
|
||||
"obsidianmd/validate-manifest": "error",
|
||||
"obsidianmd/validate-license": "error",
|
||||
"obsidianmd/ui/sentence-case": ["error", { enforceCamelCaseLower: true }],
|
||||
// Obsidian rules for src TS
|
||||
{
|
||||
files: ['src/**/*.ts', 'src/**/*.tsx'],
|
||||
plugins: { obsidianmd, import: importPlugin },
|
||||
rules: {
|
||||
'obsidianmd/sample-names': 'off',
|
||||
'obsidianmd/prefer-file-manager-trash-file': 'error',
|
||||
'obsidianmd/commands/no-command-in-command-id': 'error',
|
||||
'obsidianmd/commands/no-command-in-command-name': 'error',
|
||||
'obsidianmd/commands/no-default-hotkeys': 'error',
|
||||
'obsidianmd/commands/no-plugin-id-in-command-id': 'error',
|
||||
'obsidianmd/commands/no-plugin-name-in-command-name': 'error',
|
||||
'obsidianmd/settings-tab/no-manual-html-headings': 'error',
|
||||
'obsidianmd/settings-tab/no-problematic-settings-headings': 'error',
|
||||
'obsidianmd/vault/iterate': 'error',
|
||||
'obsidianmd/detach-leaves': 'error',
|
||||
'obsidianmd/hardcoded-config-path': 'error',
|
||||
'obsidianmd/no-forbidden-elements': 'error',
|
||||
'obsidianmd/no-plugin-as-component': 'error',
|
||||
'obsidianmd/no-sample-code': 'error',
|
||||
'obsidianmd/no-tfile-tfolder-cast': 'error',
|
||||
'obsidianmd/no-view-references-in-plugin': 'error',
|
||||
'obsidianmd/no-static-styles-assignment': 'error',
|
||||
'obsidianmd/object-assign': 'error',
|
||||
'obsidianmd/platform': 'error',
|
||||
'obsidianmd/regex-lookbehind': 'error',
|
||||
'obsidianmd/validate-manifest': 'error',
|
||||
'obsidianmd/validate-license': 'error',
|
||||
'obsidianmd/ui/sentence-case': ['error', { enforceCamelCaseLower: true }],
|
||||
|
||||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/no-misused-promises": ["error", { checksVoidReturn: false }],
|
||||
"@typescript-eslint/promise-function-async": "error",
|
||||
"@typescript-eslint/consistent-type-imports": ["error", { prefer: "type-imports", fixStyle: "separate-type-imports" }],
|
||||
"@typescript-eslint/explicit-function-return-type": ["error", { allowExpressions: true }],
|
||||
"@typescript-eslint/no-unsafe-assignment": "error",
|
||||
"@typescript-eslint/no-unsafe-member-access": "error",
|
||||
"@typescript-eslint/no-unsafe-call": "error",
|
||||
"@typescript-eslint/no-unsafe-return": "error",
|
||||
"@typescript-eslint/no-unsafe-argument": "error",
|
||||
"@typescript-eslint/restrict-template-expressions": ["error", { allowNumber: true, allowBoolean: false, allowNullish: false }],
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: false }],
|
||||
'@typescript-eslint/promise-function-async': 'error',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', fixStyle: 'separate-type-imports' },
|
||||
],
|
||||
'@typescript-eslint/explicit-function-return-type': ['error', { allowExpressions: true }],
|
||||
'@typescript-eslint/no-unsafe-assignment': 'error',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'error',
|
||||
'@typescript-eslint/no-unsafe-call': 'error',
|
||||
'@typescript-eslint/no-unsafe-return': 'error',
|
||||
'@typescript-eslint/no-unsafe-argument': 'error',
|
||||
'@typescript-eslint/restrict-template-expressions': [
|
||||
'error',
|
||||
{ allowNumber: true, allowBoolean: false, allowNullish: false },
|
||||
],
|
||||
|
||||
"eqeqeq": ["error", "smart"],
|
||||
"curly": ["error", "all"],
|
||||
"no-console": ["error", { allow: ["warn", "error", "debug"] }],
|
||||
// Naming convention (popular TS setup)
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
// Variables (includes imports): allow PascalCase for constructors/components, UPPER_CASE for consts
|
||||
{ selector: 'variable', format: ['camelCase', 'UPPER_CASE', 'PascalCase'] },
|
||||
// Parameters: allow leading underscore
|
||||
{ selector: 'parameter', format: ['camelCase'], leadingUnderscore: 'allow' },
|
||||
// Class members: default camelCase
|
||||
{ selector: 'memberLike', format: ['camelCase'], leadingUnderscore: 'allow' },
|
||||
// Static readonly members (constants) can be UPPER_CASE
|
||||
{
|
||||
selector: 'memberLike',
|
||||
modifiers: ['static', 'readonly'],
|
||||
format: ['UPPER_CASE', 'camelCase'],
|
||||
leadingUnderscore: 'allow',
|
||||
},
|
||||
// Types, classes, interfaces, enums, type aliases: PascalCase
|
||||
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||
// Enum members: PascalCase (common TS convention)
|
||||
{ selector: 'enumMember', format: ['PascalCase'] },
|
||||
// Type parameters: PascalCase with T-prefix
|
||||
{ selector: 'typeParameter', format: ['PascalCase'], prefix: ['T'] },
|
||||
// Interfaces should not be prefixed with I
|
||||
{
|
||||
selector: 'interface',
|
||||
format: ['PascalCase'],
|
||||
custom: { regex: '^I[A-Z]', match: false },
|
||||
},
|
||||
// Quoted properties are ignored
|
||||
{ selector: 'property', modifiers: ['requiresQuotes'], format: null },
|
||||
],
|
||||
|
||||
"import/order": ["error", {
|
||||
"groups": ["builtin", "external", "internal", "parent", "sibling", "index", "object", "type"],
|
||||
"newlines-between": "always",
|
||||
"alphabetize": { order: "asc", caseInsensitive: true }
|
||||
}]
|
||||
}
|
||||
}
|
||||
];
|
||||
eqeqeq: ['error', 'smart'],
|
||||
curly: ['error', 'all'],
|
||||
'no-console': ['error', { allow: ['warn', 'error', 'debug'] }],
|
||||
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'object',
|
||||
'type',
|
||||
],
|
||||
'newlines-between': 'always',
|
||||
alphabetize: { order: 'asc', caseInsensitive: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// Enforce file naming convention (kebab-case) for source files
|
||||
{
|
||||
files: ['src/**/*.{ts,tsx,js,jsx}'],
|
||||
plugins: { unicorn },
|
||||
rules: {
|
||||
'unicorn/filename-case': ['error', { cases: { kebabCase: true } }],
|
||||
},
|
||||
},
|
||||
|
||||
// Disable stylistic rules that conflict with Prettier
|
||||
eslintConfigPrettier,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -1,10 +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
|
||||
"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
|
||||
}
|
||||
|
|
|
|||
385
package-lock.json
generated
385
package-lock.json
generated
|
|
@ -18,13 +18,26 @@
|
|||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-obsidianmd": "^0.1.8",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"obsidian": "latest",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.3.0",
|
||||
"typescript-eslint": "^8.46.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@codemirror/state": {
|
||||
"version": "6.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
|
||||
|
|
@ -1529,6 +1542,16 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/baseline-browser-mapping": {
|
||||
"version": "2.8.25",
|
||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.25.tgz",
|
||||
"integrity": "sha512-2NovHVesVF5TXefsGX1yzx1xgr7+m9JQenvz6FQY3qd+YXkKkYiv+vTCc7OriP9mcDZpTC5mAOYN4ocd29+erA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"baseline-browser-mapping": "dist/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
|
|
@ -1552,6 +1575,40 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.27.0",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz",
|
||||
"integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.8.19",
|
||||
"caniuse-lite": "^1.0.30001751",
|
||||
"electron-to-chromium": "^1.5.238",
|
||||
"node-releases": "^2.0.26",
|
||||
"update-browserslist-db": "^1.1.4"
|
||||
},
|
||||
"bin": {
|
||||
"browserslist": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
|
|
@ -1660,6 +1717,27 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001754",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
|
||||
"integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
|
|
@ -1677,6 +1755,52 @@
|
|||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/change-case": {
|
||||
"version": "5.4.4",
|
||||
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ci-info": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz",
|
||||
"integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
|
||||
"integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-regexp/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
|
@ -1721,6 +1845,20 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.46.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.46.0.tgz",
|
||||
"integrity": "sha512-p9hObIIEENxSV8xIu+V68JjSeARg6UVMG5mR+JEUguG3sI6MsiS1njz2jHmyJDvA+8jX/sytkBHup6kxhM9law==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"browserslist": "^4.26.3"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
|
|
@ -1920,6 +2058,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.249",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.249.tgz",
|
||||
"integrity": "sha512-5vcfL3BBe++qZ5kuFhD/p8WOM1N9m3nwvJPULJx+4xf2usSlZFJ0qoNYO2fOX4hi3ocuDcmDobtA+5SFr4OmBg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
|
|
@ -2160,6 +2305,16 @@
|
|||
"@esbuild/win32-x64": "0.25.12"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-string-regexp": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
||||
|
|
@ -2249,6 +2404,22 @@
|
|||
"eslint": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "10.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
|
||||
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint-config-prettier"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-import-resolver-node": {
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
|
||||
|
|
@ -2672,6 +2843,55 @@
|
|||
"safe-regex": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unicorn": {
|
||||
"version": "62.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-62.0.0.tgz",
|
||||
"integrity": "sha512-HIlIkGLkvf29YEiS/ImuDZQbP12gWyx5i3C6XrRxMvVdqMroCI9qoVYCoIl17ChN+U89pn9sVwLxhIWj5nEc7g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@eslint-community/eslint-utils": "^4.9.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"change-case": "^5.4.4",
|
||||
"ci-info": "^4.3.1",
|
||||
"clean-regexp": "^1.0.0",
|
||||
"core-js-compat": "^3.46.0",
|
||||
"esquery": "^1.6.0",
|
||||
"find-up-simple": "^1.0.1",
|
||||
"globals": "^16.4.0",
|
||||
"indent-string": "^5.0.0",
|
||||
"is-builtin-module": "^5.0.0",
|
||||
"jsesc": "^3.1.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"regexp-tree": "^0.1.27",
|
||||
"regjsparser": "^0.13.0",
|
||||
"semver": "^7.7.3",
|
||||
"strip-indent": "^4.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.10.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=9.38.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
|
||||
"version": "16.5.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
|
||||
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
|
||||
|
|
@ -2984,6 +3204,19 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/find-up-simple": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
|
||||
"integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/flat-cache": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
||||
|
|
@ -3402,6 +3635,19 @@
|
|||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/indent-string": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
|
||||
"integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
|
|
@ -3495,6 +3741,35 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-builtin-module": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
|
||||
"integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"builtin-modules": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-builtin-module/node_modules/builtin-modules": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
|
||||
"integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-callable": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
|
||||
|
|
@ -3907,6 +4182,19 @@
|
|||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/json-buffer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
||||
|
|
@ -4238,6 +4526,13 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.27",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
|
||||
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
|
@ -4528,6 +4823,13 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
|
|
@ -4541,6 +4843,16 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/pluralize": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
|
||||
|
|
@ -4561,6 +4873,22 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
|
|
@ -4722,6 +5050,19 @@
|
|||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/regjsparser": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz",
|
||||
"integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"jsesc": "~3.1.0"
|
||||
},
|
||||
"bin": {
|
||||
"regjsparser": "bin/parser"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
|
|
@ -5340,6 +5681,19 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-indent": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz",
|
||||
"integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
|
@ -5670,6 +6024,37 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz",
|
||||
"integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.2.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"update-browserslist-db": "cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"browserslist": ">= 4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||
|
|
|
|||
79
package.json
79
package.json
|
|
@ -1,39 +1,44 @@
|
|||
{
|
||||
"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",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"release": "npm run lint && npm run build && node scripts/release.js"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"obsidian-plugin",
|
||||
"daily-notes",
|
||||
"sorting",
|
||||
"file-explorer"
|
||||
],
|
||||
"author": "Reifat <https://github.com/Reifat>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"monkey-around": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-obsidianmd": "^0.1.8",
|
||||
"obsidian": "latest",
|
||||
"typescript": "^5.3.0",
|
||||
"typescript-eslint": "^8.46.3"
|
||||
}
|
||||
"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",
|
||||
"lint": "eslint . --max-warnings=0",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"release": "npm run format && npm run lint && npm run build && node scripts/release.js"
|
||||
},
|
||||
"keywords": [
|
||||
"obsidian",
|
||||
"obsidian-plugin",
|
||||
"daily-notes",
|
||||
"sorting",
|
||||
"file-explorer"
|
||||
],
|
||||
"author": "Reifat <https://github.com/Reifat>",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"monkey-around": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/node": "^20.0.0",
|
||||
"archiver": "^7.0.1",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "^0.25.12",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-obsidianmd": "^0.1.8",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"obsidian": "latest",
|
||||
"prettier": "^3.6.2",
|
||||
"typescript": "^5.3.0",
|
||||
"typescript-eslint": "^8.46.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,97 +1,97 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import archiver from "archiver";
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import archiver from 'archiver';
|
||||
|
||||
function ensureDir(p) {
|
||||
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||
if (!fs.existsSync(p)) fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
|
||||
function copyFile(src, dst) {
|
||||
fs.copyFileSync(src, dst);
|
||||
fs.copyFileSync(src, dst);
|
||||
}
|
||||
|
||||
function main() {
|
||||
const cwd = process.cwd();
|
||||
const manifestPath = path.join(cwd, "manifest.json");
|
||||
const cwd = process.cwd();
|
||||
const manifestPath = path.join(cwd, 'manifest.json');
|
||||
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
console.error("manifest.json not found in repository root");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
console.error('manifest.json not found in repository root');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
||||
const pluginId = manifest.id;
|
||||
const pluginVersion = manifest.version;
|
||||
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
||||
const pluginId = manifest.id;
|
||||
const pluginVersion = manifest.version;
|
||||
|
||||
if (!pluginId || !pluginVersion) {
|
||||
console.error("Fields 'id' and/or 'version' are missing in manifest.json");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!pluginId || !pluginVersion) {
|
||||
console.error("Fields 'id' and/or 'version' are missing in manifest.json");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const releaseDir = path.join(cwd, "release");
|
||||
ensureDir(releaseDir);
|
||||
const releaseDir = path.join(cwd, 'release');
|
||||
ensureDir(releaseDir);
|
||||
|
||||
// Files to include. styles.css is optional.
|
||||
const sourceFiles = [
|
||||
{ name: "main.js", required: true },
|
||||
{ name: "manifest.json", required: true },
|
||||
{ name: "styles.css", required: false }
|
||||
];
|
||||
// Files to include. styles.css is optional.
|
||||
const sourceFiles = [
|
||||
{ name: 'main.js', required: true },
|
||||
{ name: 'manifest.json', required: true },
|
||||
{ name: 'styles.css', required: false },
|
||||
];
|
||||
|
||||
const filesToZip = [];
|
||||
for (const f of sourceFiles) {
|
||||
const src = path.join(cwd, f.name);
|
||||
if (!fs.existsSync(src)) {
|
||||
if (f.required) {
|
||||
console.error(`Required file is missing: ${f.name}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.warn(`Optional file is missing and will be skipped: ${f.name}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const dst = path.join(releaseDir, path.basename(f.name));
|
||||
copyFile(src, dst);
|
||||
filesToZip.push(dst);
|
||||
}
|
||||
const filesToZip = [];
|
||||
for (const f of sourceFiles) {
|
||||
const src = path.join(cwd, f.name);
|
||||
if (!fs.existsSync(src)) {
|
||||
if (f.required) {
|
||||
console.error(`Required file is missing: ${f.name}`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.warn(`Optional file is missing and will be skipped: ${f.name}`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const dst = path.join(releaseDir, path.basename(f.name));
|
||||
copyFile(src, dst);
|
||||
filesToZip.push(dst);
|
||||
}
|
||||
|
||||
if (filesToZip.length === 0) {
|
||||
console.error("No files to pack");
|
||||
process.exit(1);
|
||||
}
|
||||
if (filesToZip.length === 0) {
|
||||
console.error('No files to pack');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Archive name: <id>-<version>.zip
|
||||
const zipName = `${pluginId}-${pluginVersion}.zip`;
|
||||
const zipPath = path.join(releaseDir, zipName);
|
||||
// Archive name: <id>-<version>.zip
|
||||
const zipName = `${pluginId}-${pluginVersion}.zip`;
|
||||
const zipPath = path.join(releaseDir, zipName);
|
||||
|
||||
// Remove existing archive if present
|
||||
if (fs.existsSync(zipPath)) {
|
||||
fs.unlinkSync(zipPath);
|
||||
}
|
||||
// Remove existing archive if present
|
||||
if (fs.existsSync(zipPath)) {
|
||||
fs.unlinkSync(zipPath);
|
||||
}
|
||||
|
||||
const output = fs.createWriteStream(zipPath);
|
||||
const archive = archiver("zip", { zlib: { level: 9 } });
|
||||
const output = fs.createWriteStream(zipPath);
|
||||
const archive = archiver('zip', { zlib: { level: 9 } });
|
||||
|
||||
output.on("close", () => {
|
||||
console.log(`Archive ready: ${zipPath} (${archive.pointer()} total bytes)`);
|
||||
});
|
||||
output.on("error", (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
archive.on("error", (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
output.on('close', () => {
|
||||
console.log(`Archive ready: ${zipPath} (${archive.pointer()} total bytes)`);
|
||||
});
|
||||
output.on('error', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
archive.on('error', (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
archive.pipe(output);
|
||||
|
||||
// Put files into the root of the archive
|
||||
for (const filePath of filesToZip) {
|
||||
archive.file(filePath, { name: path.basename(filePath) });
|
||||
}
|
||||
// Put files into the root of the archive
|
||||
for (const filePath of filesToZip) {
|
||||
archive.file(filePath, { name: path.basename(filePath) });
|
||||
}
|
||||
|
||||
archive.finalize();
|
||||
archive.finalize();
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
|
|
|
|||
|
|
@ -1,291 +1,323 @@
|
|||
import { TFile, Notice } from 'obsidian';
|
||||
|
||||
import type { TFolder, App} from 'obsidian';
|
||||
import type { TFolder, App } 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);
|
||||
}
|
||||
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
|
||||
data: Data | undefined;
|
||||
path: string;
|
||||
basename: string;
|
||||
|
||||
constructor(data: Data | undefined, path: string, basename: string) {
|
||||
this.data = data;
|
||||
this.path = path;
|
||||
this.basename = basename;
|
||||
}
|
||||
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;
|
||||
private app: App | null = null;
|
||||
|
||||
constructor(app?: App) {
|
||||
this.app = app || null;
|
||||
}
|
||||
/**
|
||||
* Maps textual month tokens to a month number (1-12).
|
||||
* Supports English and Russian names and common abbreviations (with/without a trailing dot).
|
||||
*/
|
||||
private mapTextMonthToNumber(monthTokenRaw: string): number | undefined {
|
||||
const token = monthTokenRaw.toLowerCase().replace(/\.$/, '');
|
||||
const months: Record<string, number> = {
|
||||
// English
|
||||
'january': 1, 'jan': 1,
|
||||
'february': 2, 'feb': 2,
|
||||
'march': 3, 'mar': 3,
|
||||
'april': 4, 'apr': 4,
|
||||
'may': 5,
|
||||
'june': 6, 'jun': 6,
|
||||
'july': 7, 'jul': 7,
|
||||
'august': 8, 'aug': 8,
|
||||
'september': 9, 'sep': 9, 'sept': 9,
|
||||
'october': 10, 'oct': 10,
|
||||
'november': 11, 'nov': 11,
|
||||
'december': 12, 'dec': 12,
|
||||
constructor(app?: App) {
|
||||
this.app = app || null;
|
||||
}
|
||||
/**
|
||||
* Maps textual month tokens to a month number (1-12).
|
||||
* Supports English and Russian names and common abbreviations (with/without a trailing dot).
|
||||
*/
|
||||
private mapTextMonthToNumber(monthTokenRaw: string): number | undefined {
|
||||
const token = monthTokenRaw.toLowerCase().replace(/\.$/, '');
|
||||
const months: Record<string, number> = {
|
||||
// English
|
||||
january: 1,
|
||||
jan: 1,
|
||||
february: 2,
|
||||
feb: 2,
|
||||
march: 3,
|
||||
mar: 3,
|
||||
april: 4,
|
||||
apr: 4,
|
||||
may: 5,
|
||||
june: 6,
|
||||
jun: 6,
|
||||
july: 7,
|
||||
jul: 7,
|
||||
august: 8,
|
||||
aug: 8,
|
||||
september: 9,
|
||||
sep: 9,
|
||||
sept: 9,
|
||||
october: 10,
|
||||
oct: 10,
|
||||
november: 11,
|
||||
nov: 11,
|
||||
december: 12,
|
||||
dec: 12,
|
||||
};
|
||||
return months[token];
|
||||
}
|
||||
|
||||
};
|
||||
return months[token];
|
||||
}
|
||||
/**
|
||||
* Attempts to parse a textual date at the beginning of the filename using spaces as separators
|
||||
* and a specific component order.
|
||||
*
|
||||
* Example supported variants depending on order:
|
||||
* - ['y','m','d'] → "YYYY Month DD" (or "YY Month DD" for 2-digit year)
|
||||
* - ['d','m','y'] → "DD Month YYYY"
|
||||
* - ['m','d','y'] → "Month DD YYYY"
|
||||
*
|
||||
* Month can be in English or Russian, full or abbreviated (with/without dot).
|
||||
* expectedYearDigits: 2 or 4. If 2, year will be expanded (00-49 -> 2000-2049, 50-99 -> 1950-1999).
|
||||
*/
|
||||
private parseTextualDate(
|
||||
fileName: string,
|
||||
order: Array<'y' | 'm' | 'd'>,
|
||||
expectedYearDigits: 2 | 4,
|
||||
): Data | undefined {
|
||||
const monthWord = '([A-Za-zА-Яа-яЁё\\.]+)';
|
||||
const dayNum = '(\\d{1,2})';
|
||||
const yearPattern = expectedYearDigits === 2 ? '(\\d{2})' : '(\\d{4})';
|
||||
|
||||
/**
|
||||
* Attempts to parse a textual date at the beginning of the filename using spaces as separators
|
||||
* and a specific component order.
|
||||
*
|
||||
* Example supported variants depending on order:
|
||||
* - ['y','m','d'] → "YYYY Month DD" (or "YY Month DD" for 2-digit year)
|
||||
* - ['d','m','y'] → "DD Month YYYY"
|
||||
* - ['m','d','y'] → "Month DD YYYY"
|
||||
*
|
||||
* Month can be in English or Russian, full or abbreviated (with/without dot).
|
||||
* expectedYearDigits: 2 or 4. If 2, year will be expanded (00-49 -> 2000-2049, 50-99 -> 1950-1999).
|
||||
*/
|
||||
private parseTextualDate(fileName: string, order: Array<'y' | 'm' | 'd'>, expectedYearDigits: 2 | 4): Data | undefined {
|
||||
const monthWord = '([A-Za-zА-Яа-яЁё\\.]+)';
|
||||
const dayNum = '(\\d{1,2})';
|
||||
const yearPattern = expectedYearDigits === 2 ? '(\\d{2})' : '(\\d{4})';
|
||||
// Build pattern according to order
|
||||
const tokenToPattern: Record<'y' | 'm' | 'd', string> = {
|
||||
y: yearPattern,
|
||||
m: monthWord,
|
||||
d: dayNum,
|
||||
};
|
||||
const parts = order.map((p) => tokenToPattern[p]);
|
||||
const re = new RegExp(`^${parts[0]}\\s+${parts[1]}\\s+${parts[2]}`);
|
||||
|
||||
// Build pattern according to order
|
||||
const tokenToPattern: Record<'y' | 'm' | 'd', string> = {
|
||||
'y': yearPattern,
|
||||
'm': monthWord,
|
||||
'd': dayNum
|
||||
};
|
||||
const parts = order.map((p) => tokenToPattern[p]);
|
||||
const re = new RegExp(`^${parts[0]}\\s+${parts[1]}\\s+${parts[2]}`);
|
||||
const match = fileName.match(re);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const match = fileName.match(re);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
// match groups depend on order; map them using order
|
||||
let dayStr = '';
|
||||
let monthStr = '';
|
||||
let yearStr = '';
|
||||
|
||||
// match groups depend on order; map them using order
|
||||
let dayStr = '';
|
||||
let monthStr = '';
|
||||
let yearStr = '';
|
||||
// The first capturing group index is 1
|
||||
const g1 = match[1];
|
||||
const g2 = match[2];
|
||||
const g3 = match[3];
|
||||
|
||||
// The first capturing group index is 1
|
||||
const g1 = match[1];
|
||||
const g2 = match[2];
|
||||
const g3 = match[3];
|
||||
order.forEach((part, idx) => {
|
||||
const val = idx === 0 ? g1 : idx === 1 ? g2 : g3;
|
||||
if (part === 'd') {
|
||||
dayStr = val;
|
||||
}
|
||||
if (part === 'm') {
|
||||
monthStr = val;
|
||||
}
|
||||
if (part === 'y') {
|
||||
yearStr = val;
|
||||
}
|
||||
});
|
||||
|
||||
order.forEach((part, idx) => {
|
||||
const val = idx === 0 ? g1 : idx === 1 ? g2 : g3;
|
||||
if (part === 'd') {dayStr = val;}
|
||||
if (part === 'm') {monthStr = val;}
|
||||
if (part === 'y') {yearStr = val;}
|
||||
});
|
||||
// Convert textual month to number
|
||||
const monthNum = this.mapTextMonthToNumber(monthStr);
|
||||
if (!monthNum) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Convert textual month to number
|
||||
const monthNum = this.mapTextMonthToNumber(monthStr);
|
||||
if (!monthNum) {
|
||||
return undefined;
|
||||
}
|
||||
// If expected 4 digits but got 2, or vice versa, do not match
|
||||
if (
|
||||
(expectedYearDigits === 4 && yearStr.length !== 4) ||
|
||||
(expectedYearDigits === 2 && yearStr.length !== 2)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// If expected 4 digits but got 2, or vice versa, do not match
|
||||
if ((expectedYearDigits === 4 && yearStr.length !== 4) || (expectedYearDigits === 2 && yearStr.length !== 2)) {
|
||||
return undefined;
|
||||
}
|
||||
// Expand 2-digit year if necessary
|
||||
let yearNum = parseInt(yearStr, 10);
|
||||
if (yearStr.length === 2) {
|
||||
yearNum = yearNum < 50 ? 2000 + yearNum : 1900 + yearNum;
|
||||
}
|
||||
|
||||
// Expand 2-digit year if necessary
|
||||
let yearNum = parseInt(yearStr, 10);
|
||||
if (yearStr.length === 2) {
|
||||
yearNum = yearNum < 50 ? 2000 + yearNum : 1900 + yearNum;
|
||||
}
|
||||
// Basic sanity checks for day/month bounds (not strict calendar validation)
|
||||
const dayNumInt = parseInt(dayStr, 10);
|
||||
if (dayNumInt < 1 || dayNumInt > 31) {
|
||||
return undefined;
|
||||
}
|
||||
if (monthNum < 1 || monthNum > 12) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Basic sanity checks for day/month bounds (not strict calendar validation)
|
||||
const dayNumInt = parseInt(dayStr, 10);
|
||||
if (dayNumInt < 1 || dayNumInt > 31) {
|
||||
return undefined;
|
||||
}
|
||||
if (monthNum < 1 || monthNum > 12) {
|
||||
return undefined;
|
||||
}
|
||||
return new Data(String(dayNumInt), String(monthNum), String(yearNum));
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
return new Data(String(dayNumInt), String(monthNum), String(yearNum));
|
||||
}
|
||||
/**
|
||||
* 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 {
|
||||
// Try textual date fallback with the same order and spaces: "YYYY Month DD"
|
||||
return this.parseTextualDate(fileName, ['y', 'm', 'd'], 4);
|
||||
}
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "DD Month YYYY"
|
||||
return this.parseTextualDate(fileName, ['d', 'm', 'y'], 4);
|
||||
}
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "DD Month YY"
|
||||
return this.parseTextualDate(fileName, ['d', 'm', 'y'], 2);
|
||||
}
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "Month DD YYYY"
|
||||
return this.parseTextualDate(fileName, ['m', 'd', 'y'], 4);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`[Sorter] Unsupported date format: ${dateFormat}`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "YYYY Month DD"
|
||||
return this.parseTextualDate(fileName, ['y','m','d'], 4);
|
||||
}
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "DD Month YYYY"
|
||||
return this.parseTextualDate(fileName, ['d','m','y'], 4);
|
||||
}
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "DD Month YY"
|
||||
return this.parseTextualDate(fileName, ['d','m','y'], 2);
|
||||
}
|
||||
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 {
|
||||
// Try textual date fallback with the same order and spaces: "Month DD YYYY"
|
||||
return this.parseTextualDate(fileName, ['m','d','y'], 4);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`[Sorter] Unsupported date format: ${dateFormat}`);
|
||||
return undefined;
|
||||
}
|
||||
return new Data(day, month, year);
|
||||
}
|
||||
|
||||
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: Record<string, unknown>,
|
||||
ascending: boolean = true,
|
||||
dateFormat: string = 'YYYY-MM-DD',
|
||||
): unknown[] {
|
||||
const allFileItemsCollection = fileItems;
|
||||
|
||||
/**
|
||||
* 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: Record<string, unknown>, ascending: boolean = true, dateFormat: string = "YYYY-MM-DD"): unknown[] {
|
||||
const allFileItemsCollection = fileItems;
|
||||
// Separate files into matching and non-matching format
|
||||
const filesWithDate: Array<SortingItem> = [];
|
||||
const filesWithoutDate: Array<SortingItem> = [];
|
||||
|
||||
// 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);
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
// 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}`;
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
new Notice(message, 5000);
|
||||
}
|
||||
|
||||
// Sort files without date by name (for consistency)
|
||||
filesWithoutDate.sort((a: SortingItem, b: SortingItem) => {
|
||||
return a.basename.localeCompare(b.basename);
|
||||
});
|
||||
// Sort files with date
|
||||
if (filesWithDate.length > 0) {
|
||||
filesWithDate.sort((a: SortingItem, b: SortingItem) => {
|
||||
if (!a.data || !b.data) {
|
||||
return !a.data ? 1 : -1;
|
||||
}
|
||||
|
||||
// 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];
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to FileExplorer items
|
||||
const result = resultItems
|
||||
.map((item: SortingItem) => allFileItemsCollection[item.path])
|
||||
.filter(item => item !== undefined);
|
||||
// Sort files without date by name (for consistency)
|
||||
filesWithoutDate.sort((a: SortingItem, b: SortingItem) => {
|
||||
return a.basename.localeCompare(b.basename);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,227 +9,258 @@ import type { App, Plugin, FileExplorerView, TFolder } from 'obsidian';
|
|||
type MonkeyAroundUninstaller = () => void;
|
||||
|
||||
export class FileExplorerUtils {
|
||||
private app: App;
|
||||
private plugin: Plugin;
|
||||
private sorter: Sorter;
|
||||
private settings: { items: FolderItem[]; sortAscending?: boolean };
|
||||
private pluginInstance: { saveSettings: () => Promise<void> } | undefined; // Reference to plugin instance for saving settings
|
||||
public sortAscending: boolean = true; // Public for access from closure
|
||||
private app: App;
|
||||
private plugin: Plugin;
|
||||
private sorter: Sorter;
|
||||
private settings: { items: FolderItem[]; sortAscending?: boolean };
|
||||
private pluginInstance: { saveSettings: () => Promise<void> } | undefined; // 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?: { saveSettings: () => Promise<void> }) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: Plugin,
|
||||
sorter: Sorter,
|
||||
settings: { items: FolderItem[]; sortAscending?: boolean },
|
||||
pluginInstance?: { saveSettings: () => Promise<void> },
|
||||
) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 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
|
||||
this.settings.sortAscending = ascending;
|
||||
|
||||
// Save settings if required
|
||||
if (saveSettings && this.pluginInstance) {
|
||||
this.pluginInstance.saveSettings().catch((err: unknown) => {
|
||||
console.error("[FileExplorerUtils] Error saving sort direction:", err);
|
||||
});
|
||||
}
|
||||
|
||||
// Update sorting after changing direction
|
||||
this.applySort();
|
||||
}
|
||||
/**
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Applies sorting to FileExplorer
|
||||
*/
|
||||
applySort(): void {
|
||||
const fileExplorerView = this.getFileExplorer();
|
||||
if (fileExplorerView && typeof fileExplorerView.requestSort === 'function') {
|
||||
fileExplorerView.requestSort();
|
||||
}
|
||||
}
|
||||
// Update plugin settings
|
||||
this.settings.sortAscending = ascending;
|
||||
|
||||
/**
|
||||
* Gets current sort direction
|
||||
*/
|
||||
getSortDirection(): boolean {
|
||||
return this.sortAscending;
|
||||
}
|
||||
// Save settings if required
|
||||
if (saveSettings && this.pluginInstance) {
|
||||
this.pluginInstance.saveSettings().catch((err: unknown) => {
|
||||
console.error('[FileExplorerUtils] Error saving sort direction:', err);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets FileExplorerView instance from workspace
|
||||
*/
|
||||
getFileExplorer(): FileExplorerView | undefined {
|
||||
const fileExplorer: FileExplorerView | undefined = this.app.workspace.getLeavesOfType("file-explorer").first()
|
||||
?.view as unknown as FileExplorerView;
|
||||
return fileExplorer;
|
||||
}
|
||||
// Update sorting after changing direction
|
||||
this.applySort();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
/**
|
||||
* Applies sorting to FileExplorer
|
||||
*/
|
||||
applySort(): void {
|
||||
const fileExplorerView = this.getFileExplorer();
|
||||
if (fileExplorerView && typeof fileExplorerView.requestSort === 'function') {
|
||||
fileExplorerView.requestSort();
|
||||
}
|
||||
}
|
||||
|
||||
const checkFileExplorer = (): void => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* Gets current sort direction
|
||||
*/
|
||||
getSortDirection(): boolean {
|
||||
return this.sortAscending;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks availability and patchability of FileExplorer
|
||||
*/
|
||||
checkFileExplorerIsAvailableAndPatchable(logWarning: boolean = true): FileExplorerView | undefined {
|
||||
const 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("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
|
||||
}
|
||||
/**
|
||||
* Gets FileExplorerView instance from workspace
|
||||
*/
|
||||
getFileExplorer(): FileExplorerView | undefined {
|
||||
const fileExplorer: FileExplorerView | undefined = this.app.workspace
|
||||
.getLeavesOfType('file-explorer')
|
||||
.first()?.view as unknown as FileExplorerView;
|
||||
return fileExplorer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies patch to FileExplorer for custom sorting
|
||||
*/
|
||||
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
|
||||
// Capture methods and properties needed in closures
|
||||
const getFolderSettings = (path: string): FolderItem | undefined => this.getFolderSettings(path);
|
||||
const getSortAscending = (): boolean => this.sortAscending;
|
||||
const sorter = this.sorter;
|
||||
const checkFileExplorerIsAvailableAndPatchable = (logWarning: boolean): FileExplorerView | undefined =>
|
||||
this.checkFileExplorerIsAvailableAndPatchable(logWarning);
|
||||
/**
|
||||
* 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 requestStandardObsidianSortAfter = (patchUninstaller: MonkeyAroundUninstaller|undefined): (() => void) => {
|
||||
return (): void => {
|
||||
if (patchUninstaller) {patchUninstaller()}
|
||||
const checkFileExplorer = (): void => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
const fileExplorerView: FileExplorerView | undefined = checkFileExplorerIsAvailableAndPatchable(false)
|
||||
if (fileExplorerView) {
|
||||
fileExplorerView.requestSort()
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks availability and patchability of FileExplorer
|
||||
*/
|
||||
checkFileExplorerIsAvailableAndPatchable(
|
||||
logWarning: boolean = true,
|
||||
): FileExplorerView | undefined {
|
||||
const 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('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;
|
||||
}
|
||||
|
||||
// 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("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: (folder: TFolder) => unknown[]) => {
|
||||
return function (this: FileExplorerView, folder: TFolder): unknown[] {
|
||||
const folderPath = folder.path;
|
||||
const folderSettings = getFolderSettings(folderPath);
|
||||
if (folderSettings) {
|
||||
const ascending = getSortAscending();
|
||||
const dateFormat = folderSettings.dateFormat;
|
||||
return sorter.sortFolder(folder, this.fileItems, ascending, dateFormat);
|
||||
}
|
||||
else { // default sort
|
||||
return old.call(this, folder);
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
this.plugin.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
|
||||
return true
|
||||
} else {
|
||||
// Up to Obsidian 1.6.0
|
||||
const tmpFolder = this.app.vault.getRoot();
|
||||
const Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor;
|
||||
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(Folder.prototype, {
|
||||
sort: (old: (folder: TFolder) => unknown[]) => {
|
||||
return function (this: { fileItems: Record<string, unknown> }, folder: TFolder): unknown[] {
|
||||
const folderPath = folder.path;
|
||||
const folderSettings = getFolderSettings(folderPath);
|
||||
if (folderSettings) {
|
||||
const ascending = getSortAscending();
|
||||
const dateFormat = folderSettings.dateFormat;
|
||||
return sorter.sortFolder(folder, this.fileItems, ascending, dateFormat);
|
||||
}
|
||||
else { // default sort
|
||||
return old.call(this, folder);
|
||||
}
|
||||
};
|
||||
}
|
||||
})
|
||||
this.plugin.register(requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper))
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Applies patch to FileExplorer for custom sorting
|
||||
*/
|
||||
patchFileExplorerFolder(patchableFileExplorer?: FileExplorerView): boolean {
|
||||
// Capture methods and properties needed in closures
|
||||
const getFolderSettings = (path: string): FolderItem | undefined =>
|
||||
this.getFolderSettings(path);
|
||||
const getSortAscending = (): boolean => this.sortAscending;
|
||||
const sorter = this.sorter;
|
||||
const checkFileExplorerIsAvailableAndPatchable = (
|
||||
logWarning: boolean,
|
||||
): FileExplorerView | undefined => this.checkFileExplorerIsAvailableAndPatchable(logWarning);
|
||||
|
||||
const requestStandardObsidianSortAfter = (
|
||||
patchUninstaller: MonkeyAroundUninstaller | undefined,
|
||||
): (() => void) => {
|
||||
return (): void => {
|
||||
if (patchUninstaller) {
|
||||
patchUninstaller();
|
||||
}
|
||||
|
||||
const fileExplorerView: FileExplorerView | undefined =
|
||||
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('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: (folder: TFolder) => unknown[]) => {
|
||||
return function (this: FileExplorerView, folder: TFolder): unknown[] {
|
||||
const folderPath = folder.path;
|
||||
const folderSettings = getFolderSettings(folderPath);
|
||||
if (folderSettings) {
|
||||
const ascending = getSortAscending();
|
||||
const dateFormat = folderSettings.dateFormat;
|
||||
return sorter.sortFolder(folder, this.fileItems, ascending, dateFormat);
|
||||
} else {
|
||||
// default sort
|
||||
return old.call(this, folder);
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
this.plugin.register(
|
||||
requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper),
|
||||
);
|
||||
return true;
|
||||
} else {
|
||||
// Up to Obsidian 1.6.0
|
||||
const tmpFolder = this.app.vault.getRoot();
|
||||
const Folder = patchableFileExplorer.createFolderDom(tmpFolder).constructor;
|
||||
const uninstallerOfFolderSortFunctionWrapper: MonkeyAroundUninstaller = around(
|
||||
Folder.prototype,
|
||||
{
|
||||
sort: (old: (folder: TFolder) => unknown[]) => {
|
||||
return function (
|
||||
this: { fileItems: Record<string, unknown> },
|
||||
folder: TFolder,
|
||||
): unknown[] {
|
||||
const folderPath = folder.path;
|
||||
const folderSettings = getFolderSettings(folderPath);
|
||||
if (folderSettings) {
|
||||
const ascending = getSortAscending();
|
||||
const dateFormat = folderSettings.dateFormat;
|
||||
return sorter.sortFolder(folder, this.fileItems, ascending, dateFormat);
|
||||
} else {
|
||||
// default sort
|
||||
return old.call(this, folder);
|
||||
}
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
this.plugin.register(
|
||||
requestStandardObsidianSortAfter(uninstallerOfFolderSortFunctionWrapper),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
206
src/main.ts
206
src/main.ts
|
|
@ -5,127 +5,129 @@ import { FileExplorerUtils } from './file-explorer-utils';
|
|||
import { ExplorerUI } from './ui/explorer-ui';
|
||||
import { SorterSettings } from './ui/settings';
|
||||
|
||||
|
||||
const DEFAULT_DAILY_NOTE_FORMAT = "YYYY-MM-DD";
|
||||
const DEFAULT_DAILY_NOTE_FORMAT = 'YYYY-MM-DD';
|
||||
|
||||
export interface FolderItem {
|
||||
path: string;
|
||||
dateFormat: string;
|
||||
path: string;
|
||||
dateFormat: string;
|
||||
}
|
||||
|
||||
interface DailyNotesSorterSettings {
|
||||
items: FolderItem[];
|
||||
sortAscending: boolean;
|
||||
items: FolderItem[];
|
||||
sortAscending: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_SETTINGS: DailyNotesSorterSettings = {
|
||||
items: [],
|
||||
sortAscending: true
|
||||
items: [],
|
||||
sortAscending: true,
|
||||
};
|
||||
|
||||
export default class DailyNotesSorter extends Plugin {
|
||||
settings!: DailyNotesSorterSettings;
|
||||
private sorter!: Sorter;
|
||||
private fileExplorerUtils!: FileExplorerUtils;
|
||||
private explorerUI!: ExplorerUI;
|
||||
settings!: DailyNotesSorterSettings;
|
||||
private sorter!: Sorter;
|
||||
private fileExplorerUtils!: FileExplorerUtils;
|
||||
private explorerUI!: ExplorerUI;
|
||||
|
||||
async onload(): Promise<void> {
|
||||
|
||||
await this.loadSettings();
|
||||
async onload(): Promise<void> {
|
||||
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.app, this.fileExplorerUtils);
|
||||
// Initialize sorter with App for showing notifications
|
||||
this.sorter = new Sorter(this.app);
|
||||
|
||||
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: unknown) => {
|
||||
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
|
||||
});
|
||||
// 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.app, this.fileExplorerUtils);
|
||||
|
||||
this.addSettingTab(new SorterSettings(this.app, this));
|
||||
}
|
||||
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: unknown) => {
|
||||
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
|
||||
});
|
||||
|
||||
onunload(): void {
|
||||
// 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
|
||||
|
||||
// Cleanup UI components (e.g., remove sort button from FileExplorer)
|
||||
this.explorerUI.cleanup();
|
||||
}
|
||||
this.addSettingTab(new SorterSettings(this.app, this));
|
||||
}
|
||||
|
||||
async loadSettings(): Promise<void> {
|
||||
try {
|
||||
const loadedData: unknown = await this.loadData();
|
||||
|
||||
// Initialize settings with default values
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
|
||||
// If there is loaded data, apply it
|
||||
if (loadedData && typeof loadedData === 'object') {
|
||||
const data = loadedData as {
|
||||
items?: Array<{ path?: string; dateFormat?: string }>;
|
||||
sortAscending?: boolean;
|
||||
};
|
||||
onunload(): void {
|
||||
// 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
|
||||
|
||||
if (Array.isArray(data.items)) {
|
||||
this.settings.items = data.items.map((item) => ({
|
||||
path: item.path || "",
|
||||
dateFormat: item.dateFormat || DEFAULT_DAILY_NOTE_FORMAT,
|
||||
}));
|
||||
}
|
||||
// Load saved sort state
|
||||
if (typeof data.sortAscending === 'boolean') {
|
||||
this.settings.sortAscending = data.sortAscending;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading settings:", error);
|
||||
// In case of error, use default settings
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
}
|
||||
}
|
||||
// Cleanup UI components (e.g., remove sort button from FileExplorer)
|
||||
this.explorerUI.cleanup();
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
try {
|
||||
// Ensure data structure is correct before saving
|
||||
// Validate and normalize data before saving
|
||||
this.settings.items = this.settings.items.map((item) => ({
|
||||
path: item.path || "",
|
||||
dateFormat: item.dateFormat || DEFAULT_DAILY_NOTE_FORMAT,
|
||||
}));
|
||||
|
||||
// Save sort state from FileExplorerUtils
|
||||
this.settings.sortAscending = this.fileExplorerUtils.sortAscending;
|
||||
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Update settings in FileExplorerUtils after saving
|
||||
this.fileExplorerUtils.updateSettings(this.settings);
|
||||
// Apply sorting after updating settings
|
||||
this.fileExplorerUtils.applySort();
|
||||
} catch (error) {
|
||||
console.error("Error saving settings:", error);
|
||||
}
|
||||
}
|
||||
async loadSettings(): Promise<void> {
|
||||
try {
|
||||
const loadedData: unknown = await this.loadData();
|
||||
|
||||
// Initialize settings with default values
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
|
||||
// If there is loaded data, apply it
|
||||
if (loadedData && typeof loadedData === 'object') {
|
||||
const data = loadedData as {
|
||||
items?: Array<{ path?: string; dateFormat?: string }>;
|
||||
sortAscending?: boolean;
|
||||
};
|
||||
|
||||
if (Array.isArray(data.items)) {
|
||||
this.settings.items = data.items.map((item) => ({
|
||||
path: item.path || '',
|
||||
dateFormat: item.dateFormat || DEFAULT_DAILY_NOTE_FORMAT,
|
||||
}));
|
||||
}
|
||||
// Load saved sort state
|
||||
if (typeof data.sortAscending === 'boolean') {
|
||||
this.settings.sortAscending = data.sortAscending;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading settings:', error);
|
||||
// In case of error, use default settings
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||
}
|
||||
}
|
||||
|
||||
async saveSettings(): Promise<void> {
|
||||
try {
|
||||
// Ensure data structure is correct before saving
|
||||
// Validate and normalize data before saving
|
||||
this.settings.items = this.settings.items.map((item) => ({
|
||||
path: item.path || '',
|
||||
dateFormat: item.dateFormat || DEFAULT_DAILY_NOTE_FORMAT,
|
||||
}));
|
||||
|
||||
// Save sort state from FileExplorerUtils
|
||||
this.settings.sortAscending = this.fileExplorerUtils.sortAscending;
|
||||
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Update settings in FileExplorerUtils after saving
|
||||
this.fileExplorerUtils.updateSettings(this.settings);
|
||||
// Apply sorting after updating settings
|
||||
this.fileExplorerUtils.applySort();
|
||||
} catch (error) {
|
||||
console.error('Error saving settings:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,210 +2,215 @@
|
|||
* Class for managing autocomplete in input field
|
||||
*/
|
||||
export class AutocompleteInput {
|
||||
private suggestionsList: HTMLElement;
|
||||
private inputEl: HTMLInputElement;
|
||||
private onValueChange: (value: string) => void;
|
||||
private getSuggestions: () => string[];
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
const filtered = this.filterSuggestions(query);
|
||||
if (filtered.length > 0) {
|
||||
this.showSuggestions(filtered);
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
private handleBlur(): void {
|
||||
// Delay for handling click on suggestion
|
||||
setTimeout(() => {
|
||||
this.hideSuggestions();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
const filtered = this.filterSuggestions(query);
|
||||
if (filtered.length > 0) {
|
||||
this.showSuggestions(filtered);
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
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) {
|
||||
this.suggestionsList.createDiv({
|
||||
cls: 'suggestion-item_1 suggestion-more',
|
||||
text: `... and ${suggestions.length - maxSuggestions} more`,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
|
||||
private handleBlur(): void {
|
||||
// Delay for handling click on suggestion
|
||||
setTimeout(() => { this.hideSuggestions(); }, 100);
|
||||
// Reset any fixed heights for automatic calculation
|
||||
this.suggestionsList.setCssProps({
|
||||
'--suggestions-list-height': 'auto',
|
||||
'--suggestions-list-overflow': 'auto',
|
||||
});
|
||||
this.suggestionsList.removeClass('no-scroll');
|
||||
|
||||
// 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.setCssProps({
|
||||
'--suggestions-list-height': `${contentHeight}px`,
|
||||
'--suggestions-list-overflow': 'hidden',
|
||||
});
|
||||
this.suggestionsList.addClass('no-scroll');
|
||||
} else if (contentHeight > 0) {
|
||||
// Content doesn't fit, set maximum height with scrolling
|
||||
this.suggestionsList.setCssProps({
|
||||
'--suggestions-list-height': `${maxAllowedHeight}px`,
|
||||
'--suggestions-list-overflow': 'auto',
|
||||
});
|
||||
this.suggestionsList.removeClass('no-scroll');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private filterSuggestions(query: string): string[] {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
const suggestions = this.getSuggestions();
|
||||
return suggestions.filter((s) =>
|
||||
s.toLowerCase().includes(lowerQuery)
|
||||
);
|
||||
}
|
||||
const item = this.suggestionsList.createDiv({
|
||||
cls: 'suggestion-item_1',
|
||||
text: suggestion,
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
item.onclick = () => {
|
||||
this.inputEl.value = suggestion;
|
||||
this.onValueChange(suggestion);
|
||||
this.hideSuggestions();
|
||||
};
|
||||
}
|
||||
|
||||
// Show message if there are more suggestions
|
||||
if (suggestions.length > maxSuggestions) {
|
||||
this.suggestionsList.createDiv({
|
||||
cls: "suggestion-item_1 suggestion-more",
|
||||
text: `... and ${suggestions.length - maxSuggestions} more`,
|
||||
});
|
||||
}
|
||||
|
||||
// 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.setCssProps({
|
||||
"--suggestions-list-height": "auto",
|
||||
"--suggestions-list-overflow": "auto"
|
||||
});
|
||||
this.suggestionsList.removeClass("no-scroll");
|
||||
|
||||
// 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.setCssProps({
|
||||
"--suggestions-list-height": `${contentHeight}px`,
|
||||
"--suggestions-list-overflow": "hidden"
|
||||
});
|
||||
this.suggestionsList.addClass("no-scroll");
|
||||
} else if (contentHeight > 0) {
|
||||
// Content doesn't fit, set maximum height with scrolling
|
||||
this.suggestionsList.setCssProps({
|
||||
"--suggestions-list-height": `${maxAllowedHeight}px`,
|
||||
"--suggestions-list-overflow": "auto"
|
||||
});
|
||||
this.suggestionsList.removeClass("no-scroll");
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
private hideSuggestions(): void {
|
||||
this.suggestionsList.addClass('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,93 +1,95 @@
|
|||
import type { FileExplorerUtils } from '../file-explorer-utils';
|
||||
import type { App, FileExplorerView, WorkspaceLeaf } from 'obsidian';
|
||||
|
||||
|
||||
export class ExplorerUI {
|
||||
private app: App;
|
||||
private fileExplorerUtils: FileExplorerUtils;
|
||||
private app: App;
|
||||
private fileExplorerUtils: FileExplorerUtils;
|
||||
|
||||
private readonly SORT_OPTIONS = [
|
||||
{ id: 'ascending', label: 'Ascending' },
|
||||
{ id: 'descending', label: 'Descending' }
|
||||
];
|
||||
private readonly sortOptions = [
|
||||
{ id: 'ascending', label: 'Ascending' },
|
||||
{ id: 'descending', label: 'Descending' },
|
||||
];
|
||||
|
||||
constructor(app: App, fileExplorerUtils: FileExplorerUtils) {
|
||||
this.app = app;
|
||||
this.fileExplorerUtils = fileExplorerUtils;
|
||||
}
|
||||
constructor(app: App, fileExplorerUtils: FileExplorerUtils) {
|
||||
this.app = app;
|
||||
this.fileExplorerUtils = fileExplorerUtils;
|
||||
}
|
||||
|
||||
initialize(fileExplorer: FileExplorerView): void {
|
||||
if (!fileExplorer.headerDom.navButtonsEl) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
});
|
||||
const sortOptions: string[][] = [this.sortOptions.map((opt) => opt.id)];
|
||||
const descriptions: Record<string, () => string> = {};
|
||||
this.sortOptions.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'
|
||||
);
|
||||
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 using CSS class and custom color
|
||||
setTimeout(() => {
|
||||
const button = fileExplorer.headerDom.navButtonsEl?.lastElementChild as HTMLElement | null;
|
||||
if (button) {
|
||||
button.addClass("file-explorer-sort-button");
|
||||
const svg = button.querySelector("svg") as { setCssProps?: (props: Record<string, string>) => void } | null;
|
||||
if (svg && typeof svg.setCssProps === 'function') {
|
||||
svg.setCssProps({
|
||||
"--sort-button-icon-color": "#fff"
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
// Style the button using CSS class and custom color
|
||||
setTimeout(() => {
|
||||
const button = fileExplorer.headerDom.navButtonsEl?.lastElementChild as HTMLElement | null;
|
||||
if (button) {
|
||||
button.addClass('file-explorer-sort-button');
|
||||
const svg = button.querySelector('svg') as {
|
||||
setCssProps?: (props: Record<string, string>) => void;
|
||||
} | null;
|
||||
if (svg && typeof svg.setCssProps === 'function') {
|
||||
svg.setCssProps({
|
||||
'--sort-button-icon-color': '#fff',
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up the UI by reloading FileExplorer to remove the sort button
|
||||
* This is necessary because the button added via addSortButton() is not automatically removed
|
||||
*/
|
||||
cleanup(): void {
|
||||
const fileExplorerLeaves = this.app.workspace.getLeavesOfType("file-explorer");
|
||||
if (fileExplorerLeaves.length > 0) {
|
||||
// Save the view states before detaching
|
||||
const leavesData = fileExplorerLeaves.map(leaf => ({
|
||||
viewState: leaf.getViewState()
|
||||
}));
|
||||
|
||||
// Detach all FileExplorer leaves
|
||||
fileExplorerLeaves.forEach(leaf => {
|
||||
leaf.detach();
|
||||
});
|
||||
|
||||
// Reopen FileExplorer leaves with saved state
|
||||
// Use setTimeout to ensure detach completes before reopening
|
||||
setTimeout(() => {
|
||||
// Choose the view state to restore: use the first saved state
|
||||
const preferred = leavesData[0];
|
||||
/**
|
||||
* Cleans up the UI by reloading FileExplorer to remove the sort button
|
||||
* This is necessary because the button added via addSortButton() is not automatically removed
|
||||
*/
|
||||
cleanup(): void {
|
||||
const fileExplorerLeaves = this.app.workspace.getLeavesOfType('file-explorer');
|
||||
if (fileExplorerLeaves.length > 0) {
|
||||
// Save the view states before detaching
|
||||
const leavesData = fileExplorerLeaves.map((leaf) => ({
|
||||
viewState: leaf.getViewState(),
|
||||
}));
|
||||
|
||||
// Prefer the left leaf if available, otherwise create a new one
|
||||
const targetLeaf: WorkspaceLeaf = this.app.workspace.getLeftLeaf(false) ?? this.app.workspace.getLeaf();
|
||||
// Detach all FileExplorer leaves
|
||||
fileExplorerLeaves.forEach((leaf) => {
|
||||
leaf.detach();
|
||||
});
|
||||
|
||||
targetLeaf.setViewState(preferred.viewState)
|
||||
.then(() => {
|
||||
// Always restore focus to the restored File Explorer leaf
|
||||
this.app.workspace.setActiveLeaf(targetLeaf, { focus: true });
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error("[ExplorerUI] Error setting view state:", err);
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
// Reopen FileExplorer leaves with saved state
|
||||
// Use setTimeout to ensure detach completes before reopening
|
||||
setTimeout(() => {
|
||||
// Choose the view state to restore: use the first saved state
|
||||
const preferred = leavesData[0];
|
||||
|
||||
// Prefer the left leaf if available, otherwise create a new one
|
||||
const targetLeaf: WorkspaceLeaf =
|
||||
this.app.workspace.getLeftLeaf(false) ?? this.app.workspace.getLeaf();
|
||||
|
||||
targetLeaf
|
||||
.setViewState(preferred.viewState)
|
||||
.then(() => {
|
||||
// Always restore focus to the restored File Explorer leaf
|
||||
this.app.workspace.setActiveLeaf(targetLeaf, { focus: true });
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[ExplorerUI] Error setting view state:', err);
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,427 +3,426 @@ import { PluginSettingTab, Setting, TFolder, Notice } from 'obsidian';
|
|||
import { AutocompleteInput } from './autocomplete-input';
|
||||
|
||||
import type DailyNotesSorter from '../main';
|
||||
import type { App} from 'obsidian';
|
||||
import type { App } from 'obsidian';
|
||||
|
||||
enum SettingsLoadState {
|
||||
Unknown = "Unknown",
|
||||
Loading = "Loading",
|
||||
Loaded = "Loaded",
|
||||
Unknown = 'Unknown',
|
||||
Loading = 'Loading',
|
||||
Loaded = 'Loaded',
|
||||
}
|
||||
|
||||
// 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 APPLY_BUTTON_TEXT = "Apply settings";
|
||||
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 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)" },
|
||||
{ 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";
|
||||
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();
|
||||
private loadState: SettingsLoadState;
|
||||
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();
|
||||
private loadState: SettingsLoadState;
|
||||
|
||||
constructor(app: App, plugin: DailyNotesSorter) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.loadState = SettingsLoadState.Unknown;
|
||||
|
||||
// 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();
|
||||
})
|
||||
);
|
||||
constructor(app: App, plugin: DailyNotesSorter) {
|
||||
super(app, plugin);
|
||||
this.plugin = plugin;
|
||||
this.loadState = SettingsLoadState.Unknown;
|
||||
|
||||
// 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;
|
||||
|
||||
// State-driven loading and rendering
|
||||
if (this.loadState !== SettingsLoadState.Loaded) {
|
||||
if (this.loadState === SettingsLoadState.Unknown) {
|
||||
this.loadState = SettingsLoadState.Loading;
|
||||
this.plugin
|
||||
.loadSettings()
|
||||
.then(() => {
|
||||
this.loadState = SettingsLoadState.Loaded;
|
||||
this.display();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error('[SorterSettings] Error loading settings:', err);
|
||||
// Allow retry on next display invocation
|
||||
this.loadState = SettingsLoadState.Unknown;
|
||||
});
|
||||
}
|
||||
// Do not render anything until settings are fully loaded
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
});
|
||||
this.renderHeader(containerEl);
|
||||
this.itemsContainer = this.createItemsContainer(containerEl);
|
||||
this.renderItems();
|
||||
this.renderAddButton(containerEl);
|
||||
this.renderApplyButton(containerEl);
|
||||
}
|
||||
|
||||
private renderHeader(container: HTMLElement): void {
|
||||
new Setting(container).setName(SETTINGS_TITLE).setHeading();
|
||||
}
|
||||
|
||||
private createItemsContainer(container: HTMLElement): HTMLElement {
|
||||
return container.createDiv();
|
||||
}
|
||||
|
||||
private renderItems(): void {
|
||||
if (!this.itemsContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
display(): void {
|
||||
const { containerEl } = this;
|
||||
containerEl.empty();
|
||||
this.itemsContainer.empty();
|
||||
|
||||
// Clear list of active components on redraw
|
||||
this.activeAutocompleteInputs.clear();
|
||||
this.inputElements.clear();
|
||||
|
||||
// Reset cache when opening settings
|
||||
this.cachedFolders = null;
|
||||
if (this.plugin.settings.items.length === 0) {
|
||||
this.renderEmptyState();
|
||||
return;
|
||||
}
|
||||
|
||||
// State-driven loading and rendering
|
||||
if (this.loadState !== SettingsLoadState.Loaded) {
|
||||
if (this.loadState === SettingsLoadState.Unknown) {
|
||||
this.loadState = SettingsLoadState.Loading;
|
||||
this.plugin.loadSettings()
|
||||
.then(() => {
|
||||
this.loadState = SettingsLoadState.Loaded;
|
||||
this.display();
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error("[SorterSettings] Error loading settings:", err);
|
||||
// Allow retry on next display invocation
|
||||
this.loadState = SettingsLoadState.Unknown;
|
||||
});
|
||||
}
|
||||
// Do not render anything until settings are fully loaded
|
||||
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 isValid = this.validatePath(this.plugin.settings.items[index].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);
|
||||
}
|
||||
|
||||
this.renderHeader(containerEl);
|
||||
this.itemsContainer = this.createItemsContainer(containerEl);
|
||||
this.renderItems();
|
||||
this.renderAddButton(containerEl);
|
||||
this.renderApplyButton(containerEl);
|
||||
}
|
||||
|
||||
private renderHeader(container: HTMLElement): void {
|
||||
new Setting(container).setName(SETTINGS_TITLE).setHeading();
|
||||
}
|
||||
|
||||
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 isValid = this.validatePath(this.plugin.settings.items[index].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(() => {
|
||||
this.deleteItem(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private deleteItem(index: number): 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 = () => {
|
||||
this.addItem();
|
||||
};
|
||||
}
|
||||
|
||||
private addItem(): 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() === ""
|
||||
// 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);
|
||||
},
|
||||
);
|
||||
|
||||
if (hasEmptyItems) {
|
||||
new Notice("Please fill in all existing items before adding a new one.");
|
||||
return;
|
||||
}
|
||||
// Register component for real-time updates
|
||||
this.activeAutocompleteInputs.add(autocompleteInput);
|
||||
|
||||
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",
|
||||
// Value change handler with real-time validation
|
||||
text.onChange((newValue: string) => {
|
||||
this.plugin.settings.items[index].path = newValue;
|
||||
// Real-time validation
|
||||
this.validateItem(index);
|
||||
});
|
||||
|
||||
const applyButton = buttonContainer.createEl("button", {
|
||||
cls: "apply-button",
|
||||
text: APPLY_BUTTON_TEXT,
|
||||
// Validation on blur
|
||||
text.inputEl.addEventListener('blur', () => {
|
||||
this.validateItem(index);
|
||||
});
|
||||
});
|
||||
|
||||
applyButton.onclick = async () => {
|
||||
await this.applySettings();
|
||||
};
|
||||
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(() => {
|
||||
this.deleteItem(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private deleteItem(index: number): 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 = () => {
|
||||
this.addItem();
|
||||
};
|
||||
}
|
||||
|
||||
private addItem(): 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() !== ""
|
||||
);
|
||||
this.plugin.settings.items.push({
|
||||
path: '',
|
||||
dateFormat: DEFAULT_DATE_FORMAT,
|
||||
});
|
||||
this.renderItems();
|
||||
}
|
||||
|
||||
// Validate all items
|
||||
const isValid = this.validateAllItems();
|
||||
/**
|
||||
* Renders apply settings button
|
||||
*/
|
||||
private renderApplyButton(container: HTMLElement): void {
|
||||
const buttonContainer = container.createDiv({
|
||||
cls: 'apply-button-container',
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
new Notice("Please fix all errors before applying settings.");
|
||||
return;
|
||||
}
|
||||
const applyButton = buttonContainer.createEl('button', {
|
||||
cls: 'apply-button',
|
||||
text: APPLY_BUTTON_TEXT,
|
||||
});
|
||||
|
||||
// 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.");
|
||||
}
|
||||
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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
100
styles.css
100
styles.css
|
|
@ -1,96 +1,94 @@
|
|||
.setting-item-custom-plugin {
|
||||
position: relative; /* Child elements are now positioned relative to this container */
|
||||
width: 100%; /* Ensure the container takes the required width */
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
position: relative; /* Child elements are now positioned relative to this container */
|
||||
width: 100%; /* Ensure the container takes the required width */
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
|
||||
.suggestions-list_1 {
|
||||
position: absolute; /* Positioned relative to setting-item-custom-plugin */
|
||||
top: 100%; /* Starts immediately below the input field */
|
||||
left: 0; /* Aligned to the left edge of the parent */
|
||||
width: 100%; /* Matches the width of the input field */
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
max-height: 400px; /* Maximum height to prevent overflow beyond the screen */
|
||||
z-index: 1000;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
/* Size adjusts to content automatically */
|
||||
height: var(--suggestions-list-height, auto);
|
||||
overflow-y: var(--suggestions-list-overflow, auto);
|
||||
min-height: 0;
|
||||
position: absolute; /* Positioned relative to setting-item-custom-plugin */
|
||||
top: 100%; /* Starts immediately below the input field */
|
||||
left: 0; /* Aligned to the left edge of the parent */
|
||||
width: 100%; /* Matches the width of the input field */
|
||||
background-color: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
max-height: 400px; /* Maximum height to prevent overflow beyond the screen */
|
||||
z-index: 1000;
|
||||
box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.2);
|
||||
border-radius: 5px;
|
||||
/* Size adjusts to content automatically */
|
||||
height: var(--suggestions-list-height, auto);
|
||||
overflow-y: var(--suggestions-list-overflow, 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);
|
||||
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);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.suggestion-item_1:last-child {
|
||||
border-bottom: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.suggestion-more {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.suggestions-list_1.no-scroll {
|
||||
overflow-y: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
.input-error {
|
||||
border: 2px solid var(--text-error) !important;
|
||||
background-color: var(--background-modifier-error) !important;
|
||||
border: 2px solid var(--text-error) !important;
|
||||
background-color: var(--background-modifier-error) !important;
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
border: 2px solid var(--text-success) !important;
|
||||
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);
|
||||
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;
|
||||
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);
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.apply-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* File Explorer sort button styles */
|
||||
.file-explorer-sort-button svg {
|
||||
color: var(--sort-button-icon-color, #fff);
|
||||
}
|
||||
color: var(--sort-button-icon-color, #fff);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@
|
|||
"importHelpers": true,
|
||||
"isolatedModules": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ES2020", "DOM"]
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["ES2020", "DOM"]
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules", "main.js", "backup"]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
122
types/types.d.ts
vendored
122
types/types.d.ts
vendored
|
|
@ -1,79 +1,79 @@
|
|||
import {TFolder, WorkspaceLeaf} from "obsidian";
|
||||
import { 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>;
|
||||
}
|
||||
export interface ViewRegistry {
|
||||
viewByType: Record<string, (leaf: WorkspaceLeaf) => unknown>;
|
||||
}
|
||||
|
||||
// undocumented internal interface - for experimental features
|
||||
export interface PluginInstance {
|
||||
id: string;
|
||||
}
|
||||
// undocumented internal interface - for experimental features
|
||||
export interface PluginInstance {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export type CommunityPluginId = 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 CommunityPlugin {
|
||||
manifest: {
|
||||
id: CommunityPluginId;
|
||||
};
|
||||
_loaded: boolean;
|
||||
}
|
||||
|
||||
// undocumented internal interface - for experimental features
|
||||
export interface CommunityPlugins {
|
||||
enabledPlugins: Set<CommunityPluginId>
|
||||
plugins: {[key: CommunityPluginId]: CommunityPlugin}
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
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 InstalledPlugin {
|
||||
enabled: boolean;
|
||||
instance: PluginInstance;
|
||||
}
|
||||
|
||||
// undocumented internal interface - for experimental features
|
||||
export interface InternalPlugins {
|
||||
plugins: Record<string, InstalledPlugin>;
|
||||
getPluginById(id: string): InstalledPlugin;
|
||||
}
|
||||
// undocumented internal interface - for experimental features
|
||||
export interface InternalPlugins {
|
||||
plugins: Record<string, InstalledPlugin>;
|
||||
getPluginById(id: string): InstalledPlugin;
|
||||
}
|
||||
|
||||
interface FileExplorerFolder {
|
||||
fileItems: Record<string, unknown>;
|
||||
}
|
||||
interface FileExplorerFolder {
|
||||
fileItems: Record<string, unknown>;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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): unknown[];
|
||||
export interface FileExplorerView extends View {
|
||||
createFolderDom(folder: TFolder): FileExplorerFolder;
|
||||
getSortedFolderItems(sortedFolder: TFolder): unknown[];
|
||||
|
||||
requestSort(): void;
|
||||
requestSort(): void;
|
||||
|
||||
sortOrder: string;
|
||||
headerDom: EI;
|
||||
fileItems: Record<string, unknown>;
|
||||
}
|
||||
sortOrder: string;
|
||||
headerDom: EI;
|
||||
fileItems: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
setSubmenu: () => Menu;
|
||||
}
|
||||
interface MenuItem {
|
||||
setSubmenu: () => Menu;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue