refactor: remove unnecessary await calls, clean up ESLint comments, and refine UI element selection.

This commit is contained in:
dralkh 2026-01-01 11:22:31 +03:00
parent 341c222231
commit 77b214b936
25 changed files with 253 additions and 271 deletions

View file

@ -13,18 +13,15 @@ export class ClaudeService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.claudeApiKey) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Claude API key is not set, please add it in the Spaceforge settings');
new Notice('Claude API key is not set. Please add it in the Spaceforge settings');
return null;
}
if (!settings.claudeModel) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Claude model is not set, please add it in the Spaceforge settings');
new Notice('Claude model is not set. Please add it in the Spaceforge settings');
return null;
}
try {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Generating MCQs using Claude...');
// Determine the number of questions to generate
@ -41,8 +38,7 @@ export class ClaudeService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate valid MCQs from Claude, please try again');
new Notice('Failed to generate valid MCQs from Claude. Please try again');
return null;
}
@ -52,8 +48,7 @@ export class ClaudeService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate MCQs with Claude, please check console for details');
new Notice('Failed to generate MCQs with Claude. Please check console for details');
return null;
}
}
@ -227,8 +222,7 @@ export class ClaudeService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Error parsing MCQ response from Claude, please try again');
new Notice('Error parsing MCQ response from Claude. Please try again');
return [];
}
}

View file

@ -15,18 +15,15 @@ export class GeminiService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.geminiApiKey) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Gemini API key is not set, please add it in the Spaceforge settings');
new Notice('Gemini API key is not set. Please add it in the Spaceforge settings');
return null;
}
if (!settings.geminiModel) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Gemini model is not set, please add it in the Spaceforge settings');
new Notice('Gemini model is not set. Please add it in the Spaceforge settings');
return null;
}
try {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Generating MCQs using Gemini...');
// Determine the number of questions to generate
@ -43,8 +40,7 @@ export class GeminiService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate valid MCQs from Gemini, please try again');
new Notice('Failed to generate valid MCQs from Gemini. Please try again');
return null;
}
@ -54,8 +50,7 @@ export class GeminiService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate MCQs with Gemini, please check console for details');
new Notice('Failed to generate MCQs with Gemini. Please check console for details');
return null;
}
}
@ -179,8 +174,7 @@ export class GeminiService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Error parsing MCQ response from Gemini, please try again');
new Notice('Error parsing MCQ response from Gemini. Please try again');
return [];
}
}

View file

@ -13,18 +13,15 @@ export class OllamaService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.ollamaApiUrl) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Ollama API URL is not set, please add it in the Spaceforge settings');
new Notice('Ollama API URL is not set. Please add it in the Spaceforge settings');
return null;
}
if (!settings.ollamaModel) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Ollama model is not set, please add it in the Spaceforge settings');
new Notice('Ollama model is not set. Please add it in the Spaceforge settings');
return null;
}
try {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Generating MCQs using Ollama...');
// Determine the number of questions to generate
@ -41,8 +38,7 @@ export class OllamaService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate valid MCQs from Ollama, please try again');
new Notice('Failed to generate valid MCQs from Ollama. Please try again');
return null;
}
@ -52,8 +48,7 @@ export class OllamaService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate MCQs with Ollama, please check console for details');
new Notice('Failed to generate MCQs with Ollama. Please check console for details');
return null;
}
}
@ -176,8 +171,7 @@ export class OllamaService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Error parsing MCQ response from Ollama, please try again');
new Notice('Error parsing MCQ response from Ollama. Please try again');
return [];
}
}

View file

@ -13,13 +13,11 @@ export class OpenAIService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.openaiApiKey) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('OpenAI API key is not set, please add it in the Spaceforge settings');
new Notice('OpenAI API key is not set. Please add it in the Spaceforge settings');
return null;
}
if (!settings.openaiModel) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('OpenAI model is not set, please add it in the Spaceforge settings');
new Notice('OpenAI model is not set. Please add it in the Spaceforge settings');
return null;
}
@ -172,8 +170,7 @@ export class OpenAIService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Error parsing MCQ response from OpenAI, please try again');
new Notice('Error parsing MCQ response from OpenAI. Please try again');
return [];
}
}

View file

@ -33,14 +33,12 @@ export class OpenRouterService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
// Check if API key is set
if (!settings.openRouterApiKey) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('OpenRouter API key is not set, please add it in the settings');
new Notice('OpenRouter API key is not set. Please add it in the settings');
return null;
}
try {
// Show loading notice
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Generating MCQs using OpenRouter...');
// Determine the number of questions to generate
@ -65,8 +63,7 @@ export class OpenRouterService implements IMCQGenerationService {
// Ensure we have at least one question if requested
if (questions.length === 0) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate valid MCQs from OpenRouter, please try again');
new Notice('Failed to generate valid MCQs from OpenRouter. Please try again');
return null;
}
@ -79,8 +76,7 @@ export class OpenRouterService implements IMCQGenerationService {
return mcqSet;
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate MCQs with OpenRouter, please check console for details');
new Notice('Failed to generate MCQs with OpenRouter. Please check console for details');
return null;
}
}
@ -298,8 +294,7 @@ For example:
// Use numQuestionsToGenerate instead of settings.mcqQuestionsPerNote
return questions.slice(0, numQuestionsToGenerate);
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Error parsing MCQ response from OpenRouter, please try again');
new Notice('Error parsing MCQ response from OpenRouter. Please try again');
return [];
}
}

View file

@ -13,18 +13,15 @@ export class TogetherService implements IMCQGenerationService {
async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise<MCQSet | null> {
if (!settings.togetherApiKey) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Together AI API key is not set, please add it in the Spaceforge settings');
new Notice('Together AI API key is not set. Please add it in the Spaceforge settings');
return null;
}
if (!settings.togetherModel) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Together AI model is not set, please add it in the Spaceforge settings');
new Notice('Together AI model is not set. Please add it in the Spaceforge settings');
return null;
}
try {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Generating MCQs using Together AI...');
// Determine the number of questions to generate
@ -41,8 +38,7 @@ export class TogetherService implements IMCQGenerationService {
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate valid MCQs from Together AI, please try again');
new Notice('Failed to generate valid MCQs from Together AI. Please try again');
return null;
}
@ -52,8 +48,7 @@ export class TogetherService implements IMCQGenerationService {
generatedAt: Date.now()
};
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Failed to generate MCQs with Together AI, please check console for details');
new Notice('Failed to generate MCQs with Together AI. Please check console for details');
return null;
}
}
@ -205,8 +200,7 @@ export class TogetherService implements IMCQGenerationService {
}
return questions.slice(0, numQuestionsToGenerate); // Use calculated number
} catch {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice('Error parsing MCQ response from Together AI, please try again');
new Notice('Error parsing MCQ response from Together AI. Please try again');
return [];
}
}

View file

@ -134,8 +134,7 @@ export class ReviewBatchController implements IReviewBatchController {
}
if (!this.plugin.mcqController) {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice("MCQ controller not initialized, please check MCQ settings");
new Notice("MCQ controller not initialized. Please check MCQ settings");
return;
}

View file

@ -259,7 +259,7 @@ export class ReviewControllerCore implements IReviewController {
if (advanced) {
await this.plugin.savePluginData();
await this.handleNoteAdvanced(path); // New handler for advancing
this.handleNoteAdvanced(path); // New handler for advancing
void this.plugin.getSidebarView()?.refresh();
new Notice(`Note advanced.`); // Controller handles notice for advance
@ -320,7 +320,7 @@ export class ReviewControllerCore implements IReviewController {
}
// Update custom order in storage
await this.plugin.dataStorage.reviewScheduleService.updateCustomNoteOrder(this.traversalOrder);
this.plugin.dataStorage.reviewScheduleService.updateCustomNoteOrder(this.traversalOrder);
// Removed redundant savePluginData() here; postponeNote already saves.
// If the postponed note was the current one, select a new note

View file

@ -68,12 +68,12 @@ export class MCQController {
externalOnCompleteCallback?: (path: string, success: boolean) => void
): Promise<void> {
if (!this.plugin.settings.enableMCQ) {
new Notice('MCQ feature is disabled in settings.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ feature is disabled in settings.');
if (externalOnCompleteCallback) externalOnCompleteCallback(notePath, false);
return;
}
if (!this.mcqGenerationService) {
new Notice('MCQ generation service is not available. Check API provider settings.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ generation service is not available. Check API provider settings.');
return;
}
@ -135,7 +135,7 @@ export class MCQController {
// Pass the new internal callback to the modal constructor
new MCQModal(this.plugin, notePath, mcqSet, internalOnComplete).open();
} catch {
new Notice('Error starting MCQ review. Please check console for details.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Error starting MCQ review. Please check console for details.');
if (externalOnCompleteCallback) externalOnCompleteCallback(notePath, false);
}
}
@ -149,7 +149,7 @@ export class MCQController {
*/
async generateMCQs(notePath: string, forceRegeneration = false): Promise<MCQSet | null> {
if (!this.plugin.settings.enableMCQ || !this.mcqGenerationService) {
new Notice('MCQ feature is disabled or the generation service is not available. Check API provider settings.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ feature is disabled or the generation service is not available. Check API provider settings.');
return null;
}
@ -202,11 +202,11 @@ export class MCQController {
*/
async startConsolidatedMCQReviewForSelectedDate(): Promise<void> {
if (!this.plugin.settings.enableMCQ) {
new Notice('MCQ feature is disabled in settings.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ feature is disabled in settings.');
return;
}
if (!this.mcqGenerationService) {
new Notice('MCQ generation service is not available. Check API provider settings.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ generation service is not available. Check API provider settings.');
return;
}
@ -295,7 +295,6 @@ export class MCQController {
if (reviewsProcessed > 0) {
new Notice(`${reviewsProcessed} note review(s) updated based on consolidated MCQ session.`);
} else {
// eslint-disable-next-line obsidianmd/ui/sentence-case
new Notice("No note reviews were updated from the MCQ session");
}
})();

59
main.ts
View file

@ -29,6 +29,11 @@ import { CalendarEventService } from './services/calendar-event-service';
/**
* Spaceforge: Spaced Repetition Plugin for Obsidian
*/
export interface SpaceforgeEvents {
'sidebar-update': [];
'pomodoro-update': [];
}
export default class SpaceforgePlugin extends Plugin {
settings: SpaceforgeSettings;
pluginState: PluginStateData;
@ -56,10 +61,10 @@ export default class SpaceforgePlugin extends Plugin {
contextMenuHandler: ContextMenuHandler;
// sidebarView: ReviewSidebarView; // Avoid storing direct references to views
public clickedDateFromCalendar: Date | null = null;
events: EventEmitter;
events: EventEmitter<SpaceforgeEvents>;
async onload(): Promise<void> {
this.events = new EventEmitter();
this.events = new EventEmitter<SpaceforgeEvents>();
// Initialize settings with defaults FIRST
this.settings = { ...DEFAULT_SETTINGS };
@ -121,7 +126,6 @@ export default class SpaceforgePlugin extends Plugin {
EstimationUtils.setPlugin(this);
this.addIcons();
this.contextMenuHandler.register();
// eslint-disable-next-line obsidianmd/ui/sentence-case
this.addRibbonIcon('calendar-clock', 'Spaceforge review', async () => {
await this.activateSidebarView();
});
@ -130,7 +134,6 @@ export default class SpaceforgePlugin extends Plugin {
this.addCommand({
id: 'add-selected-file-to-review',
// eslint-disable-next-line obsidianmd/ui/sentence-case
name: 'Add selected file to review schedule (file explorer)',
callback: async () => {
const fileExplorerLeaf = this.app.workspace.getLeavesOfType('file-explorer')[0];
@ -142,7 +145,7 @@ export default class SpaceforgePlugin extends Plugin {
await this.savePluginData();
new Notice(`Added "${selectedFile.path}" to review schedule.`);
} else {
new Notice("Selected item is not a markdown file."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Selected item is not a markdown file.");
}
} else {
new Notice("No file selected in file explorer");
@ -222,7 +225,7 @@ export default class SpaceforgePlugin extends Plugin {
await this.savePluginData();
} catch { /* handle error */ }
})();
} catch (_error) {
} catch {
try {
const minimalBackup = JSON.stringify({ settings: this.settings, reviewData: { schedules: this.reviewScheduleService.schedules || {}, customNoteOrder: this.reviewScheduleService.customNoteOrder || [], lastLinkAnalysisTimestamp: this.reviewScheduleService.lastLinkAnalysisTimestamp, version: this.manifest.version } });
this.app.saveLocalStorage('spaceforge-minimal-backup', minimalBackup);
@ -232,7 +235,7 @@ export default class SpaceforgePlugin extends Plugin {
window.addEventListener('beforeunload', this.beforeUnloadHandler);
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
// eslint-disable-next-line @typescript-eslint/no-misused-promises -- onunload can be async
async onunload(): Promise<void> {
if (this.cssHotReloadIntervalId !== null) {
window.clearInterval(this.cssHotReloadIntervalId);
@ -351,7 +354,7 @@ export default class SpaceforgePlugin extends Plugin {
rawLoadedData = JSON.parse(oldJsonData);
// Data will be saved to new path by savePluginData later
}
} catch (_migrationReadError) { /* handle error */ }
} catch { /* handle error */ }
}
if (!rawLoadedData) {
new Notice(`Spaceforge: No data file found at custom path ${effectivePath}. New data file will be created on save.`, 3000);
@ -441,7 +444,7 @@ export default class SpaceforgePlugin extends Plugin {
if (parsedBackup.reviewData) {
this.settings = { ...DEFAULT_SETTINGS, ...parsedBackup.settings };
this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA, ...parsedBackup.reviewData };
new Notice("Spaceforge: Recovered data from backup", 5000); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Spaceforge: Recovered data from backup", 5000);
} else {
throw new Error("Invalid backup format");
}
@ -452,7 +455,7 @@ export default class SpaceforgePlugin extends Plugin {
console.warn("Spaceforge: Backup recovery failed, using defaults:", backupError);
this.settings = { ...DEFAULT_SETTINGS };
this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA };
new Notice("Spaceforge: Error loading data, using defaults to prevent crash", 5000); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Spaceforge: Error loading data, using defaults to prevent crash", 5000);
}
// Repopulate services
@ -542,16 +545,16 @@ export default class SpaceforgePlugin extends Plugin {
if (oldFile instanceof TFile) {
// Instead of deleting, we'll keep the old file as a backup
// Users can manually clean it up later if needed
new Notice(`Spaceforge: Successfully saved to custom path, original data file kept as backup for safety`, 5000); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice(`Spaceforge: Successfully saved to custom path, original data file kept as backup for safety`, 5000);
}
} catch (writeError) {
new Notice(`Error saving data to custom path ${effectiveSavePath}: ${writeError.message}. Falling back to default path for this save.`, 10000);
// Fallback save to default location if custom path write fails
try {
await this.saveData(dataToSave); // Plugin's internal save
new Notice(`Spaceforge: Data saved to default plugin folder due to error with custom path`, 5000); // eslint-disable-line obsidianmd/ui/sentence-case
} catch (_error) {
new Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations`, 10000); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice(`Spaceforge: Data saved to default plugin folder due to error with custom path`, 5000);
} catch {
new Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations`, 10000);
}
}
} else {
@ -563,8 +566,8 @@ export default class SpaceforgePlugin extends Plugin {
// Removed localStorage backup for settings as it's not the source of truth for the path anymore.
// If user wants to backup settings, they can use the export feature.
} catch (_error) {
new Notice("Error saving Spaceforge data, check console for details", 5000); // eslint-disable-line obsidianmd/ui/sentence-case
} catch {
new Notice("Error saving Spaceforge data. Check console for details", 5000);
}
}
@ -583,7 +586,7 @@ export default class SpaceforgePlugin extends Plugin {
});
void this.app.workspace.revealLeaf(leaf); // Reveal the newly created leaf
} else {
new Notice("Spaceforge: Could not open sidebar view"); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Spaceforge: Could not open sidebar view");
}
}
}
@ -613,7 +616,7 @@ export default class SpaceforgePlugin extends Plugin {
if (activeFile && activeFile instanceof TFile && activeFile.extension === 'md') {
void this.reviewController.reviewNote(activeFile.path);
} else {
new Notice('No active markdown file to review'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('No active markdown file to review');
}
},
});
@ -628,7 +631,7 @@ export default class SpaceforgePlugin extends Plugin {
await this.savePluginData();
new Notice(`Added "${activeFile.path}" to review schedule`);
} else {
new Notice('No active markdown file to add to review'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('No active markdown file to add to review');
}
},
});
@ -643,7 +646,7 @@ export default class SpaceforgePlugin extends Plugin {
// Use the same logic as the context menu for consistency
await this.contextMenuHandler.addFolderToReview(folder);
} else {
new Notice("Could not determine the current note's folder, no active file, or parent is not a folder"); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Could not determine the current note's folder, no active file, or parent is not a folder");
}
},
});
@ -661,7 +664,7 @@ export default class SpaceforgePlugin extends Plugin {
// Pass the mcqService for data management, and the new mcqGenerationService for API calls
this.mcqController = new MCQController(this, this.mcqService, this.mcqGenerationService);
} else {
new Notice('MCQ generation service could not be initialized, check API provider settings in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ generation service could not be initialized. Check API provider settings in Spaceforge settings');
}
}
}
@ -670,37 +673,37 @@ export default class SpaceforgePlugin extends Plugin {
switch (this.settings.mcqApiProvider) {
case ApiProvider.OpenRouter:
if (!this.settings.openRouterApiKey) {
new Notice('OpenRouter API key is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('OpenRouter API key is not set in Spaceforge settings');
return undefined;
}
return new OpenRouterService(this);
case ApiProvider.OpenAI:
if (!this.settings.openaiApiKey) {
new Notice('OpenAI API key is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('OpenAI API key is not set in Spaceforge settings');
return undefined;
}
return new OpenAIService(this);
case ApiProvider.Ollama:
if (!this.settings.ollamaApiUrl || !this.settings.ollamaModel) {
new Notice('Ollama API URL or model is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Ollama API URL or model is not set in Spaceforge settings');
return undefined;
}
return new OllamaService(this);
case ApiProvider.Gemini:
if (!this.settings.geminiApiKey) {
new Notice('Gemini API key is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Gemini API key is not set in Spaceforge settings');
return undefined;
}
return new GeminiService(this);
case ApiProvider.Claude:
if (!this.settings.claudeApiKey || !this.settings.claudeModel) {
new Notice('Claude API key or model is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Claude API key or model is not set in Spaceforge settings');
return undefined;
}
return new ClaudeService(this);
case ApiProvider.Together:
if (!this.settings.togetherApiKey || !this.settings.togetherModel) {
new Notice('Together AI API key or model is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Together AI API key or model is not set in Spaceforge settings');
return undefined;
}
return new TogetherService(this);
@ -708,7 +711,7 @@ export default class SpaceforgePlugin extends Plugin {
// It's good practice to handle unexpected enum values, even if TypeScript provides some safety.
// This could happen if settings data is corrupted or from an older version.
// const exhaustiveCheck: never = this.settings.mcqApiProvider; // This will cause a type error now, which is good!
new Notice(`Unsupported MCQ API provider selected: ${this.settings.mcqApiProvider}`);
new Notice(`Unsupported MCQ API provider selected: ${this.settings.mcqApiProvider as any}`);
return undefined;
}
}

View file

@ -63,7 +63,7 @@ export class FsrsScheduleService {
case FsrsRating.Hard: return TsFsrsRating.Hard;
case FsrsRating.Good: return TsFsrsRating.Good;
case FsrsRating.Easy: return TsFsrsRating.Easy;
default: throw new Error(`Unknown FsrsRating: ${rating}`);
default: throw new Error(`Unknown FsrsRating: ${rating as any}`);
}
}

View file

@ -76,7 +76,7 @@ export class ReviewScheduleService {
// Check if file exists and is a markdown file
const file = this.plugin.app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile) || file.extension !== "md") {
new Notice("Only markdown files can be added to the review schedule."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Only markdown files can be added to the review schedule.");
return;
}

View file

@ -113,7 +113,7 @@ export class BatchReviewModal extends Modal {
async collectAllMCQs(): Promise<void> {
const { contentEl } = this;
contentEl.empty();
new Setting(contentEl).setName("Collecting MCQs").setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case
new Setting(contentEl).setName("Collecting MCQs").setHeading();
const progressEl = contentEl.createDiv("batch-review-progress");
progressEl.createEl("p", { text: `Preparing MCQs for ${this.notes.length} notes...` });
const progressBar = contentEl.createDiv("mcq-collection-progress sf-progress-bar-container-batch");
@ -172,12 +172,12 @@ export class BatchReviewModal extends Modal {
);
consolidatedModal.open();
} catch (_error) {
new Notice("Error showing MCQ review. Falling back to manual review."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Error showing MCQ review. Falling back to manual review.");
this.open();
void this.processNextManual();
}
} else {
new Notice("MCQ controller not available. Falling back to manual review."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("MCQ controller not available. Falling back to manual review.");
this.open();
void this.processNextManual();
}
@ -198,7 +198,7 @@ export class BatchReviewModal extends Modal {
const note = this.notes[this.currentIndex];
const { contentEl } = this;
contentEl.empty();
new Setting(contentEl).setName("MCQ review in progress").setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case
new Setting(contentEl).setName("MCQ review in progress").setHeading();
const progressEl = contentEl.createDiv("batch-review-progress");
progressEl.createEl("p", { text: `Processing note ${this.currentIndex + 1}/${this.notes.length}` });
const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
@ -254,7 +254,7 @@ export class BatchReviewModal extends Modal {
void this.processNextManual();
}
} else {
new Notice("MCQ controller not available, falling back to manual review."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("MCQ controller not available, falling back to manual review.");
this.open();
void this.processNextManual();
}
@ -285,17 +285,17 @@ export class BatchReviewModal extends Modal {
}
const buttonsContainer = contentEl.createDiv("batch-review-buttons");
const blackoutButton = buttonsContainer.createEl("button", { text: "0: Complete blackout", cls: "review-button review-button-complete-blackout" }); // eslint-disable-line obsidianmd/ui/sentence-case
const blackoutButton = buttonsContainer.createEl("button", { text: "0: Complete blackout", cls: "review-button review-button-complete-blackout" });
blackoutButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.CompleteBlackout));
const incorrectButton = buttonsContainer.createEl("button", { text: "1: Incorrect response", cls: "review-button review-button-incorrect" }); // eslint-disable-line obsidianmd/ui/sentence-case
const incorrectButton = buttonsContainer.createEl("button", { text: "1: Incorrect response", cls: "review-button review-button-incorrect" });
incorrectButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.IncorrectResponse));
const incorrectFamiliarButton = buttonsContainer.createEl("button", { text: "2: Incorrect but familiar", cls: "review-button review-button-incorrect-familiar" }); // eslint-disable-line obsidianmd/ui/sentence-case
const incorrectFamiliarButton = buttonsContainer.createEl("button", { text: "2: Incorrect but familiar", cls: "review-button review-button-incorrect-familiar" });
incorrectFamiliarButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.IncorrectButFamiliar));
const correctDifficultyButton = buttonsContainer.createEl("button", { text: "3: Correct with difficulty", cls: "review-button review-button-correct-difficulty" }); // eslint-disable-line obsidianmd/ui/sentence-case
const correctDifficultyButton = buttonsContainer.createEl("button", { text: "3: Correct with difficulty", cls: "review-button review-button-correct-difficulty" });
correctDifficultyButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.CorrectWithDifficulty));
const correctHesitationButton = buttonsContainer.createEl("button", { text: "4: Correct with hesitation", cls: "review-button review-button-correct-hesitation" }); // eslint-disable-line obsidianmd/ui/sentence-case
const correctHesitationButton = buttonsContainer.createEl("button", { text: "4: Correct with hesitation", cls: "review-button review-button-correct-hesitation" });
correctHesitationButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.CorrectWithHesitation));
const perfectRecallButton = buttonsContainer.createEl("button", { text: "5: Perfect recall", cls: "review-button review-button-perfect-recall" }); // eslint-disable-line obsidianmd/ui/sentence-case
const perfectRecallButton = buttonsContainer.createEl("button", { text: "5: Perfect recall", cls: "review-button review-button-perfect-recall" });
perfectRecallButton.addEventListener("click", () => void this.recordManualResult(note.path, ReviewResponse.PerfectRecall));
const skipButton = buttonsContainer.createEl("button", { text: "Skip", cls: "review-button review-button-skip" });
skipButton.addEventListener("click", () => {
@ -322,7 +322,7 @@ export class BatchReviewModal extends Modal {
showSummary(): void {
const { contentEl } = this;
contentEl.empty();
new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header'); // eslint-disable-line obsidianmd/ui/sentence-case
new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header');
const statsEl = contentEl.createDiv("batch-review-summary-stats");
const totalNotes = this.results.length;
const successfulNotes = this.results.filter(r => r.success).length;

View file

@ -101,7 +101,7 @@ export class CalendarView {
private ensureCalendarBaseStructure(): void {
if (!this.containerEl) return;
let calendarContainer = this.containerEl.querySelector(".calendar-container") as HTMLElement;
let calendarContainer = this.containerEl.children[1].querySelector<HTMLElement>(".calendar-container");
if (!calendarContainer) {
calendarContainer = this.containerEl.createDiv("calendar-container");
}

View file

@ -213,7 +213,7 @@ export class ConsolidatedMCQModal extends Modal {
// Verify we have a valid question
if (!question || !question.choices || question.choices.length < 2) {
new Notice('Error: Invalid question data. Moving to next question.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Error: Invalid question data. Moving to next question.');
// Skip to next question
this.currentQuestionIndex++;
@ -522,7 +522,7 @@ export class ConsolidatedMCQModal extends Modal {
contentEl.empty();
// Display results header with stylized heading
new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header'); // eslint-disable-line obsidianmd/ui/sentence-case
new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header');
// Display overall score with enhanced styling
const totalCorrectOverall = this.answers.filter(a => a.correct && a.attempts <= 1).length;
@ -538,16 +538,16 @@ export class ConsolidatedMCQModal extends Modal {
const performanceIndicator = scoreEl.createDiv('mcq-performance-indicator');
if (scorePercentOverall >= 90) {
performanceIndicator.setText('🎓 Excellent performance!'); // eslint-disable-line obsidianmd/ui/sentence-case
performanceIndicator.setText('🎓 Excellent performance!');
performanceIndicator.addClass('excellent');
} else if (scorePercentOverall >= 70) {
performanceIndicator.setText('👍 Good work!'); // eslint-disable-line obsidianmd/ui/sentence-case
performanceIndicator.setText('👍 Good work!');
performanceIndicator.addClass('good');
} else if (scorePercentOverall >= 50) {
performanceIndicator.setText('🔄 Keep practicing'); // eslint-disable-line obsidianmd/ui/sentence-case
performanceIndicator.setText('🔄 Keep practicing');
performanceIndicator.addClass('needs-improvement');
} else {
performanceIndicator.setText('📚 More review recommended'); // eslint-disable-line obsidianmd/ui/sentence-case
performanceIndicator.setText('📚 More review recommended');
performanceIndicator.addClass('review-recommended');
}

View file

@ -246,7 +246,7 @@ export class EventModal extends Modal {
* Validate form and enable/disable save button
*/
private validateForm(): void {
const saveButton = this.contentEl.querySelector(".event-modal-btn-primary") as HTMLButtonElement;
const saveButton = this.contentEl.querySelector<HTMLButtonElement>(".event-modal-btn-primary");
const isValid = this.titleInput.value.trim() !== "" && this.dateInput.value !== "";
if (saveButton) {

View file

@ -75,7 +75,7 @@ export class MCQModal extends Modal {
new Notice('Could not find note file to regenerate MCQs.');
}
} else {
new Notice('MCQ generation service not available.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('MCQ generation service not available.');
}
})();
});
@ -205,7 +205,7 @@ export class MCQModal extends Modal {
}
const question = this.mcqSet.questions[questionIndex];
if (!question || !question.choices || question.choices.length < 2) {
new Notice('Error: Invalid question data. Moving to next question.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Error: Invalid question data. Moving to next question.');
this.session.currentQuestionIndex++;
if (this.session.currentQuestionIndex < this.mcqSet.questions.length) {
this.displayCurrentQuestion(containerEl);
@ -430,7 +430,7 @@ export class MCQModal extends Modal {
resultItem.createDiv({ cls: 'mcq-result-time', text: `Time: ${Math.round(answer.timeToAnswer)} seconds` });
// FSRS/SM-2 data per question removed from here
} catch (_error) { /* Error displaying answer result: ${error} */ }
} catch { /* Error displaying answer result: ${error} */ }
});
}
const closeBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' });
@ -441,9 +441,9 @@ export class MCQModal extends Modal {
}
this.close();
});
} catch (_error) {
} catch {
new Setting(contentEl).setName('Error completing session').setHeading();
contentEl.createEl('p', { text: 'There was an error completing the MCQ session. Please try again.' }); // eslint-disable-line obsidianmd/ui/sentence-case
contentEl.createEl('p', { text: 'There was an error completing the MCQ session. Please try again.' });
const errorCloseBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' });
errorCloseBtn.addEventListener('click', () => this.close());
}
@ -514,7 +514,7 @@ function getSm2RatingText(rating: number): string {
case 1: return "Incorrect response";
case 2: return "Incorrect but familiar";
case 3: return "Correct with difficulty";
case 4: return "Correct with hHesitation";
case 4: return "Correct with hesitation";
case 5: return "Perfect recall";
default: return "Unknown";
}

View file

@ -79,7 +79,7 @@ export class ReviewModal extends Modal {
const mcqButton = buttonsContainer.createEl("button", { cls: "review-button review-button-mcq" });
const mcqIconSpan = mcqButton.createSpan("mcq-button-icon"); setIcon(mcqIconSpan, "mcq-quiz");
const textSpan = mcqButton.createSpan("mcq-button-text"); textSpan.setText("Test with MCQs"); // eslint-disable-line obsidianmd/ui/sentence-case
const textSpan = mcqButton.createSpan("mcq-button-text"); textSpan.setText("Test with MCQs");
mcqButton.addEventListener("click", () => {
const mcqController = this.plugin.mcqController;
@ -100,7 +100,7 @@ export class ReviewModal extends Modal {
void initializedMcqController.startMCQReview(this.path);
this.close();
} else {
new Notice("MCQ feature could not be initialized. Please check settings."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("MCQ feature could not be initialized. Please check settings.");
}
}
});
@ -110,7 +110,7 @@ export class ReviewModal extends Modal {
if (mcqController && this.plugin.mcqService.getMCQSetForNote(this.path)) {
const refreshMcqButton = buttonsContainer.createEl("button", { cls: "review-button review-button-mcq-refresh" });
const refreshIconSpan = refreshMcqButton.createSpan("mcq-button-icon"); setIcon(refreshIconSpan, "refresh-cw");
const refreshTextSpan = refreshMcqButton.createSpan("mcq-button-text"); refreshTextSpan.setText("Generate new MCQs"); // eslint-disable-line obsidianmd/ui/sentence-case
const refreshTextSpan = refreshMcqButton.createSpan("mcq-button-text"); refreshTextSpan.setText("Generate new MCQs");
refreshMcqButton.addEventListener("click", () => {
if (mcqController) {
@ -150,13 +150,13 @@ export class ReviewModal extends Modal {
if (schedule.lastReviewDate) infoText.createEl("p", { text: `Last reviewed: ${new Date(schedule.lastReviewDate).toLocaleDateString()}` });
if (schedule.schedulingAlgorithm === 'fsrs' && schedule.fsrsData) {
infoText.createEl("p", { text: `Algorithm: FSRS`, cls: "review-phase-fsrs" }); // eslint-disable-line obsidianmd/ui/sentence-case
infoText.createEl("p", { text: `Algorithm: FSRS`, cls: "review-phase-fsrs" });
infoText.createEl("p", { text: `Stability: ${schedule.fsrsData.stability.toFixed(2)}` });
infoText.createEl("p", { text: `Difficulty: ${schedule.fsrsData.difficulty.toFixed(2)}` });
infoText.createEl("p", { text: `State: ${FsrsState[schedule.fsrsData.state]}` }); // Display FSRS state name
infoText.createEl("p", { text: `Interval: ${schedule.fsrsData.scheduled_days} days` });
} else { // SM-2 or fallback
infoText.createEl("p", { text: `Algorithm: SM-2`, cls: "review-phase-sm2" }); // eslint-disable-line obsidianmd/ui/sentence-case
infoText.createEl("p", { text: `Algorithm: SM-2`, cls: "review-phase-sm2" });
let phaseText: string; let phaseClass: string;
if (schedule.scheduleCategory === 'initial') {
const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length;

View file

@ -265,7 +265,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.display(); // Refresh settings UI
void this.plugin.getSidebarView()?.refresh();
} catch (_error) {
} catch {
new Notice('Failed to clear review data. Check console for details.');
}
}
@ -301,7 +301,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.display(); // Refresh settings UI
void this.plugin.getSidebarView()?.refresh();
} catch (_error) {
} catch {
new Notice('Failed to clear calendar events. Check console for details.');
}
}
@ -360,7 +360,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.display(); // Refresh settings UI
void this.plugin.getSidebarView()?.refresh();
} catch (_error) {
} catch {
new Notice('Failed to clear all data. Check console for details.');
}
}
@ -383,8 +383,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('Default scheduling algorithm')
.setDesc('Choose the default algorithm for newly created notes.')
.addDropdown(dropdown => dropdown
.addOption('sm2', 'SM-2') // eslint-disable-line obsidianmd/ui/sentence-case
.addOption('fsrs', 'FSRS') // eslint-disable-line obsidianmd/ui/sentence-case
.addOption('sm2', 'SM-2')
.addOption('fsrs', 'FSRS')
.setValue(this.plugin.settings.defaultSchedulingAlgorithm)
.onChange(async (value: 'sm2' | 'fsrs') => {
this.plugin.settings.defaultSchedulingAlgorithm = value;
@ -400,13 +400,13 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const sm2ParamsContainer = spacedRepSection.createEl('details', { cls: 'sf-settings-collapsible-subsection' });
const sm2Summary = sm2ParamsContainer.createEl('summary');
// Changed to h3
sm2Summary.setText('SM-2 parameters'); // eslint-disable-line obsidianmd/ui/sentence-case
sm2Summary.setText('SM-2 parameters');
sm2ParamsContainer.open = false; // Initially closed
new Setting(sm2ParamsContainer)
.setName('SM-2: base ease factor') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Initial ease factor for new SM-2 notes (2.5 is SM-2 default). Higher ease increases interval growth. Value shown is internal format (250 = 2.5).') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('SM-2: base ease factor')
.setDesc('Initial ease factor for new SM-2 notes (2.5 is SM-2 default). Higher ease increases interval growth. Value shown is internal format (250 = 2.5).')
.addSlider(slider => slider
.setLimits(130, 500, 10)
.setValue(this.plugin.settings.baseEase)
@ -417,8 +417,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(sm2ParamsContainer)
.setName('SM-2: use initial learning schedule') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('For new SM-2 notes, use a fixed set of initial intervals before applying the full algorithm.') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('SM-2: use initial learning schedule')
.setDesc('For new SM-2 notes, use a fixed set of initial intervals before applying the full algorithm.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.useInitialSchedule)
.onChange(async (value: boolean) => {
@ -429,8 +429,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
if (this.plugin.settings.useInitialSchedule) {
new Setting(sm2ParamsContainer)
.setName('SM-2: custom initial intervals (days)') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Comma-separated list for initial SM-2 reviews (e.g., 0,1,3,7). Must start with 0.') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('SM-2: custom initial intervals (days)')
.setDesc('Comma-separated list for initial SM-2 reviews (e.g., 0,1,3,7). Must start with 0.')
.addText(text => text
.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(', '))
.onChange(async (value: string) => {
@ -439,15 +439,15 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
this.plugin.settings.initialScheduleCustomIntervals = intervals;
await this.plugin.savePluginData();
} else {
new Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5000); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5000);
text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(', '));
}
}));
}
new Setting(sm2ParamsContainer)
.setName('SM-2: maximum interval (days)') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Longest possible interval between SM-2 reviews.') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('SM-2: maximum interval (days)')
.setDesc('Longest possible interval between SM-2 reviews.')
.addText(text => text
.setValue(this.plugin.settings.maximumInterval.toString())
.onChange(async (value: string) => {
@ -459,8 +459,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(sm2ParamsContainer)
.setName('SM-2: load balancing') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('SM-2: load balancing')
.setDesc('Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.loadBalance)
.onChange(async (value: boolean) => {
@ -472,7 +472,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const fsrsParamsContainer = spacedRepSection.createEl('details', { cls: 'sf-settings-collapsible-subsection' });
const fsrsSummary = fsrsParamsContainer.createEl('summary');
// Changed to h3
fsrsSummary.setText('FSRS parameters'); // eslint-disable-line obsidianmd/ui/sentence-case
fsrsSummary.setText('FSRS parameters');
fsrsParamsContainer.open = false; // Initially closed
new Setting(fsrsParamsContainer)
@ -487,13 +487,13 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS request retention must be between 0.7 and 0.99."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("FSRS request retention must be between 0.7 and 0.99.");
}
}));
new Setting(fsrsParamsContainer)
.setName('Maximum interval (days)')
.setDesc('Longest possible interval FSRS will schedule.') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Longest possible interval FSRS will schedule.')
.addText(text => text
.setValue(this.plugin.settings.fsrsParameters?.maximum_interval?.toString() ?? DEFAULT_SETTINGS.fsrsParameters.maximum_interval?.toString() ?? '36500')
.onChange(async (value) => {
@ -503,7 +503,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS maximum interval must be a positive number."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("FSRS maximum interval must be a positive number.");
}
}));
@ -523,12 +523,12 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS learning steps must be valid comma-separated numbers > 0, or empty."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("FSRS learning steps must be valid comma-separated numbers > 0, or empty.");
}
}));
new Setting(fsrsParamsContainer)
.setName('Enable fuzz')
.setDesc('Add slight randomness to FSRS intervals (recommended).') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Add slight randomness to FSRS intervals (recommended).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.fsrsParameters?.enable_fuzz ?? DEFAULT_SETTINGS.fsrsParameters.enable_fuzz ?? false)
.onChange(async (value) => {
@ -538,7 +538,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(fsrsParamsContainer)
.setName('Enable short term scheduling')
.setDesc('Use FSRS short-term memory model (affects initial learning steps).') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Use FSRS short-term memory model (affects initial learning steps).')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.fsrsParameters?.enable_short_term ?? DEFAULT_SETTINGS.fsrsParameters.enable_short_term ?? false)
.onChange(async (value) => {
@ -548,7 +548,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(fsrsParamsContainer)
.setName('Weights (w)')
.setDesc('FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.')
.addTextArea(text => {
text.inputEl.rows = 3;
text.inputEl.addClass('sf-full-width-textarea');
@ -560,7 +560,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
new Notice("FSRS weights must be a comma-separated list of 17 valid numbers."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("FSRS weights must be a comma-separated list of 17 valid numbers.");
}
});
});
@ -573,31 +573,31 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
conversionContainer.open = false; // Initially closed
new Setting(conversionContainer)
.setName('Convert all SM-2 cards to FSRS') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Migrate all existing SM-2 cards to use the FSRS algorithm. This will reset their learning state for FSRS.') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Convert all SM-2 cards to FSRS')
.setDesc('Migrate all existing SM-2 cards to use the FSRS algorithm. This will reset their learning state for FSRS.')
.addButton(button => button
.setButtonText('Convert SM-2 to FSRS') // eslint-disable-line obsidianmd/ui/sentence-case
.setButtonText('Convert SM-2 to FSRS')
.setCta() // Call to action style
.onClick(() => {
new ConfirmationModal(
this.app,
'Convert SM-2 to FSRS', // eslint-disable-line obsidianmd/ui/sentence-case
'Are you sure you want to convert ALL SM-2 cards to FSRS? Their FSRS learning state will be reset. This action cannot be easily undone.', // eslint-disable-line obsidianmd/ui/sentence-case
'Convert SM-2 to FSRS',
'Are you sure you want to convert ALL SM-2 cards to FSRS? Their FSRS learning state will be reset. This action cannot be easily undone.',
async () => {
new Notice('Converting SM-2 cards to FSRS... This may take a moment.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Converting SM-2 cards to FSRS... This may take a moment.');
this.plugin.reviewScheduleService.convertAllSm2ToFsrs();
await this.plugin.savePluginData();
new Notice('All SM-2 cards have been converted to FSRS.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('All SM-2 cards have been converted to FSRS.');
this.display(); // Refresh settings tab
}
).open();
}));
new Setting(conversionContainer)
.setName('Convert all FSRS cards to SM-2') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Migrate all existing FSRS cards to use the SM-2 algorithm. Their SM-2 learning state will be initialized with defaults.') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Convert all FSRS cards to SM-2')
.setDesc('Migrate all existing FSRS cards to use the SM-2 algorithm. Their SM-2 learning state will be initialized with defaults.')
.addButton(button => button
.setButtonText('Convert FSRS to SM-2') // eslint-disable-line obsidianmd/ui/sentence-case
.setButtonText('Convert FSRS to SM-2')
.setCta()
.onClick(() => {
new ConfirmationModal(
@ -605,10 +605,10 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
'Convert FSRS to SM-2',
'Are you sure you want to convert ALL FSRS cards to SM-2? Their SM-2 learning state will be reset to defaults. This action cannot be easily undone.',
async () => {
new Notice('Converting FSRS cards to SM-2... This may take a moment.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('Converting FSRS cards to SM-2... This may take a moment.');
this.plugin.reviewScheduleService.convertAllFsrsToSm2();
await this.plugin.savePluginData();
new Notice('All FSRS cards have been converted to SM-2.'); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice('All FSRS cards have been converted to SM-2.');
this.display(); // Refresh settings tab
}
).open();
@ -674,7 +674,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(interfaceSection)
.setName('Reading speed (WPM)') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Reading speed (WPM)')
.setDesc('Words per minute for estimating review time')
.addSlider(slider => slider
.setLimits(100, 500, 10)
@ -687,7 +687,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
interfaceSection.createEl('div', {
cls: 'sf-setting-explain',
text: 'Average adults read 200-250 WPM for regular content, 100-150 WPM for technical content' // eslint-disable-line obsidianmd/ui/sentence-case
text: 'Average adults read 200-250 WPM for regular content, 100-150 WPM for technical content'
});
new Setting(interfaceSection)
@ -756,7 +756,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const mcqSection = createCollapsible('Multiple choice questions', 'newspaper', false); // Closed by default
new Setting(mcqSection)
.setName('Enable MCQ feature') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Enable MCQ feature')
.setDesc('Use AI-generated multiple-choice questions to test your knowledge')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableMCQ)
@ -878,17 +878,17 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('Ollama API URL')
.setDesc('URL of your running Ollama instance (e.g., http://localhost:11434)')
.addText(text => text
.setPlaceholder('http://localhost:11434') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('http://localhost:11434')
.setValue(this.plugin.settings.ollamaApiUrl)
.onChange(async (value: string) => {
this.plugin.settings.ollamaApiUrl = value;
await this.plugin.savePluginData();
}));
new Setting(mcqSection)
.setName('Ollama model') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Name of the Ollama model to use (e.g., llama3, mistral)') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Ollama model')
.setDesc('Name of the Ollama model to use (e.g., llama3, mistral)')
.addText(text => text
.setPlaceholder('Enter Ollama model name') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter Ollama model name')
.setValue(this.plugin.settings.ollamaModel)
.onChange(async (value: string) => {
this.plugin.settings.ollamaModel = value;
@ -901,9 +901,9 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Gemini API key')
.setDesc('Your Google AI Gemini API key.') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Your Google AI Gemini API key.')
.addText(text => text
.setPlaceholder('Enter your Gemini API key') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter your Gemini API key')
.setValue(this.plugin.settings.geminiApiKey)
.onChange(async (value: string) => {
this.plugin.settings.geminiApiKey = value;
@ -913,7 +913,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('Gemini model')
.setDesc('Model name (e.g., gemini-pro)')
.addText(text => text
.setPlaceholder('Enter Gemini model name') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter Gemini model name')
.setValue(this.plugin.settings.geminiModel)
.onChange(async (value: string) => {
this.plugin.settings.geminiModel = value;
@ -926,9 +926,9 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Claude API key')
.setDesc('Your Anthropic Claude API key.') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Your Anthropic Claude API key.')
.addText(text => text
.setPlaceholder('Enter your Claude API key') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter your Claude API key')
.setValue(this.plugin.settings.claudeApiKey)
.onChange(async (value: string) => {
this.plugin.settings.claudeApiKey = value;
@ -938,7 +938,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('Claude model')
.setDesc('Model name (e.g., claude-3-opus-20240229, claude-3-sonnet-20240229)')
.addText(text => text
.setPlaceholder('Enter Claude model name') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter Claude model name')
.setValue(this.plugin.settings.claudeModel)
.onChange(async (value: string) => {
this.plugin.settings.claudeModel = value;
@ -951,9 +951,9 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setClass("sf-settings-subsection-provider-header");
new Setting(mcqSection)
.setName('Together AI API key')
.setDesc('Your Together AI API key.') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Your Together AI API key.')
.addText(text => text
.setPlaceholder('Enter your Together AI API key') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter your Together AI API key')
.setValue(this.plugin.settings.togetherApiKey)
.onChange(async (value: string) => {
this.plugin.settings.togetherApiKey = value;
@ -963,7 +963,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setName('Together AI model')
.setDesc('Model identifier from Together AI (e.g., meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8)')
.addText(text => text
.setPlaceholder('Enter Together AI model identifier') // eslint-disable-line obsidianmd/ui/sentence-case
.setPlaceholder('Enter Together AI model identifier')
.setValue(this.plugin.settings.togetherModel)
.onChange(async (value: string) => {
this.plugin.settings.togetherModel = value;
@ -1056,7 +1056,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
const promptTypeContainer = mcqFormattingGrid.createEl('div');
new Setting(promptTypeContainer)
.setName('Prompt type')
.setDesc('Format for MCQ generation') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Format for MCQ generation')
.addDropdown(dropdown => dropdown
.addOption('basic', 'Basic')
.addOption('detailed', 'Detailed')
@ -1069,7 +1069,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
// Second item in grid
const difficultyContainer = mcqFormattingGrid.createEl('div');
new Setting(difficultyContainer)
.setName('MCQ difficulty') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('MCQ difficulty')
.setDesc('Complexity level')
.addDropdown(dropdown => dropdown
.addOption(MCQDifficulty.Basic, 'Basic recall')
@ -1177,8 +1177,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
if (this.plugin.settings.enableQuestionRegenerationOnRating) {
new Setting(mcqSection)
.setName('Min SM-2 rating for MCQ regeneration') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('For SM-2: Regenerate MCQs if review rating (0-5) is this value or higher. (0:Blackout, 5:Perfect)') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Min SM-2 rating for MCQ regeneration')
.setDesc('For SM-2: Regenerate MCQs if review rating (0-5) is this value or higher. (0:Blackout, 5:Perfect)')
.addSlider(slider => slider
.setLimits(0, 5, 1)
.setValue(this.plugin.settings.minSm2RatingForQuestionRegeneration)
@ -1189,8 +1189,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
}));
new Setting(mcqSection)
.setName('Min FSRS rating for MCQ regeneration') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('For FSRS: Regenerate MCQs if review rating (1-4) is this value or higher. (1:Again, 4:Easy)') // eslint-disable-line obsidianmd/ui/sentence-case
.setName('Min FSRS rating for MCQ regeneration')
.setDesc('For FSRS: Regenerate MCQs if review rating (1-4) is this value or higher. (1:Again, 4:Easy)')
.addSlider(slider => slider
.setLimits(1, 4, 1) // FSRS ratings are 1-4
.setValue(this.plugin.settings.minFsrsRatingForQuestionRegeneration)
@ -1205,7 +1205,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
// If MCQ is disabled, show a message about enabling it
const mcqDisabledMessage = mcqSection.createEl('div', { cls: 'sf-info-box' });
mcqDisabledMessage.createEl('p', {
text: 'Multiple Choice Questions are currently disabled. Enable it to configure durations and notifications.' // eslint-disable-line obsidianmd/ui/sentence-case
text: 'Multiple choice questions are currently disabled. Enable it to configure durations and notifications.'
});
}
@ -1214,7 +1214,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
new Setting(pomodoroSection)
.setName('Enable pomodoro timer')
.setDesc('Show the Pomodoro timer in the sidebar.') // eslint-disable-line obsidianmd/ui/sentence-case
.setDesc('Show the Pomodoro timer in the sidebar.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.pomodoroEnabled)
.onChange(async (value: boolean) => {
@ -1310,7 +1310,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
} else {
const pomodoroDisabledMessage = pomodoroSection.createEl('div', { cls: 'sf-info-box' });
pomodoroDisabledMessage.createEl('p', {
text: 'Pomodoro Timer is currently disabled. Enable it to configure durations and notifications.' // eslint-disable-line obsidianmd/ui/sentence-case
text: 'Pomodoro timer is currently disabled. Enable it to configure durations and notifications.'
});
}
@ -1331,7 +1331,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
.setDesc(locationDesc)
.addExtraButton(button => button
.setIcon('info')
.setTooltip('Current location of your Spaceforge data') // eslint-disable-line obsidianmd/ui/sentence-case
.setTooltip('Current location of your Spaceforge data')
.onClick(() => {
const message = this.plugin.settings.useCustomDataPath && this.plugin.settings.customDataPath
? `Your data is stored at: ${this.plugin.settings.customDataPath.endsWith('/data.json') ? this.plugin.settings.customDataPath : `${this.plugin.settings.customDataPath}/data.json`}`
@ -1385,7 +1385,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
container.empty(); // Clear previous content
if (algorithm === 'sm2') {
new Setting(container).setName('About the modified SM-2 algorithm').setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case
new Setting(container).setName('About the modified SM-2 algorithm').setHeading();
container.createEl('p', {
text: 'Spaceforge uses a modified version of the SuperMemo SM-2 algorithm (1991) which schedules reviews based on how well you recall information. ' +
'When you rate your recall quality from 0-5, the algorithm adjusts the interval and difficulty (ease factor) accordingly.'
@ -1394,8 +1394,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
text: 'Our implementation includes specific handling for overdue or skipped items to prevent them from accumulating in a backlog:'
});
const sm2List = container.createEl('ul');
sm2List.createEl('li', { text: 'Overdue items: Automatically set to review tomorrow with a quality rating of 0.' }); // eslint-disable-line obsidianmd/ui/sentence-case
sm2List.createEl('li', { text: 'Postponed items: Explicitly moved to tomorrow with a one-step quality penalty.' }); // eslint-disable-line obsidianmd/ui/sentence-case
sm2List.createEl('li', { text: 'Overdue items: Automatically set to review tomorrow with a quality rating of 0.' });
sm2List.createEl('li', { text: 'Postponed items: Explicitly moved to tomorrow with a one-step quality penalty.' });
sm2List.createEl('li', { text: 'Both cases reset the repetition count to 1 and update the ease factor.' });
const ratingsTable = container.createEl('table', { cls: 'sf-ratings-table' }); // Added a class for potential styling
@ -1426,19 +1426,19 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
row4.createEl('td', { text: 'Perfect recall' });
row4.createEl('td', { text: 'Largest increase' });
} else if (algorithm === 'fsrs') {
new Setting(container).setName('About the FSRS algorithm').setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case
new Setting(container).setName('About the FSRS algorithm').setHeading();
container.createEl('p', {
text: 'FSRS (Free Spaced Repetition Scheduler) is a modern, evidence-based algorithm that models memory retention to optimize review schedules. ' +
'It calculates card difficulty and stability dynamically based on your review history and aims for a target retention rate.'
}); // eslint-disable-line obsidianmd/ui/sentence-case
});
container.createEl('p', {
text: 'Key concepts in FSRS:' // eslint-disable-line obsidianmd/ui/sentence-case
text: 'Key concepts in FSRS:'
});
const fsrsList = container.createEl('ul');
fsrsList.createEl('li', { text: 'Difficulty: How hard a card is to remember.' }); // eslint-disable-line obsidianmd/ui/sentence-case
fsrsList.createEl('li', { text: 'Stability: How long a card is likely to be remembered.' }); // eslint-disable-line obsidianmd/ui/sentence-case
fsrsList.createEl('li', { text: 'Retention: The probability of recalling a card at the time of review.' }); // eslint-disable-line obsidianmd/ui/sentence-case
fsrsList.createEl('li', { text: 'Learning Steps: Initial short intervals for new cards (configurable).' }); // eslint-disable-line obsidianmd/ui/sentence-case
fsrsList.createEl('li', { text: 'Difficulty: How hard a card is to remember.' });
fsrsList.createEl('li', { text: 'Stability: How long a card is likely to be remembered.' });
fsrsList.createEl('li', { text: 'Retention: The probability of recalling a card at the time of review.' });
fsrsList.createEl('li', { text: 'Learning steps: Initial short intervals for new cards (configurable).' });
const ratingsTable = container.createEl('table', { cls: 'sf-ratings-table' });
const thead = ratingsTable.createTHead();
@ -1449,22 +1449,22 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
headerRow.createEl('th', { text: 'Effect on stability/difficulty' });
const row1 = tbody.insertRow();
row1.createEl('td', { text: '1 (Again)' }); // eslint-disable-line obsidianmd/ui/sentence-case
row1.createEl('td', { text: '1 (Again)' });
row1.createEl('td', { text: 'Forgot the card' });
row1.createEl('td', { text: 'Decreases stability, may increase difficulty' });
const row2 = tbody.insertRow();
row2.createEl('td', { text: '2 (Hard)' }); // eslint-disable-line obsidianmd/ui/sentence-case
row2.createEl('td', { text: '2 (Hard)' });
row2.createEl('td', { text: 'Recalled with significant difficulty' });
row2.createEl('td', { text: 'Smaller increase in stability' });
const row3 = tbody.insertRow();
row3.createEl('td', { text: '3 (Good)' }); // eslint-disable-line obsidianmd/ui/sentence-case
row3.createEl('td', { text: '3 (Good)' });
row3.createEl('td', { text: 'Recalled correctly' });
row3.createEl('td', { text: 'Standard increase in stability' });
const row4 = tbody.insertRow();
row4.createEl('td', { text: '4 (Easy)' }); // eslint-disable-line obsidianmd/ui/sentence-case
row4.createEl('td', { text: '4 (Easy)' });
row4.createEl('td', { text: 'Recalled easily' });
row4.createEl('td', { text: 'Largest increase in stability' });
}

View file

@ -52,7 +52,7 @@ export class ReviewSidebarView extends ItemView {
}
getViewType(): string { return "spaceforge-review-schedule"; }
getDisplayText(): string { return "Spaceforge Review"; } // eslint-disable-line obsidianmd/ui/sentence-case
getDisplayText(): string { return "Spaceforge review"; }
getIcon(): string { return "calendar-clock"; }
async onOpen(): Promise<void> {
@ -248,7 +248,7 @@ export class ReviewSidebarView extends ItemView {
if (this.listViewRenderer) { // Check if renderer is initialized
await this.listViewRenderer.render(container);
} else {
container.setText("Error: Could not render list view. Renderer not ready."); // eslint-disable-line obsidianmd/ui/sentence-case
container.setText("Error: Could not render list view. Renderer not ready.");
}
}
@ -267,7 +267,7 @@ export class ReviewSidebarView extends ItemView {
if (this.calendarView) { // Should always exist due to ensureBaseStructure
await this.calendarView.render();
} else {
container.setText("Error: Could not render calendar view. CalendarView not ready."); // eslint-disable-line obsidianmd/ui/sentence-case
container.setText("Error: Could not render calendar view. CalendarView not ready.");
return; // Avoid further errors if calendarView is somehow null
}

View file

@ -127,15 +127,15 @@ export class ListViewRenderer {
private _ensureAndUpdateReviewButtonsSection(container: HTMLElement, notesForDisplay: ReviewSchedule[], _selectedNotes: string[]): void {
// --- Pomodoro Section (Always visible when enabled, independent of notes) ---
let pomodoroContainer = container.querySelector(".sidebar-pomodoro-section") as HTMLElement;
let pomodoroContainer = container.querySelector<HTMLElement>(".sidebar-pomodoro-section");
if (!pomodoroContainer) {
pomodoroContainer = container.createDiv("sidebar-pomodoro-section");
}
let pomodoroSectionContainerEl = pomodoroContainer.querySelector(".sidebar-pomodoro-section-container") as HTMLElement;
let pomodoroSectionContainerEl = pomodoroContainer.querySelector<HTMLElement>(".sidebar-pomodoro-section-container");
if (!pomodoroSectionContainerEl) {
// Check for legacy class names and migrate if found
const legacyEl = pomodoroContainer.querySelector(".pomodoro-section-container, .sidebar-pomodoro-button-container") as HTMLElement;
const legacyEl = pomodoroContainer.querySelector<HTMLElement>(".pomodoro-section-container, .sidebar-pomodoro-button-container");
if (legacyEl) {
legacyEl.className = "sidebar-pomodoro-section-container";
pomodoroSectionContainerEl = legacyEl;
@ -161,7 +161,7 @@ export class ListViewRenderer {
}
// --- Review Buttons Section (Only visible when there are notes) ---
let reviewButtonsContainer = container.querySelector(".review-buttons-container") as HTMLElement;
let reviewButtonsContainer = container.querySelector<HTMLElement>(".review-buttons-container");
// Visibility of review buttons should depend on whether there are notes in the current context (notesForDisplay)
if (notesForDisplay.length > 0) {
@ -187,7 +187,7 @@ export class ListViewRenderer {
reviewButtonsContainer.classList.remove('sf-hidden');
// Bulk action buttons
let bulkActionButtons = container.querySelector(".review-bulk-actions") as HTMLElement;
let bulkActionButtons = container.querySelector<HTMLElement>(".review-bulk-actions");
if (!bulkActionButtons) {
bulkActionButtons = container.createDiv("review-bulk-actions");
const reviewSelectedBtn = bulkActionButtons.createEl("button", { text: "Review selected", cls: "review-bulk-button" });
@ -253,13 +253,13 @@ export class ListViewRenderer {
} else if (reviewButtonsContainer) {
reviewButtonsContainer.classList.add('sf-hidden');
const bulkActionButtons = container.querySelector(".review-bulk-actions") as HTMLElement;
const bulkActionButtons = container.querySelector<HTMLElement>(".review-bulk-actions");
if (bulkActionButtons) bulkActionButtons.toggleClass('sf-hidden', true);
}
}
private _ensureAndUpdateAllCaughtUpMessage(container: HTMLElement, dueNotesForStats: ReviewSchedule[], activeListBaseDate: Date | null): void {
let caughtUpEl = container.querySelector(".review-all-caught-up") as HTMLElement;
let caughtUpEl = container.querySelector<HTMLElement>(".review-all-caught-up");
if (dueNotesForStats.length === 0 && !activeListBaseDate) {
if (!caughtUpEl) {
caughtUpEl = container.createDiv("review-all-caught-up");
@ -283,7 +283,7 @@ export class ListViewRenderer {
}
private async _ensureAndUpdateDateSections(container: HTMLElement, sortedDateKeys: string[], groupedNotes: Record<string, ReviewSchedule[]>): Promise<void> {
const existingSectionElements = Array.from(container.querySelectorAll(".review-date-section")) as HTMLElement[];
const existingSectionElements = Array.from(container.querySelectorAll<HTMLElement>(".review-date-section"));
const dataKeysFromData = new Set(sortedDateKeys);
let notesDisplayed = false;
@ -301,8 +301,8 @@ export class ListViewRenderer {
if (!notesForSection || notesForSection.length === 0) continue;
notesDisplayed = true;
let dateSectionEl = container.querySelector(`.review-date-section[data-date-key="${dateStr}"]`) as HTMLElement;
let notesContainerEl: HTMLElement;
let dateSectionEl = container.querySelector<HTMLElement>(`.review-date-section[data-date-key="${dateStr}"]`);
let notesContainerEl: HTMLElement | null;
if (!dateSectionEl) {
dateSectionEl = container.createDiv("review-date-section");
@ -355,7 +355,7 @@ export class ListViewRenderer {
headerRow.createSpan("review-date-time"); // Placeholder for time
notesContainerEl = dateSectionEl.createDiv("review-notes-container");
} else {
notesContainerEl = dateSectionEl.querySelector(".review-notes-container") as HTMLElement;
notesContainerEl = dateSectionEl.querySelector<HTMLElement>(".review-notes-container");
if (!notesContainerEl) { // Should not happen if structure is consistent
notesContainerEl = dateSectionEl.createDiv("review-notes-container");
}
@ -372,9 +372,11 @@ export class ListViewRenderer {
dateSectionEl.addClass("review-date-section-overdue");
}
const headerContainer = dateSectionEl.querySelector(".review-date-header-container") as HTMLElement;
const dateHeading = headerContainer.querySelector("h3") as HTMLElement;
const reviewTimeEl = dateSectionEl.querySelector(".review-date-time") as HTMLElement;
const headerContainer = dateSectionEl.querySelector<HTMLElement>(".review-date-header-container");
const dateHeading = headerContainer?.querySelector<HTMLElement>("h3");
const reviewTimeEl = dateSectionEl.querySelector<HTMLElement>(".review-date-time");
if (!headerContainer || !dateHeading) continue;
let displayHeader = dateStr;
const noteCountText = `${notesForSection.length} ${notesForSection.length === 1 ? 'note' : 'notes'}`;
@ -390,7 +392,7 @@ export class ListViewRenderer {
}
dateHeading.setText(displayHeader);
let overdueBadge = dateHeading.querySelector(".review-overdue-badge") as HTMLElement | null;
let overdueBadge = dateHeading.querySelector<HTMLElement>(".review-overdue-badge");
const todayActualStart = DateUtils.startOfDay(); // Timestamp for actual today's midnight
// Condition for showing overdue badge:
@ -432,13 +434,13 @@ export class ListViewRenderer {
let sectionTime = 0;
for (const note of notesForSection) { sectionTime += await this.plugin.reviewScheduleService.estimateReviewTime(note.path); }
reviewTimeEl.setText(`(${EstimationUtils.formatTime(sectionTime)})`);
if (reviewTimeEl) reviewTimeEl.setText(`(${EstimationUtils.formatTime(sectionTime)})`);
await this._updateOrRenderNoteList(notesContainerEl, notesForSection, dateStr, container);
}
// Message for activeListBaseDate with no notes
let noNotesForDateMsg = container.querySelector(".review-no-notes-for-date") as HTMLElement;
let noNotesForDateMsg = container.querySelector<HTMLElement>(".review-no-notes-for-date");
const activeListBaseDate = this.getActiveListBaseDate();
if (activeListBaseDate && !notesDisplayed) {
if (!noNotesForDateMsg) {
@ -453,7 +455,7 @@ export class ListViewRenderer {
private _ensureAndUpdateActiveSessionSection(container: HTMLElement): void {
const activeSession = this.plugin.reviewSessionService.getActiveSession();
let sessionSection = container.querySelector(".review-session-section") as HTMLElement;
let sessionSection = container.querySelector<HTMLElement>(".review-session-section");
if (activeSession) {
if (!sessionSection) {
@ -473,11 +475,17 @@ export class ListViewRenderer {
});
}
sessionSection.toggleClass('sf-hidden', false);
(sessionSection.querySelector(".review-session-name") as HTMLElement).setText(activeSession.name);
(sessionSection.querySelector(".review-session-progress") as HTMLElement).setText(`Progress: ${activeSession.currentIndex}/${activeSession.hierarchy.traversalOrder.length}`);
const progressBar = sessionSection.querySelector(".review-session-progress-bar") as HTMLElement;
const progressPercent = Math.min(100, Math.round((activeSession.currentIndex / activeSession.hierarchy.traversalOrder.length) * 100));
progressBar.style.width = `${progressPercent}%`;
const nameEl = sessionSection.querySelector<HTMLElement>(".review-session-name");
if (nameEl) nameEl.setText(activeSession.name);
const progressEl = sessionSection.querySelector<HTMLElement>(".review-session-progress");
if (progressEl) progressEl.setText(`Progress: ${activeSession.currentIndex}/${activeSession.hierarchy.traversalOrder.length}`);
const progressBar = sessionSection.querySelector<HTMLElement>(".review-session-progress-bar");
if (progressBar) {
const progressPercent = Math.min(100, Math.round((activeSession.currentIndex / activeSession.hierarchy.traversalOrder.length) * 100));
progressBar.style.width = `${progressPercent}%`;
}
} else if (sessionSection) {
sessionSection.toggleClass('sf-hidden', true);
}
@ -495,7 +503,7 @@ export class ListViewRenderer {
return true;
});
let upcomingSection = container.querySelector(".review-upcoming-section") as HTMLElement;
let upcomingSection = container.querySelector<HTMLElement>(".review-upcoming-section");
if (upcomingKeys.length > 0) {
if (!upcomingSection) {
@ -504,10 +512,10 @@ export class ListViewRenderer {
upcomingSection.createDiv("review-upcoming-list"); // List container
}
upcomingSection.toggleClass('sf-hidden', false);
const upcomingListEl = upcomingSection.querySelector(".review-upcoming-list") as HTMLElement;
const upcomingListEl = upcomingSection.querySelector<HTMLElement>(".review-upcoming-list");
if (!upcomingListEl) return; // Should not happen
const existingDayItemElements = Array.from(upcomingListEl.querySelectorAll(".review-upcoming-day")) as HTMLElement[];
const existingDayItemElements = Array.from(upcomingListEl.querySelectorAll<HTMLElement>(".review-upcoming-day"));
// Remove stale day items
@ -528,7 +536,7 @@ export class ListViewRenderer {
continue;
}
let dayItemEl = upcomingListEl.querySelector(`.review-upcoming-day[data-day-key="${dayKey}"]`) as HTMLElement;
let dayItemEl = upcomingListEl.querySelector<HTMLElement>(`.review-upcoming-day[data-day-key="${dayKey}"]`);
if (!dayItemEl) {
dayItemEl = upcomingListEl.createDiv("review-upcoming-day");
dayItemEl.addClass("clickable");
@ -536,8 +544,8 @@ export class ListViewRenderer {
const daySummary = dayItemEl.createDiv("review-upcoming-day-summary");
daySummary.createEl("span", { cls: "review-upcoming-day-name" }); // Placeholder for name
dayItemEl.addEventListener("click", () => { // Attach listener once
const currentDayKey = dayItemEl.dataset.dayKey;
dayItemEl.addEventListener("click", (e) => { // Attach listener once
const currentDayKey = (e.currentTarget as HTMLElement).dataset.dayKey;
if (!currentDayKey) return;
const expandedUpcomingDayKey = this.getExpandedUpcomingDayKey();
const isCurrentlyExpanded = expandedUpcomingDayKey === currentDayKey;
@ -548,7 +556,7 @@ export class ListViewRenderer {
}
// Update summary
const daySummaryNameEl = dayItemEl.querySelector(".review-upcoming-day-summary .review-upcoming-day-name") as HTMLElement;
const daySummaryNameEl = dayItemEl.querySelector<HTMLElement>(".review-upcoming-day-summary .review-upcoming-day-name");
let upcomingDisplayHeader = dayKey;
if (notesForDay.length > 0) { // Should always be true here
const sampleUpcomingDate = new Date(notesForDay[0].nextReviewDate);
@ -564,7 +572,7 @@ export class ListViewRenderer {
// Handle expanded state
const isExpanded = this.getExpandedUpcomingDayKey() === dayKey;
dayItemEl.classList.toggle("is-expanded", isExpanded);
let notesContainerEl = dayItemEl.querySelector(".review-upcoming-notes-container") as HTMLElement;
let notesContainerEl = dayItemEl.querySelector<HTMLElement>(".review-upcoming-notes-container");
if (isExpanded) {
if (!notesContainerEl) {
@ -591,7 +599,7 @@ export class ListViewRenderer {
dateStr: string,
parentContainerForBulkActions: HTMLElement
): Promise<void> {
const existingNoteElements = Array.from(notesContainer.querySelectorAll('.review-note-item[data-note-path]')) as HTMLElement[];
const existingNoteElements = Array.from(notesContainer.querySelectorAll<HTMLElement>('.review-note-item[data-note-path]'));
const existingNotesMap = new Map(existingNoteElements.map(el => [el.dataset.notePath, el]));
const notesInOrder: HTMLElement[] = [];
@ -677,13 +685,13 @@ export class ListViewRenderer {
*/
private updateBulkActionButtonsVisibility(container: HTMLElement): void {
const selectedNotesPaths = this.getSelectedNotes();
const bulkActionsContainer = container.querySelector('.review-bulk-actions') as HTMLElement;
const bulkActionsContainer = container.querySelector<HTMLElement>('.review-bulk-actions');
if (bulkActionsContainer) {
bulkActionsContainer.toggleClass('sf-hidden', selectedNotesPaths.length <= 1);
// Handle visibility/disabled state of "Advance Selected" button
const advanceSelectedBtn = bulkActionsContainer.querySelector('.review-bulk-advance') as HTMLButtonElement | null;
const advanceSelectedBtn = bulkActionsContainer.querySelector<HTMLButtonElement>('.review-bulk-advance');
if (advanceSelectedBtn) {
if (selectedNotesPaths.length > 1) {
const todayStart = DateUtils.startOfDay(new Date()); // Returns timestamp

View file

@ -38,7 +38,7 @@ export class NoteItemRenderer {
}
// Title
const titleEl = noteEl.querySelector(".review-note-title") as HTMLElement;
const titleEl = noteEl.querySelector<HTMLElement>(".review-note-title");
if (titleEl) {
const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
titleEl.setText(file instanceof TFile ? file.basename : note.path);
@ -48,8 +48,8 @@ export class NoteItemRenderer {
const formattedTime = EstimationUtils.formatTime(estimatedTime);
// Phase and Time
const phaseEl = noteEl.querySelector(".review-note-phase") as HTMLElement;
const timeElOld = noteEl.querySelector(".review-note-time") as HTMLElement; // For non-initial
const phaseEl = noteEl.querySelector<HTMLElement>(".review-note-phase");
const timeElOld = noteEl.querySelector<HTMLElement>(".review-note-time"); // For non-initial
if (timeElOld) timeElOld.remove(); // Remove old time element if it exists from a previous state
if (phaseEl) {
@ -74,8 +74,8 @@ export class NoteItemRenderer {
}
// Drag handle visibility (buttons are static, drag handle might change)
const buttonsEl = noteEl.querySelector(".review-note-buttons") as HTMLElement;
const dragHandleEl = buttonsEl?.querySelector(".review-note-drag-handle") as HTMLElement;
const buttonsEl = noteEl.querySelector<HTMLElement>(".review-note-buttons");
const dragHandleEl = buttonsEl?.querySelector<HTMLElement>(".review-note-drag-handle");
if (dragHandleEl) { // If it exists, update its state or recreate if logic is complex
const isDraggable = (dateStr === 'Due notes' || dateStr === 'Today');
dragHandleEl.classList.toggle("is-disabled", !isDraggable);
@ -88,7 +88,7 @@ export class NoteItemRenderer {
}
// Advance button state
const advanceBtn = noteEl.querySelector(".review-note-advance") as HTMLButtonElement | null;
const advanceBtn = noteEl.querySelector<HTMLButtonElement>(".review-note-advance");
if (advanceBtn) {
const todayStartTs = DateUtils.startOfDay(new Date()); // Returns timestamp
const noteReviewDayStartTs = DateUtils.startOfDay(new Date(note.nextReviewDate)); // Returns timestamp

View file

@ -55,7 +55,7 @@ export class PomodoroUIManager {
// new Notice("Pomodoro durations updated."); // Removed notification
return true;
} else {
new Notice("Invalid Pomodoro durations. Settings not saved. Please enter positive numbers."); // eslint-disable-line obsidianmd/ui/sentence-case
new Notice("Invalid Pomodoro durations. Settings not saved. Please enter positive numbers.");
// Re-populate with current valid settings to prevent saving invalid on next close if not corrected
if (this.pomodoroQuickWorkInput) this.pomodoroQuickWorkInput.value = String(this.plugin.settings.pomodoroWorkDuration);
if (this.pomodoroQuickShortInput) this.pomodoroQuickShortInput.value = String(this.plugin.settings.pomodoroShortBreakDuration);
@ -339,7 +339,7 @@ export class PomodoroUIManager {
this.pomodoroUserOverrideHoursInput.value = String(this.plugin.pluginState.pomodoroUserOverrideHours);
const hoursLabel = overrideInputsContainer.createSpan("pomodoro-override-label-small");
hoursLabel.setText("h"); // eslint-disable-line obsidianmd/ui/sentence-case
hoursLabel.setText("h");
this.pomodoroUserOverrideMinutesInput = overrideInputsContainer.createEl("input", { type: "number", cls: "pomodoro-override-minutes" });
this.pomodoroUserOverrideMinutesInput.setAttr("min", "0");
@ -347,7 +347,7 @@ export class PomodoroUIManager {
this.pomodoroUserOverrideMinutesInput.value = String(this.plugin.pluginState.pomodoroUserOverrideMinutes);
const minutesLabel = overrideInputsContainer.createSpan("pomodoro-override-label-small");
minutesLabel.setText("m"); // eslint-disable-line obsidianmd/ui/sentence-case
minutesLabel.setText("m");
// Add to estimation toggle
const toggleContainer = overrideContainer.createDiv("pomodoro-override-toggle-container");

View file

@ -1,11 +1,13 @@
/**
* Simple event emitter implementation
*/
export class EventEmitter {
export type EventMap = Record<string, any[]>;
export class EventEmitter<Events extends { [K in keyof Events]: any[] } = Record<string, any[]>> {
/**
* Event listeners by event name
*/
private listeners: Record<string, ((...args: any[]) => void)[]> = {};
private listeners: { [K in keyof Events]?: ((...args: Events[K]) => void)[] } = {};
/**
* Register a listener for an event
@ -13,12 +15,13 @@ export class EventEmitter {
* @param event Event name
* @param callback Function to call when event is emitted
*/
on(event: string, callback: (...args: any[]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
on<K extends keyof Events>(event: K, callback: (...args: Events[K]) => void): void {
const key = event as string;
if (!this.listeners[key as keyof Events]) {
this.listeners[key as keyof Events] = [];
}
this.listeners[event].push(callback);
this.listeners[key as keyof Events]!.push(callback);
}
/**
@ -27,12 +30,13 @@ export class EventEmitter {
* @param event Event name
* @param args Arguments to pass to listeners
*/
emit(event: string, ...args: unknown[]): void {
if (!this.listeners[event]) {
emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
const key = event as string;
if (!this.listeners[key as keyof Events]) {
return;
}
for (const callback of this.listeners[event]) {
for (const callback of this.listeners[key as keyof Events]!) {
callback(...args);
}
}
@ -43,12 +47,13 @@ export class EventEmitter {
* @param event Event name
* @param callback Function to remove
*/
off(event: string, callback: (...args: any[]) => void): void {
if (!this.listeners[event]) {
off<K extends keyof Events>(event: K, callback: (...args: Events[K]) => void): void {
const key = event as string;
if (!this.listeners[key as keyof Events]) {
return;
}
this.listeners[event] = this.listeners[event].filter(cb => cb !== callback);
this.listeners[key as keyof Events] = this.listeners[key as keyof Events]!.filter(cb => cb !== callback);
}
/**
@ -56,9 +61,9 @@ export class EventEmitter {
*
* @param event Event name
*/
removeAllListeners(event?: string): void {
removeAllListeners<K extends keyof Events>(event?: K): void {
if (event) {
delete this.listeners[event];
delete this.listeners[event as keyof Events];
} else {
this.listeners = {};
}

View file

@ -161,7 +161,7 @@ export class LinkAnalyzer {
}
}
}
} catch (_error) { /* handle error */ }
} catch { /* handle error */ }
}
// Find the node with the most outgoing links as the starting point
@ -506,6 +506,6 @@ export class LinkAnalyzer {
return resolvedLinks;
} catch (_error) { /* handle error */ return []; }
} catch { /* handle error */ return []; }
}
}