mirror of
https://github.com/reifat/daily-notes-sorter.git
synced 2026-07-22 06:42:08 +00:00
review fixes
This commit is contained in:
parent
d24635f0dc
commit
8e3d2f6f9c
8 changed files with 159 additions and 94 deletions
2
LICENSE
2
LICENSE
|
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 Nick (Reifat)
|
||||
Copyright (c) 2026 Nick (Reifat)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"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.",
|
||||
"description": "Automatically sort daily notes and files by date in the file explorer. Supports multiple date formats and flexible folder configuration.",
|
||||
"author": "Nick",
|
||||
"authorUrl": "https://github.com/Reifat",
|
||||
"isDesktopOnly": false
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ export class FileExplorerUtils {
|
|||
private sorter: Sorter;
|
||||
private settings: { items: FolderItem[]; sortAscending?: boolean };
|
||||
private pluginInstance: { saveSettings: () => Promise<void> } | undefined; // Reference to plugin instance for saving settings
|
||||
private fileExplorerInitialized = false;
|
||||
private fileExplorerInitializationInProgress = false;
|
||||
public sortAscending: boolean = true; // Public for access from closure
|
||||
|
||||
constructor(
|
||||
|
|
@ -102,10 +104,12 @@ export class FileExplorerUtils {
|
|||
* 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;
|
||||
const view = this.app.workspace.getLeavesOfType('file-explorer').first()?.view;
|
||||
if (view && typeof (view as Partial<FileExplorerView>).requestSort === 'function') {
|
||||
return view as FileExplorerView;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -125,18 +129,55 @@ export class FileExplorerUtils {
|
|||
const checkFileExplorer = (): void => {
|
||||
const fileExplorer = this.getFileExplorer();
|
||||
if (fileExplorer) {
|
||||
clearInterval(timer);
|
||||
window.clearInterval(timer);
|
||||
callback(fileExplorer);
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
clearInterval(timer);
|
||||
window.clearInterval(timer);
|
||||
reject(new Error('[FileExplorerUtils] FileExplorer was not loaded, reason is time up!'));
|
||||
}
|
||||
};
|
||||
const timer = setInterval(checkFileExplorer, interval);
|
||||
const timer = window.setInterval(checkFileExplorer, interval);
|
||||
});
|
||||
}
|
||||
|
||||
async ensureFileExplorerInitialized(
|
||||
onInitialized: (fileExplorer: FileExplorerView) => void,
|
||||
): Promise<void> {
|
||||
if (this.fileExplorerInitialized || this.fileExplorerInitializationInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.fileExplorerInitializationInProgress = true;
|
||||
return this.waitForFileExplorer(
|
||||
(fileExplorer) => {
|
||||
if (this.fileExplorerInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.patchFileExplorerFolder(fileExplorer);
|
||||
onInitialized(fileExplorer);
|
||||
this.fileExplorerInitialized = true;
|
||||
|
||||
// Use a small delay to ensure the patch is applied.
|
||||
window.setTimeout(() => {
|
||||
this.applySort();
|
||||
}, 100);
|
||||
},
|
||||
100,
|
||||
1500,
|
||||
)
|
||||
.catch(() => {
|
||||
// File Explorer may still be deferred/not visible yet. We'll retry on next layout change.
|
||||
console.debug(
|
||||
"[FileExplorerUtils] File Explorer may still be deferred/not visible yet. We'll retry on next layout change.",
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
this.fileExplorerInitializationInProgress = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks availability and patchability of FileExplorer
|
||||
*/
|
||||
|
|
|
|||
26
src/main.ts
26
src/main.ts
|
|
@ -46,22 +46,16 @@ export default class DailyNotesSorter extends Plugin {
|
|||
// Initialize UI for FileExplorer
|
||||
this.explorerUI = new ExplorerUI(this.app, this.fileExplorerUtils);
|
||||
|
||||
this.fileExplorerUtils
|
||||
.waitForFileExplorer((fileExplorer) => {
|
||||
this.fileExplorerUtils.patchFileExplorerFolder(fileExplorer);
|
||||
this.explorerUI.initialize(fileExplorer);
|
||||
|
||||
// Apply sorting after plugin load
|
||||
// Use a small delay to ensure the patch is applied
|
||||
setTimeout(() => {
|
||||
this.fileExplorerUtils.applySort();
|
||||
}, 100);
|
||||
})
|
||||
.catch((err: 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
|
||||
});
|
||||
void this.fileExplorerUtils.ensureFileExplorerInitialized((fileExplorer) => {
|
||||
this.explorerUI.initialize(fileExplorer);
|
||||
});
|
||||
this.registerEvent(
|
||||
this.app.workspace.on('layout-change', () => {
|
||||
void this.fileExplorerUtils.ensureFileExplorerInitialized((fileExplorer) => {
|
||||
this.explorerUI.initialize(fileExplorer);
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
this.addSettingTab(new SorterSettings(this.app, this));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ export class AutocompleteInput {
|
|||
private inputEl: HTMLInputElement;
|
||||
private onValueChange: (value: string) => void;
|
||||
private getSuggestions: () => string[];
|
||||
private blurTimeoutId: number | null = null;
|
||||
private disconnectObserver: MutationObserver | null = null;
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
|
|
@ -18,13 +20,14 @@ export class AutocompleteInput {
|
|||
this.getSuggestions = getSuggestions;
|
||||
this.suggestionsList = this.createSuggestionsList(container);
|
||||
this.attachEventListeners();
|
||||
this.setupAutoCleanupOnDetach();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates suggestion list if it's visible
|
||||
*/
|
||||
public refresh(): void {
|
||||
if (!this.suggestionsList.hasClass('hidden')) {
|
||||
if (!this.suggestionsList.hasClass('dns-hidden')) {
|
||||
const query = this.inputEl.value.trim();
|
||||
if (query) {
|
||||
this.handleInput();
|
||||
|
|
@ -39,35 +42,58 @@ export class AutocompleteInput {
|
|||
* Checks if suggestion list is visible
|
||||
*/
|
||||
public isVisible(): boolean {
|
||||
return !this.suggestionsList.hasClass('hidden');
|
||||
return !this.suggestionsList.hasClass('dns-hidden');
|
||||
}
|
||||
|
||||
private createSuggestionsList(container: HTMLElement): HTMLElement {
|
||||
return container.createDiv({
|
||||
cls: 'suggestions-list_1 hidden',
|
||||
cls: 'dns-suggestions-list dns-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();
|
||||
}
|
||||
});
|
||||
this.inputEl.addEventListener('input', this.handleInput);
|
||||
this.inputEl.addEventListener('focus', this.handleFocus);
|
||||
this.inputEl.addEventListener('blur', this.handleBlur);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
private handleInput(): void {
|
||||
private setupAutoCleanupOnDetach(): void {
|
||||
const rootNode = this.inputEl.ownerDocument.body;
|
||||
this.disconnectObserver = new MutationObserver(() => {
|
||||
if (!this.inputEl.isConnected) {
|
||||
this.dispose();
|
||||
}
|
||||
});
|
||||
this.disconnectObserver.observe(rootNode, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
private dispose(): void {
|
||||
if (!this.disconnectObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.inputEl.removeEventListener('input', this.handleInput);
|
||||
this.inputEl.removeEventListener('focus', this.handleFocus);
|
||||
this.inputEl.removeEventListener('blur', this.handleBlur);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
|
||||
if (this.blurTimeoutId !== null) {
|
||||
window.clearTimeout(this.blurTimeoutId);
|
||||
this.blurTimeoutId = null;
|
||||
}
|
||||
|
||||
this.disconnectObserver.disconnect();
|
||||
this.disconnectObserver = null;
|
||||
}
|
||||
|
||||
private handleResize = (): void => {
|
||||
if (!this.suggestionsList.hasClass('dns-hidden')) {
|
||||
this.adjustListHeight();
|
||||
}
|
||||
};
|
||||
|
||||
private handleInput = (): void => {
|
||||
const query = this.inputEl.value.trim();
|
||||
this.suggestionsList.empty();
|
||||
|
||||
|
|
@ -82,9 +108,9 @@ export class AutocompleteInput {
|
|||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleFocus(): void {
|
||||
private handleFocus = (): void => {
|
||||
if (this.inputEl.value.trim()) {
|
||||
const query = this.inputEl.value.trim();
|
||||
const filtered = this.filterSuggestions(query);
|
||||
|
|
@ -94,14 +120,18 @@ export class AutocompleteInput {
|
|||
this.showSuggestions(filtered);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private handleBlur(): void {
|
||||
private handleBlur = (): void => {
|
||||
// Delay for handling click on suggestion
|
||||
setTimeout(() => {
|
||||
if (this.blurTimeoutId !== null) {
|
||||
window.clearTimeout(this.blurTimeoutId);
|
||||
}
|
||||
this.blurTimeoutId = window.setTimeout(() => {
|
||||
this.hideSuggestions();
|
||||
this.blurTimeoutId = null;
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
|
||||
private filterSuggestions(query: string): string[] {
|
||||
const lowerQuery = query.toLowerCase();
|
||||
|
|
@ -112,7 +142,7 @@ export class AutocompleteInput {
|
|||
private showSuggestions(suggestions: string[]): void {
|
||||
// Always clear list before adding new elements
|
||||
this.suggestionsList.empty();
|
||||
this.suggestionsList.removeClass('hidden');
|
||||
this.suggestionsList.removeClass('dns-hidden');
|
||||
|
||||
// Limit number of displayed suggestions for performance
|
||||
const maxSuggestions = 20;
|
||||
|
|
@ -132,7 +162,7 @@ export class AutocompleteInput {
|
|||
// Show message if there are more suggestions
|
||||
if (suggestions.length > maxSuggestions) {
|
||||
this.suggestionsList.createDiv({
|
||||
cls: 'suggestion-item_1 suggestion-more',
|
||||
cls: 'dns-suggestion-item dns-suggestion-more',
|
||||
text: `... and ${suggestions.length - maxSuggestions} more`,
|
||||
});
|
||||
}
|
||||
|
|
@ -149,7 +179,7 @@ export class AutocompleteInput {
|
|||
*/
|
||||
private adjustListHeight(): void {
|
||||
// Check if list is visible
|
||||
if (this.suggestionsList.hasClass('hidden')) {
|
||||
if (this.suggestionsList.hasClass('dns-hidden')) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +188,7 @@ export class AutocompleteInput {
|
|||
'--suggestions-list-height': 'auto',
|
||||
'--suggestions-list-overflow': 'auto',
|
||||
});
|
||||
this.suggestionsList.removeClass('no-scroll');
|
||||
this.suggestionsList.removeClass('dns-no-scroll');
|
||||
|
||||
// Get actual content height
|
||||
const contentHeight = this.suggestionsList.scrollHeight;
|
||||
|
|
@ -176,14 +206,14 @@ export class AutocompleteInput {
|
|||
'--suggestions-list-height': `${contentHeight}px`,
|
||||
'--suggestions-list-overflow': 'hidden',
|
||||
});
|
||||
this.suggestionsList.addClass('no-scroll');
|
||||
this.suggestionsList.addClass('dns-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');
|
||||
this.suggestionsList.removeClass('dns-no-scroll');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -199,7 +229,7 @@ export class AutocompleteInput {
|
|||
}
|
||||
|
||||
const item = this.suggestionsList.createDiv({
|
||||
cls: 'suggestion-item_1',
|
||||
cls: 'dns-suggestion-item',
|
||||
text: suggestion,
|
||||
});
|
||||
|
||||
|
|
@ -211,6 +241,6 @@ export class AutocompleteInput {
|
|||
}
|
||||
|
||||
private hideSuggestions(): void {
|
||||
this.suggestionsList.addClass('hidden');
|
||||
this.suggestionsList.addClass('dns-hidden');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ export class ExplorerUI {
|
|||
);
|
||||
|
||||
// Style the button using CSS class and custom color
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
const button = fileExplorer.headerDom.navButtonsEl?.lastElementChild as HTMLElement | null;
|
||||
if (button) {
|
||||
button.addClass('file-explorer-sort-button');
|
||||
button.addClass('dns-file-explorer-sort-button');
|
||||
const svg = button.querySelector('svg') as {
|
||||
setCssProps?: (props: Record<string, string>) => void;
|
||||
} | null;
|
||||
|
|
@ -72,7 +72,7 @@ export class ExplorerUI {
|
|||
|
||||
// Reopen FileExplorer leaves with saved state
|
||||
// Use setTimeout to ensure detach completes before reopening
|
||||
setTimeout(() => {
|
||||
window.setTimeout(() => {
|
||||
// Choose the view state to restore: use the first saved state
|
||||
const preferred = leavesData[0];
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ export class SorterSettings extends PluginSettingTab {
|
|||
}
|
||||
|
||||
const itemDiv = this.itemsContainer.createDiv({
|
||||
cls: 'setting-item-custom-plugin',
|
||||
cls: 'dns-setting-item',
|
||||
});
|
||||
|
||||
const inputSetting = this.createInputSetting(itemDiv, item.path || '', index);
|
||||
|
|
@ -213,12 +213,12 @@ export class SorterSettings extends PluginSettingTab {
|
|||
|
||||
if (inputEl) {
|
||||
if (!isValidPath) {
|
||||
inputEl.addClass('input-error');
|
||||
inputEl.removeClass('input-valid');
|
||||
inputEl.addClass('dns-input-error');
|
||||
inputEl.removeClass('dns-input-valid');
|
||||
isValid = false;
|
||||
} else {
|
||||
inputEl.removeClass('input-error');
|
||||
inputEl.addClass('input-valid');
|
||||
inputEl.removeClass('dns-input-error');
|
||||
inputEl.addClass('dns-input-valid');
|
||||
}
|
||||
} else {
|
||||
if (!isValidPath) {
|
||||
|
|
@ -239,11 +239,11 @@ export class SorterSettings extends PluginSettingTab {
|
|||
|
||||
if (inputEl) {
|
||||
if (!isValid) {
|
||||
inputEl.addClass('input-error');
|
||||
inputEl.removeClass('input-valid');
|
||||
inputEl.addClass('dns-input-error');
|
||||
inputEl.removeClass('dns-input-valid');
|
||||
} else {
|
||||
inputEl.removeClass('input-error');
|
||||
inputEl.addClass('input-valid');
|
||||
inputEl.removeClass('dns-input-error');
|
||||
inputEl.addClass('dns-input-valid');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -379,11 +379,11 @@ export class SorterSettings extends PluginSettingTab {
|
|||
*/
|
||||
private renderApplyButton(container: HTMLElement): void {
|
||||
const buttonContainer = container.createDiv({
|
||||
cls: 'apply-button-container',
|
||||
cls: 'dns-apply-button-container',
|
||||
});
|
||||
|
||||
const applyButton = buttonContainer.createEl('button', {
|
||||
cls: 'apply-button',
|
||||
cls: 'dns-apply-button',
|
||||
text: APPLY_BUTTON_TEXT,
|
||||
});
|
||||
|
||||
|
|
@ -415,8 +415,8 @@ export class SorterSettings extends PluginSettingTab {
|
|||
|
||||
// Remove all validation indicators after successful application
|
||||
this.inputElements.forEach((inputEl) => {
|
||||
inputEl.removeClass('input-error');
|
||||
inputEl.removeClass('input-valid');
|
||||
inputEl.removeClass('dns-input-error');
|
||||
inputEl.removeClass('dns-input-valid');
|
||||
});
|
||||
|
||||
new Notice('Settings applied successfully!');
|
||||
|
|
|
|||
38
styles.css
38
styles.css
|
|
@ -1,12 +1,12 @@
|
|||
.setting-item-custom-plugin {
|
||||
.dns-setting-item {
|
||||
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 */
|
||||
.dns-suggestions-list {
|
||||
position: absolute; /* Positioned relative to dns-setting-item */
|
||||
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 */
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.suggestion-item_1 {
|
||||
.dns-suggestion-item {
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
color: var(--text-normal);
|
||||
|
|
@ -30,38 +30,38 @@
|
|||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.suggestion-item_1:hover {
|
||||
.dns-suggestion-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.suggestion-item_1:last-child {
|
||||
.dns-suggestion-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.suggestion-more {
|
||||
.dns-suggestion-more {
|
||||
font-style: italic;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.suggestions-list_1.no-scroll {
|
||||
.dns-suggestions-list.dns-no-scroll {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
.dns-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Validation styles */
|
||||
.input-error {
|
||||
border: 2px solid var(--text-error) !important;
|
||||
background-color: var(--background-modifier-error) !important;
|
||||
.dns-setting-item input.dns-input-error {
|
||||
border: 2px solid var(--text-error);
|
||||
background-color: var(--background-modifier-error);
|
||||
}
|
||||
|
||||
.input-valid {
|
||||
border: 2px solid var(--text-success) !important;
|
||||
.dns-setting-item input.dns-input-valid {
|
||||
border: 2px solid var(--text-success);
|
||||
}
|
||||
|
||||
.apply-button-container {
|
||||
.dns-apply-button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
border-top: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.apply-button {
|
||||
.dns-apply-button {
|
||||
padding: 8px 20px;
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
|
|
@ -79,16 +79,16 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
.apply-button:hover {
|
||||
.dns-apply-button:hover {
|
||||
background-color: var(--interactive-accent-hover);
|
||||
}
|
||||
|
||||
.apply-button:disabled {
|
||||
.dns-apply-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* File Explorer sort button styles */
|
||||
.file-explorer-sort-button svg {
|
||||
.dns-file-explorer-sort-button svg {
|
||||
color: var(--sort-button-icon-color, #fff);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue