diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..b10a8ee --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + plugins: ['@typescript-eslint'], + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:obsidianmd/recommended', + ], + rules: { + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-function': 'off', + 'no-unused-vars': 'off', + 'no-prototype-builtins': 'off', + '@typescript-eslint/ban-types': 'off' + }, + env: { + node: true, + browser: true, + es6: true + } +}; diff --git a/api/claude-service.ts b/api/claude-service.ts index e5f516a..c5639a8 100644 --- a/api/claude-service.ts +++ b/api/claude-service.ts @@ -2,7 +2,7 @@ import { Notice, requestUrl } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQQuestion, MCQSet } from '../models/mcq'; import { IMCQGenerationService } from './mcq-generation-service'; -import { SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings'; // Import MCQQuestionAmountMode +import { SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; // Import MCQQuestionAmountMode export class ClaudeService implements IMCQGenerationService { plugin: SpaceforgePlugin; @@ -13,15 +13,18 @@ export class ClaudeService implements IMCQGenerationService { async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise { if (!settings.claudeApiKey) { - new Notice('Claude API key is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Claude API key is not set, please add it in the Spaceforge settings'); return null; } if (!settings.claudeModel) { - new Notice('Claude Model is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + 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 @@ -38,7 +41,8 @@ export class ClaudeService implements IMCQGenerationService { const questions = this.parseResponse(response, settings, numQuestionsToGenerate); if (questions.length === 0) { - new Notice('Failed to generate valid MCQs from Claude. Please try again.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate valid MCQs from Claude, please try again'); return null; } @@ -47,8 +51,9 @@ export class ClaudeService implements IMCQGenerationService { questions, generatedAt: Date.now() }; - } catch (error) { - new Notice('Failed to generate MCQs with Claude. Please check console for details.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate MCQs with Claude, please check console for details'); return null; } } @@ -66,7 +71,7 @@ export class ClaudeService implements IMCQGenerationService { basePrompt = `Generate ${questionCount} multiple-choice questions that test understanding of key concepts in the following note. Each question should have ${choiceCount} choices, with only one correct answer. Format the output as a numbered list of questions with lettered choices (A, B, C, etc.). Mark the correct answer by putting [CORRECT] at the end of the line.\n\nFor example:\n1. What is the capital of France?\n A) London\n B) Berlin\n C) Paris [CORRECT]\n D) Madrid\n E) Rome`; } - if (difficulty === 'basic') { + if (difficulty === MCQDifficulty.Basic) { basePrompt += `\n\nCreate straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`; } else { basePrompt += `\n\nCreate challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`; @@ -78,9 +83,9 @@ export class ClaudeService implements IMCQGenerationService { const apiKey = settings.claudeApiKey; const model = settings.claudeModel; const difficulty = settings.mcqDifficulty; - - const systemPrompt = difficulty === 'basic' - ? settings.mcqBasicSystemPrompt + + const systemPrompt = difficulty === MCQDifficulty.Basic + ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt; try { @@ -124,12 +129,12 @@ export class ClaudeService implements IMCQGenerationService { // Claude might not start with "1. ", so we adjust the split logic if needed. // The current logic tries to split by number then newline, which might be robust enough. let questionBlocks: string[] = response.split(/\n\d+\.\s+/).filter(block => block.trim().length > 0); - + // If the first block doesn't start with a number, prepend "1. " to it if it's not empty if (questionBlocks.length > 0 && !/^\d+\.\s+/.test(response.trimStart())) { - // If the original response starts with a question directly (no "1. ") + // If the original response starts with a question directly (no "1. ") if (!/^\d+\.\s+/.test(questionBlocks[0])) { - // Check if the first block is the start of the first question + // Check if the first block is the start of the first question if (response.trimStart().length > 0 && questionBlocks.length === 1 && !response.includes("\n1.")) { // This means the entire response is treated as a single block without "1." // We might need a different splitting strategy or assume the first block is the first question @@ -149,7 +154,7 @@ export class ClaudeService implements IMCQGenerationService { } } } - // If initial split by "\n\d+." fails or gives few blocks, try splitting by "\d+." then handling newlines + // If initial split by "\n\d+." fails or gives few blocks, try splitting by "\d+." then handling newlines if (questionBlocks.length === 0 || (questionBlocks.length < settings.mcqQuestionsPerNote / 2 && response.includes("1."))) { const lines = response.split('\n'); let currentQuestionBlock = ''; @@ -162,7 +167,7 @@ export class ClaudeService implements IMCQGenerationService { currentQuestionBlock = line + '\n'; } else if (currentQuestionBlock.length > 0) { // only add to current block if it has started currentQuestionBlock += line + '\n'; - } else if (tempBlocks.length === 0 && line.trim().length > 0) { + } else if (tempBlocks.length === 0 && line.trim().length > 0) { // Handle case where the first question doesn't start with "1." but is the first content currentQuestionBlock = line + '\n'; } @@ -191,8 +196,8 @@ export class ClaudeService implements IMCQGenerationService { const isCorrect = line.includes('[CORRECT]'); // More robustly remove common choice markers (A., A), 1., 1), -, *) const cleanedLine = line.replace(/\[CORRECT\]/gi, '') // Case-insensitive removal - .replace(/^([A-Z]\.|[A-Z]\)|\d+\.|\d+\)|-\s*|\*\s*)/, '') - .trim(); + .replace(/^([A-Z]\.|[A-Z]\)|\d+\.|\d+\)|-\s*|\*\s*)/, '') + .trim(); if (cleanedLine.length > 0) { // Only add non-empty choices choices.push(cleanedLine); if (isCorrect) correctAnswerIndex = choices.length - 1; @@ -209,20 +214,21 @@ export class ClaudeService implements IMCQGenerationService { break; } } - // If still not found, default to the first choice as per original logic + // If still not found, default to the first choice as per original logic if (correctAnswerIndex === -1) correctAnswerIndex = 0; } - if (questionText && choices.length >= settings.mcqChoicesPerQuestion -1 && choices.length > 0) { // Allow slightly fewer choices if parsing is tricky + if (questionText && choices.length >= settings.mcqChoicesPerQuestion - 1 && choices.length > 0) { // Allow slightly fewer choices if parsing is tricky questions.push({ question: questionText, choices, correctAnswerIndex }); } else if (questionText && choices.length >= 2) { // Absolute minimum of 2 choices - questions.push({ question: questionText, choices, correctAnswerIndex }); + questions.push({ question: questionText, choices, correctAnswerIndex }); } } return questions.slice(0, numQuestionsToGenerate); // Use calculated number - } catch (error) { - new Notice('Error parsing MCQ response from Claude. Please try again.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Error parsing MCQ response from Claude, please try again'); return []; } } diff --git a/api/gemini-service.ts b/api/gemini-service.ts index e045eec..eb4365c 100644 --- a/api/gemini-service.ts +++ b/api/gemini-service.ts @@ -2,12 +2,9 @@ import { Notice, requestUrl } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQQuestion, MCQSet } from '../models/mcq'; import { IMCQGenerationService } from './mcq-generation-service'; -import { SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings'; // Import MCQQuestionAmountMode +import { SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; // Import MCQQuestionAmountMode + -// Define a simple type for Gemini API parts, as it expects an array of these. -interface GeminiPart { - text: string; -} export class GeminiService implements IMCQGenerationService { plugin: SpaceforgePlugin; @@ -18,15 +15,18 @@ export class GeminiService implements IMCQGenerationService { async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise { if (!settings.geminiApiKey) { - new Notice('Gemini API key is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Gemini API key is not set, please add it in the Spaceforge settings'); return null; } if (!settings.geminiModel) { - new Notice('Gemini Model is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + 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,7 +43,8 @@ export class GeminiService implements IMCQGenerationService { const questions = this.parseResponse(response, settings, numQuestionsToGenerate); if (questions.length === 0) { - new Notice('Failed to generate valid MCQs from Gemini. Please try again.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate valid MCQs from Gemini, please try again'); return null; } @@ -52,8 +53,9 @@ export class GeminiService implements IMCQGenerationService { questions, generatedAt: Date.now() }; - } catch (error) { - new Notice('Failed to generate MCQs with Gemini. Please check console for details.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate MCQs with Gemini, please check console for details'); return null; } } @@ -67,8 +69,8 @@ export class GeminiService implements IMCQGenerationService { let basePrompt = ""; // Gemini prefers direct instructions. System prompts are handled differently or not at all in simpler SDKs. // For direct API calls, we include system-like instructions at the beginning of the user prompt. - const systemInstruction = difficulty === 'basic' - ? settings.mcqBasicSystemPrompt + const systemInstruction = difficulty === MCQDifficulty.Basic + ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt; if (promptType === 'basic') { @@ -76,7 +78,7 @@ export class GeminiService implements IMCQGenerationService { } else { basePrompt = `${systemInstruction}\n\nGenerate ${questionCount} multiple-choice questions that test understanding of key concepts in the following note. Each question should have ${choiceCount} choices, with only one correct answer. Format the output as a numbered list of questions with lettered choices (A, B, C, etc.). Mark the correct answer by putting [CORRECT] at the end of the line.\n\nFor example:\n1. What is the capital of France?\n A) London\n B) Berlin\n C) Paris [CORRECT]\n D) Madrid\n E) Rome`; } - + // Difficulty instructions are already prepended via systemInstruction. return `${basePrompt}\n\nNote Content:\n${noteContent}`; } @@ -126,7 +128,7 @@ export class GeminiService implements IMCQGenerationService { private parseResponse(response: string, settings: SpaceforgeSettings, numQuestionsToGenerate: number): MCQQuestion[] { const questions: MCQQuestion[] = []; try { - let questionBlocks: string[] = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); + const questionBlocks: string[] = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); if (questionBlocks.length === 0) { const lines = response.split('\n'); @@ -161,9 +163,9 @@ export class GeminiService implements IMCQGenerationService { if (isCorrect) correctAnswerIndex = choices.length - 1; } - if (correctAnswerIndex === -1) { + if (correctAnswerIndex === -1) { for (let i = 0; i < choices.length; i++) { - if (lines[i+1] && (lines[i+1].toLowerCase().includes('correct') || lines[i+1].includes('✓') || lines[i+1].includes('✔️'))) { + if (lines[i + 1] && (lines[i + 1].toLowerCase().includes('correct') || lines[i + 1].includes('✓') || lines[i + 1].includes('✔️'))) { correctAnswerIndex = i; break; } @@ -176,8 +178,9 @@ export class GeminiService implements IMCQGenerationService { } } return questions.slice(0, numQuestionsToGenerate); // Use calculated number - } catch (error) { - new Notice('Error parsing MCQ response from Gemini. Please try again.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Error parsing MCQ response from Gemini, please try again'); return []; } } diff --git a/api/ollama-service.ts b/api/ollama-service.ts index 0acb471..d65d83d 100644 --- a/api/ollama-service.ts +++ b/api/ollama-service.ts @@ -2,7 +2,7 @@ import { Notice, requestUrl } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQQuestion, MCQSet } from '../models/mcq'; import { IMCQGenerationService } from './mcq-generation-service'; -import { SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings'; // Import MCQQuestionAmountMode +import { SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; // Import MCQQuestionAmountMode export class OllamaService implements IMCQGenerationService { plugin: SpaceforgePlugin; @@ -13,15 +13,18 @@ export class OllamaService implements IMCQGenerationService { async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise { if (!settings.ollamaApiUrl) { - new Notice('Ollama API URL is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Ollama API URL is not set, please add it in the Spaceforge settings'); return null; } if (!settings.ollamaModel) { - new Notice('Ollama Model is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + 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 @@ -38,7 +41,8 @@ export class OllamaService implements IMCQGenerationService { const questions = this.parseResponse(response, settings, numQuestionsToGenerate); if (questions.length === 0) { - new Notice('Failed to generate valid MCQs from Ollama. Please try again.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate valid MCQs from Ollama, please try again'); return null; } @@ -47,8 +51,9 @@ export class OllamaService implements IMCQGenerationService { questions, generatedAt: Date.now() }; - } catch (error) { - new Notice('Failed to generate MCQs with Ollama. Please check console for details.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate MCQs with Ollama, please check console for details'); return null; } } @@ -66,7 +71,7 @@ export class OllamaService implements IMCQGenerationService { basePrompt = `Generate ${questionCount} multiple-choice questions that test understanding of key concepts in the following note. Each question should have ${choiceCount} choices, with only one correct answer. Format the output as a numbered list of questions with lettered choices (A, B, C, etc.). Mark the correct answer by putting [CORRECT] at the end of the line.\n\nFor example:\n1. What is the capital of France?\n A) London\n B) Berlin\n C) Paris [CORRECT]\n D) Madrid\n E) Rome`; } - if (difficulty === 'basic') { + if (difficulty === MCQDifficulty.Basic) { basePrompt += `\n\nCreate straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`; } else { basePrompt += `\n\nCreate challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`; @@ -78,9 +83,9 @@ export class OllamaService implements IMCQGenerationService { const apiUrl = settings.ollamaApiUrl.endsWith('/') ? settings.ollamaApiUrl.slice(0, -1) : settings.ollamaApiUrl; const model = settings.ollamaModel; const difficulty = settings.mcqDifficulty; - - const systemPrompt = difficulty === 'basic' - ? settings.mcqBasicSystemPrompt + + const systemPrompt = difficulty === MCQDifficulty.Basic + ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt; try { @@ -120,7 +125,7 @@ export class OllamaService implements IMCQGenerationService { private parseResponse(response: string, settings: SpaceforgeSettings, numQuestionsToGenerate: number): MCQQuestion[] { const questions: MCQQuestion[] = []; try { - let questionBlocks: string[] = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); + const questionBlocks: string[] = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); if (questionBlocks.length === 0) { const lines = response.split('\n'); @@ -155,9 +160,9 @@ export class OllamaService implements IMCQGenerationService { if (isCorrect) correctAnswerIndex = choices.length - 1; } - if (correctAnswerIndex === -1) { + if (correctAnswerIndex === -1) { for (let i = 0; i < choices.length; i++) { - if (lines[i+1] && (lines[i+1].toLowerCase().includes('correct') || lines[i+1].includes('✓') || lines[i+1].includes('✔️'))) { + if (lines[i + 1] && (lines[i + 1].toLowerCase().includes('correct') || lines[i + 1].includes('✓') || lines[i + 1].includes('✔️'))) { correctAnswerIndex = i; break; } @@ -170,8 +175,9 @@ export class OllamaService implements IMCQGenerationService { } } return questions.slice(0, numQuestionsToGenerate); // Use calculated number - } catch (error) { - new Notice('Error parsing MCQ response from Ollama. Please try again.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Error parsing MCQ response from Ollama, please try again'); return []; } } diff --git a/api/openai-service.ts b/api/openai-service.ts index c697fcc..d1b5622 100644 --- a/api/openai-service.ts +++ b/api/openai-service.ts @@ -2,7 +2,7 @@ import { Notice, requestUrl } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQQuestion, MCQSet } from '../models/mcq'; import { IMCQGenerationService } from './mcq-generation-service'; -import { SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings'; // Import MCQQuestionAmountMode +import { SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; // Import MCQQuestionAmountMode export class OpenAIService implements IMCQGenerationService { plugin: SpaceforgePlugin; @@ -13,11 +13,13 @@ export class OpenAIService implements IMCQGenerationService { async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise { if (!settings.openaiApiKey) { - new Notice('OpenAI API key is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('OpenAI API key is not set, please add it in the Spaceforge settings'); return null; } if (!settings.openaiModel) { - new Notice('OpenAI Model is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('OpenAI model is not set, please add it in the Spaceforge settings'); return null; } @@ -38,7 +40,7 @@ export class OpenAIService implements IMCQGenerationService { const questions = this.parseResponse(response, settings, numQuestionsToGenerate); if (questions.length === 0) { - new Notice('Failed to generate valid MCQs from OpenAI. Please try again.'); + new Notice('Failed to generate valid MCQs from OpenAI, please try again'); return null; } @@ -47,8 +49,8 @@ export class OpenAIService implements IMCQGenerationService { questions, generatedAt: Date.now() }; - } catch (error) { - new Notice('Failed to generate MCQs with OpenAI. Please check console for details.'); + } catch { + new Notice('Failed to generate MCQs with OpenAI, please check console for details'); return null; } } @@ -66,7 +68,7 @@ export class OpenAIService implements IMCQGenerationService { basePrompt = `Generate ${questionCount} multiple-choice questions that test understanding of key concepts in the following note. Each question should have ${choiceCount} choices, with only one correct answer. Format the output as a numbered list of questions with lettered choices (A, B, C, etc.). Mark the correct answer by putting [CORRECT] at the end of the line.\n\nFor example:\n1. What is the capital of France?\n A) London\n B) Berlin\n C) Paris [CORRECT]\n D) Madrid\n E) Rome`; } - if (difficulty === 'basic') { + if (difficulty === MCQDifficulty.Basic) { basePrompt += `\n\nCreate straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`; } else { basePrompt += `\n\nCreate challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`; @@ -78,9 +80,9 @@ export class OpenAIService implements IMCQGenerationService { const apiKey = settings.openaiApiKey; const model = settings.openaiModel; const difficulty = settings.mcqDifficulty; - - const systemPrompt = difficulty === 'basic' - ? settings.mcqBasicSystemPrompt + + const systemPrompt = difficulty === MCQDifficulty.Basic + ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt; try { @@ -119,7 +121,7 @@ export class OpenAIService implements IMCQGenerationService { private parseResponse(response: string, settings: SpaceforgeSettings, numQuestionsToGenerate: number): MCQQuestion[] { const questions: MCQQuestion[] = []; try { - let questionBlocks: string[] = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); + const questionBlocks: string[] = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); if (questionBlocks.length === 0) { const lines = response.split('\n'); @@ -156,7 +158,7 @@ export class OpenAIService implements IMCQGenerationService { if (correctAnswerIndex === -1) { // Fallback if [CORRECT] not found for (let i = 0; i < choices.length; i++) { - if (lines[i+1] && (lines[i+1].toLowerCase().includes('correct') || lines[i+1].includes('✓') || lines[i+1].includes('✔️'))) { + if (lines[i + 1] && (lines[i + 1].toLowerCase().includes('correct') || lines[i + 1].includes('✓') || lines[i + 1].includes('✔️'))) { correctAnswerIndex = i; break; } @@ -169,8 +171,9 @@ export class OpenAIService implements IMCQGenerationService { } } return questions.slice(0, numQuestionsToGenerate); // Use calculated number - } catch (error) { - new Notice('Error parsing MCQ response from OpenAI. Please try again.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Error parsing MCQ response from OpenAI, please try again'); return []; } } diff --git a/api/openrouter-service.ts b/api/openrouter-service.ts index 4eb2277..c5fd445 100644 --- a/api/openrouter-service.ts +++ b/api/openrouter-service.ts @@ -1,8 +1,8 @@ -import { Notice, requestUrl } from 'obsidian'; // Removed TFile as it wasn't used +import { Notice, requestUrl } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQQuestion, MCQSet } from '../models/mcq'; import { IMCQGenerationService } from './mcq-generation-service'; -import { SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings'; // Import MCQQuestionAmountMode +import { SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; // Import MCQQuestionAmountMode /** * Service for generating MCQs using the OpenRouter API @@ -12,7 +12,7 @@ export class OpenRouterService implements IMCQGenerationService { * Reference to the main plugin */ plugin: SpaceforgePlugin; - + /** * Initialize OpenRouter service * @@ -21,7 +21,7 @@ export class OpenRouterService implements IMCQGenerationService { constructor(plugin: SpaceforgePlugin) { this.plugin = plugin; } - + /** * Generate MCQs for a note * @@ -33,12 +33,14 @@ export class OpenRouterService implements IMCQGenerationService { async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise { // Check if API key is set if (!settings.openRouterApiKey) { - new Notice('OpenRouter API key is not set. Please add it in the settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + 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 @@ -46,41 +48,43 @@ export class OpenRouterService implements IMCQGenerationService { if (settings.mcqQuestionAmountMode === MCQQuestionAmountMode.WordsPerQuestion) { const wordCount = noteContent.split(/\s+/).filter(Boolean).length; // Ensure at least 1 question, and use ceiling to round up slightly - numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion)); + numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion)); // Optional: Add a reasonable upper limit if desired, e.g., Math.min(numQuestionsToGenerate, 15); } else { // Fixed mode numQuestionsToGenerate = settings.mcqQuestionsPerNote; } - + // Generate prompt based on settings and calculated question count const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate); - + // Make API request const response = await this.makeApiRequest(prompt, settings); - + // Parse response to extract MCQs, respecting the target number const questions = this.parseResponse(response, settings, numQuestionsToGenerate); - + // Ensure we have at least one question if requested if (questions.length === 0) { - new Notice('Failed to generate valid MCQs from OpenRouter. Please try again.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate valid MCQs from OpenRouter, please try again'); return null; } - + // Create MCQ set const mcqSet: MCQSet = { notePath, questions, generatedAt: Date.now() }; - + return mcqSet; - } catch (error) { - new Notice('Failed to generate MCQs with OpenRouter. Please check console for details.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate MCQs with OpenRouter, please check console for details'); return null; } } - + /** * Generate prompt for the AI * @@ -91,14 +95,14 @@ export class OpenRouterService implements IMCQGenerationService { */ private generatePrompt(noteContent: string, settings: SpaceforgeSettings, numQuestionsToGenerate: number): string { // Use the passed numQuestionsToGenerate instead of settings.mcqQuestionsPerNote directly - const questionCount = numQuestionsToGenerate; + const questionCount = numQuestionsToGenerate; const choiceCount = settings.mcqChoicesPerQuestion; const promptType = settings.mcqPromptType; const difficulty = settings.mcqDifficulty; - + // Base prompt template according to prompt type let basePrompt = ""; - + if (promptType === 'basic') { basePrompt = `Generate ${questionCount} multiple-choice questions based on the following note content. Each question should have ${choiceCount} choices, with one correct answer. Format the output as a list of questions with bullet points for each answer choice. Mark the correct answer by putting [CORRECT] at the end of the line.`; } else { @@ -112,18 +116,18 @@ For example: D) Madrid E) Rome`; } - + // Add difficulty-specific instructions - if (difficulty === 'basic') { + if (difficulty === MCQDifficulty.Basic) { basePrompt += `\n\nCreate straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`; } else { basePrompt += `\n\nCreate challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`; } - + // Add note content return `${basePrompt}\n\nNote Content:\n${noteContent}`; } - + /** * Make API request to OpenRouter * @@ -135,13 +139,13 @@ For example: const apiKey = settings.openRouterApiKey; // Use key from settings argument const model = settings.openRouterModel; // Use model from settings argument const difficulty = settings.mcqDifficulty; - + try { // Get the appropriate system prompt based on difficulty - const systemPrompt = difficulty === 'basic' - ? settings.mcqBasicSystemPrompt + const systemPrompt = difficulty === MCQDifficulty.Basic + ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt; - + const response = await requestUrl({ url: 'https://openrouter.ai/api/v1/chat/completions', method: 'POST', @@ -154,13 +158,13 @@ For example: body: JSON.stringify({ model: model, messages: [ - { - role: 'system', + { + role: 'system', content: systemPrompt }, - { - role: 'user', - content: prompt + { + role: 'user', + content: prompt } ] }) @@ -182,7 +186,7 @@ For example: throw error; } } - + /** * Parse the AI response to extract MCQs * @@ -193,21 +197,21 @@ For example: */ private parseResponse(response: string, settings: SpaceforgeSettings, numQuestionsToGenerate: number): MCQQuestion[] { const questions: MCQQuestion[] = []; - + try { // Log the raw response for debugging // Check for common response formats let questionBlocks: string[] = []; - + // Method 1: Split by numbered questions (1., 2., etc.) questionBlocks = response.split(/\d+\.\s+/).filter(block => block.trim().length > 0); - + // If we didn't find questions, try another method if (questionBlocks.length === 0) { // Method 2: Look for line breaks with numbered patterns const lines = response.split('\n'); let currentQuestion = ''; - + for (const line of lines) { if (/^\d+\./.test(line.trim())) { if (currentQuestion) { @@ -218,65 +222,65 @@ For example: currentQuestion += line + '\n'; } } - + if (currentQuestion) { questionBlocks.push(currentQuestion); } } - + for (const block of questionBlocks) { // Extract the question text and choices const lines = block.split('\n').filter(line => line.trim().length > 0); if (lines.length < 2) { continue; } - + let questionText = lines[0].trim(); // Remove and tags from the question text questionText = questionText.replace(//g, '').replace(/<\/think>/g, ''); const choices: string[] = []; let correctAnswerIndex = -1; - + // Debug the question // Extract choices and identify the correct answer for (let i = 1; i < lines.length; i++) { const line = lines[i].trim(); const isCorrect = line.includes('[CORRECT]'); - + // Remove the [CORRECT] marker and any leading identifiers (A), B., etc.) const cleanedLine = line .replace(/\[CORRECT\]/g, '') .replace(/^[A-Z]\)\s*|^[A-Z]\.\s*|^\w+\)\s*|^\w+\.\s*/, '') .trim(); - + choices.push(cleanedLine); - + if (isCorrect) { correctAnswerIndex = choices.length - 1; } } - + // If no answer was marked as correct, try to check for other indicators if (correctAnswerIndex === -1) { for (let i = 0; i < choices.length; i++) { // Check for any other correct answer indicator - if (lines[i+1] && ( - lines[i+1].toLowerCase().includes('correct') || - lines[i+1].includes('✓') || - lines[i+1].includes('✔️') + if (lines[i + 1] && ( + lines[i + 1].toLowerCase().includes('correct') || + lines[i + 1].includes('✓') || + lines[i + 1].includes('✔️') )) { correctAnswerIndex = i; break; } } } - + // If still no correct answer found, default to the first answer as a fallback if (correctAnswerIndex === -1 && choices.length > 0) { correctAnswerIndex = 0; } - + // Only add if we found a valid question with choices if (questionText && choices.length >= 2) { questions.push({ @@ -285,15 +289,17 @@ For example: correctAnswerIndex }); } else { + // no-op } } - + // Log the parsed questions // Limit to the target number of questions calculated earlier // Use numQuestionsToGenerate instead of settings.mcqQuestionsPerNote - return questions.slice(0, numQuestionsToGenerate); - } catch (error) { - new Notice('Error parsing MCQ response from OpenRouter. Please try again.'); + 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'); return []; } } diff --git a/api/together-service.ts b/api/together-service.ts index 86b13ce..a1d32ee 100644 --- a/api/together-service.ts +++ b/api/together-service.ts @@ -2,7 +2,7 @@ import { Notice, requestUrl } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQQuestion, MCQSet } from '../models/mcq'; import { IMCQGenerationService } from './mcq-generation-service'; -import { SpaceforgeSettings, MCQQuestionAmountMode } from '../models/settings'; // Import MCQQuestionAmountMode +import { SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; // Import MCQQuestionAmountMode export class TogetherService implements IMCQGenerationService { plugin: SpaceforgePlugin; @@ -13,15 +13,18 @@ export class TogetherService implements IMCQGenerationService { async generateMCQs(notePath: string, noteContent: string, settings: SpaceforgeSettings): Promise { if (!settings.togetherApiKey) { - new Notice('Together AI API key is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Together AI API key is not set, please add it in the Spaceforge settings'); return null; } if (!settings.togetherModel) { - new Notice('Together AI Model is not set. Please add it in the Spaceforge settings.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + 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 @@ -38,7 +41,8 @@ export class TogetherService implements IMCQGenerationService { const questions = this.parseResponse(response, settings, numQuestionsToGenerate); if (questions.length === 0) { - new Notice('Failed to generate valid MCQs from Together AI. Please try again.'); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate valid MCQs from Together AI, please try again'); return null; } @@ -47,8 +51,9 @@ export class TogetherService implements IMCQGenerationService { questions, generatedAt: Date.now() }; - } catch (error) { - new Notice('Failed to generate MCQs with Together AI. Please check console for details.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Failed to generate MCQs with Together AI, please check console for details'); return null; } } @@ -66,7 +71,7 @@ export class TogetherService implements IMCQGenerationService { basePrompt = `Generate ${questionCount} multiple-choice questions that test understanding of key concepts in the following note. Each question should have ${choiceCount} choices, with only one correct answer. Format the output as a numbered list of questions with lettered choices (A, B, C, etc.). Mark the correct answer by putting [CORRECT] at the end of the line.\n\nFor example:\n1. What is the capital of France?\n A) London\n B) Berlin\n C) Paris [CORRECT]\n D) Madrid\n E) Rome`; } - if (difficulty === 'basic') { + if (difficulty === MCQDifficulty.Basic) { basePrompt += `\n\nCreate straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`; } else { basePrompt += `\n\nCreate challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`; @@ -78,9 +83,9 @@ export class TogetherService implements IMCQGenerationService { const apiKey = settings.togetherApiKey; const model = settings.togetherModel; const difficulty = settings.mcqDifficulty; - - const systemPrompt = difficulty === 'basic' - ? settings.mcqBasicSystemPrompt + + const systemPrompt = difficulty === MCQDifficulty.Basic + ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt; try { @@ -135,7 +140,7 @@ export class TogetherService implements IMCQGenerationService { } } } - + if (questionBlocks.length === 0 || (questionBlocks.length < settings.mcqQuestionsPerNote / 2 && response.includes("1."))) { const lines = response.split('\n'); let currentQuestionBlock = ''; @@ -173,8 +178,8 @@ export class TogetherService implements IMCQGenerationService { const line = lines[i].trim(); const isCorrect = line.includes('[CORRECT]'); const cleanedLine = line.replace(/\[CORRECT\]/gi, '') - .replace(/^([A-Z]\.|[A-Z]\)|\d+\.|\d+\)|-\s*|\*\s*)/, '') - .trim(); + .replace(/^([A-Z]\.|[A-Z]\)|\d+\.|\d+\)|-\s*|\*\s*)/, '') + .trim(); if (cleanedLine.length > 0) { choices.push(cleanedLine); if (isCorrect) correctAnswerIndex = choices.length - 1; @@ -192,15 +197,16 @@ export class TogetherService implements IMCQGenerationService { if (correctAnswerIndex === -1) correctAnswerIndex = 0; } - if (questionText && choices.length >= settings.mcqChoicesPerQuestion -1 && choices.length > 0) { + if (questionText && choices.length >= settings.mcqChoicesPerQuestion - 1 && choices.length > 0) { questions.push({ question: questionText, choices, correctAnswerIndex }); } else if (questionText && choices.length >= 2) { - questions.push({ question: questionText, choices, correctAnswerIndex }); + questions.push({ question: questionText, choices, correctAnswerIndex }); } } return questions.slice(0, numQuestionsToGenerate); // Use calculated number - } catch (error) { - new Notice('Error parsing MCQ response from Together AI. Please try again.'); + } catch { + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice('Error parsing MCQ response from Together AI, please try again'); return []; } } diff --git a/controllers/interfaces.ts b/controllers/interfaces.ts index 6c20bc6..d3407f8 100644 --- a/controllers/interfaces.ts +++ b/controllers/interfaces.ts @@ -1,5 +1,4 @@ import { ReviewResponse, ReviewSchedule } from '../models/review-schedule'; -import { MCQSet } from '../models/mcq'; /** * Interface for the core review controller @@ -10,27 +9,27 @@ export interface IReviewController { * * @param preserveCurrentIndex Whether to try to preserve the current note index */ - updateTodayNotes(preserveCurrentIndex?: boolean): Promise; - + updateTodayNotes(preserveCurrentIndex?: boolean): void; + /** * Review the current note */ reviewCurrentNote(): Promise; - + /** * Review a note * * @param path Path to the note file */ reviewNote(path: string): Promise; - + /** * Show the review modal for a note * * @param path Path to the note file */ showReviewModal(path: string): void; - + /** * Process a review response * @@ -38,7 +37,7 @@ export interface IReviewController { * @param response User's response during review */ processReviewResponse(path: string, response: ReviewResponse): Promise; - + /** * Skip the review of a note and reschedule for tomorrow with penalty * @@ -53,14 +52,14 @@ export interface IReviewController { * @param days Number of days to postpone (default: 1) */ postponeNote(path: string, days?: number): Promise; - + /** * Handle a note being postponed, updating navigation state * * @param path Path to the postponed note */ handleNotePostponed(path: string): Promise; - + /** * Get the currently loaded notes due for review */ @@ -81,7 +80,7 @@ export interface IReviewController { * Sets an override for the current review date. * @param date Timestamp of the date to simulate, or null to use actual Date.now(). */ - setReviewDateOverride(date: number | null): Promise; + setReviewDateOverride(date: number | null): void; /** * Gets the current review date override. @@ -98,29 +97,29 @@ export interface IReviewNavigationController { * Navigate to the next note following the current order */ navigateToNextNote(): Promise; - + /** * Navigate to the previous note in the current order */ navigateToPreviousNote(): Promise; - + /** * Navigate to the next note without recording a review */ navigateToNextNoteWithoutRating(): Promise; - + /** * Navigate to the current note without showing review modal */ navigateToCurrentNoteWithoutModal(): Promise; - + /** * Open a note without showing the review modal * * @param path Path to the note file */ openNoteWithoutReview(path: string): Promise; - + /** * Swap two notes in the traversal order * @@ -138,27 +137,27 @@ export interface IReviewBatchController { * Start reviewing all of today's notes */ reviewAllTodaysNotes(): Promise; - + /** * Review a specific set of notes * * @param paths Array of note paths to review * @param useMCQ Whether to use MCQs for testing */ - reviewNotes(paths: string[], useMCQ?: boolean): Promise; - + reviewNotes(paths: string[], useMCQ?: boolean): void; + /** * Review all notes with MCQs in a batch * * @param useMCQ Whether to use MCQs for testing */ - reviewAllNotesWithMCQ(useMCQ?: boolean): Promise; - + reviewAllNotesWithMCQ(useMCQ?: boolean): void; + /** * Regenerate MCQs for all notes due today */ regenerateAllMCQs(): Promise; - + /** * Postpone a specific set of notes * @@ -166,7 +165,7 @@ export interface IReviewBatchController { * @param days Number of days to postpone */ postponeNotes(paths: string[], days?: number): Promise; - + /** * Remove a specific set of notes from the review schedule * diff --git a/controllers/review-batch-controller.ts b/controllers/review-batch-controller.ts index 41a1dba..78ab620 100644 --- a/controllers/review-batch-controller.ts +++ b/controllers/review-batch-controller.ts @@ -1,9 +1,7 @@ import { Notice } from 'obsidian'; import SpaceforgePlugin from '../main'; import { IReviewBatchController } from './interfaces'; -import { ReviewSchedule } from '../models/review-schedule'; import { BatchReviewModal } from '../ui/batch-review-modal'; -import { ConsolidatedMCQModal } from '../ui/consolidated-mcq-modal'; /** * Controller for batch reviewing multiple notes @@ -31,7 +29,7 @@ export class ReviewBatchController implements IReviewBatchController { if (!reviewController) return; // Update notes while preserving existing order - await reviewController.updateTodayNotes(true); + void reviewController.updateTodayNotes(true); const todayNotes = reviewController.getTodayNotes(); if (todayNotes.length === 0) { @@ -44,8 +42,8 @@ export class ReviewBatchController implements IReviewBatchController { // We need to update the core controller's state if (reviewController) { // Force update to ensure we start at the first note - await reviewController.updateTodayNotes(false); - + void reviewController.updateTodayNotes(false); + const note = todayNotes[0]; // Start the review with the selected note @@ -61,7 +59,7 @@ export class ReviewBatchController implements IReviewBatchController { * @param paths Array of note paths to review * @param useMCQ Whether to use MCQs for testing (default: false) */ - async reviewNotes(paths: string[], useMCQ: boolean = false): Promise { + reviewNotes(paths: string[], useMCQ = false): void { if (paths.length === 0) { new Notice("No notes selected for review."); return; @@ -76,7 +74,7 @@ export class ReviewBatchController implements IReviewBatchController { } if (useMCQ) { - new Notice(`Preparing MCQs for ${notesToReview.length} selected notes. This may take a moment...`); + new Notice(`Preparing MCQs for ${notesToReview.length} selected notes.This may take a moment...`); } else { new Notice(`Starting review of ${notesToReview.length} selected notes.`); } @@ -91,13 +89,13 @@ export class ReviewBatchController implements IReviewBatchController { * * @param useMCQ Whether to use MCQs for testing */ - async reviewAllNotesWithMCQ(useMCQ: boolean = true): Promise { + reviewAllNotesWithMCQ(useMCQ = true): void { const reviewController = this.plugin.reviewController; if (!reviewController) return; // Update today's notes first, preserving custom order if it exists const hasCustomOrder = this.plugin.reviewScheduleService.customNoteOrder.length > 0; - await reviewController.updateTodayNotes(hasCustomOrder); + void reviewController.updateTodayNotes(hasCustomOrder); const todayNotes = reviewController.getTodayNotes(); if (todayNotes.length === 0) { @@ -127,7 +125,7 @@ export class ReviewBatchController implements IReviewBatchController { // Update notes respecting custom order if it exists const hasCustomOrder = this.plugin.reviewScheduleService.customNoteOrder.length > 0; - await reviewController.updateTodayNotes(hasCustomOrder); + void reviewController.updateTodayNotes(hasCustomOrder); const todayNotes = reviewController.getTodayNotes(); if (todayNotes.length === 0) { @@ -136,7 +134,8 @@ export class ReviewBatchController implements IReviewBatchController { } if (!this.plugin.mcqController) { - new Notice("MCQ controller not initialized. Please check MCQ settings."); + // eslint-disable-next-line obsidianmd/ui/sentence-case + new Notice("MCQ controller not initialized, please check MCQ settings"); return; } @@ -162,7 +161,7 @@ export class ReviewBatchController implements IReviewBatchController { * @param paths Array of note paths to postpone * @param days Number of days to postpone (default: 1) */ - async postponeNotes(paths: string[], days: number = 1): Promise { + async postponeNotes(paths: string[], days = 1): Promise { const reviewController = this.plugin.reviewController; if (!reviewController) return; @@ -180,10 +179,10 @@ export class ReviewBatchController implements IReviewBatchController { await this.plugin.savePluginData(); // Add save call after loop // Refresh the sidebar view if available - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); // Update today's notes after postponing - await reviewController.updateTodayNotes(true); + void reviewController.updateTodayNotes(true); new Notice(`Postponed ${paths.length} notes by ${days} day(s).`); } @@ -216,9 +215,9 @@ export class ReviewBatchController implements IReviewBatchController { await this.plugin.savePluginData(); // Save once after all operations // Refresh the sidebar view if available - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); // Update today's notes after advancing - await reviewController.updateTodayNotes(true); + void reviewController.updateTodayNotes(true); new Notice(`Advanced ${advancedCount} note(s).`); } else { new Notice("No eligible notes were advanced."); @@ -248,10 +247,10 @@ export class ReviewBatchController implements IReviewBatchController { await this.plugin.savePluginData(); // Add save call after loop // Refresh the sidebar view if available - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); // Update today's notes after removing - await reviewController.updateTodayNotes(true); + void reviewController.updateTodayNotes(true); new Notice(`Removed ${paths.length} notes from review schedule.`); } diff --git a/controllers/review-controller-core.ts b/controllers/review-controller-core.ts index 7bc53e5..a83a2f2 100644 --- a/controllers/review-controller-core.ts +++ b/controllers/review-controller-core.ts @@ -1,9 +1,8 @@ -import { Modal, Notice, TFile } from 'obsidian'; +import { Notice, TFile } from 'obsidian'; import SpaceforgePlugin from '../main'; import { ReviewResponse, ReviewSchedule, FsrsRating } from '../models/review-schedule'; // Added FsrsRating import { IReviewController } from './interfaces'; import { ReviewModal } from '../ui/review-modal'; -import { getNextFileInSession, isSessionComplete } from '../models/review-session'; /** * Core controller that handles the review process @@ -22,7 +21,7 @@ export class ReviewControllerCore implements IReviewController { /** * Current index in today's notes */ - private currentNoteIndex: number = 0; + private currentNoteIndex = 0; /** * Cache of linked notes to improve performance @@ -61,9 +60,9 @@ export class ReviewControllerCore implements IReviewController { * Sets an override for the current review date. * @param date Timestamp of the date to simulate, or null to use actual Date.now(). */ - public async setReviewDateOverride(date: number | null): Promise { + public setReviewDateOverride(date: number | null): void { this.currentReviewDateOverride = date; - await this.updateTodayNotes(); // Ensure notes are updated when the override changes + this.updateTodayNotes(); // Ensure notes are updated when the override changes } /** @@ -116,7 +115,7 @@ export class ReviewControllerCore implements IReviewController { * * @param preserveCurrentIndex Whether to try to preserve the current note index */ - async updateTodayNotes(preserveCurrentIndex: boolean = false): Promise { + updateTodayNotes(preserveCurrentIndex = false): void { // Store current note path if we want to preserve the index let currentNotePath: string | null = null; if (preserveCurrentIndex && this.todayNotes.length > 0 && this.currentNoteIndex < this.todayNotes.length) { @@ -128,10 +127,6 @@ export class ReviewControllerCore implements IReviewController { this.traversalOrder = []; this.traversalPositions.clear(); - // Save the existing traversal order and positions - const originalTraversalOrder = [...this.traversalOrder]; - const originalPositions = new Map(this.traversalPositions); - // Get the latest due notes from storage, already sorted by custom order (if available) // by getDueNotesWithCustomOrder, using the effective review date. const effectiveDate = this.getEffectiveReviewDate(); @@ -205,7 +200,7 @@ export class ReviewControllerCore implements IReviewController { */ async reviewCurrentNote(): Promise { if (this.todayNotes.length === 0) { - await this.updateTodayNotes(); + this.updateTodayNotes(); if (this.todayNotes.length === 0) { new Notice("No notes due for review today!"); return; @@ -241,20 +236,16 @@ export class ReviewControllerCore implements IReviewController { * @param path Path to the note file * @param days Number of days to postpone (default: 1) */ - async postponeNote(path: string, days: number = 1): Promise { + async postponeNote(path: string, days = 1): Promise { // Store the current note path to potentially adjust currentNoteIndex later - // const currentPath = this.todayNotes.length > 0 && this.currentNoteIndex < this.todayNotes.length // Not strictly needed here as handleNotePostponed re-evaluates - // ? this.todayNotes[this.currentNoteIndex].path - // : null; - - await this.plugin.dataStorage.reviewScheduleService.postponeNote(path, days); // Corrected to call service via dataStorage - await this.plugin.savePluginData(); + this.plugin.dataStorage.reviewScheduleService.postponeNote(path, days); // Corrected to call service via dataStorage + await this.plugin.savePluginData(); // Explicitly update navigation state in controller to ensure UI consistency await this.handleNotePostponed(path); // Refresh the sidebar view if available - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); // Notice is handled by the service for postponeNote } @@ -264,13 +255,13 @@ export class ReviewControllerCore implements IReviewController { * @param path Path to the note file */ async advanceNote(path: string): Promise { - const advanced = await this.plugin.dataStorage.reviewScheduleService.advanceNote(path); + const advanced = this.plugin.dataStorage.reviewScheduleService.advanceNote(path); if (advanced) { await this.plugin.savePluginData(); await this.handleNoteAdvanced(path); // New handler for advancing - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); new Notice(`Note advanced.`); // Controller handles notice for advance } else { new Notice(`Note is not eligible to be advanced.`); @@ -283,14 +274,14 @@ export class ReviewControllerCore implements IReviewController { * * @param path Path to the advanced note */ - async handleNoteAdvanced(path: string): Promise { + handleNoteAdvanced(_path: string): void { // Re-fetch and re-sort today's notes, preserving current selection if possible. // updateTodayNotes(true) should correctly place the advanced note // if it's now due, or remove it if it was advanced from future to still future. // It also updates traversalOrder and currentNoteIndex. - await this.updateTodayNotes(true); + this.updateTodayNotes(true); } - + /** * Handle a note being postponed, updating navigation state * @@ -370,17 +361,17 @@ export class ReviewControllerCore implements IReviewController { */ async skipReview(path: string): Promise { const effectiveDate = this.getEffectiveReviewDate(); - await this.plugin.dataStorage.reviewScheduleService.skipNote(path, ReviewResponse.CorrectWithDifficulty, effectiveDate); // Pass effectiveDate + this.plugin.dataStorage.reviewScheduleService.skipNote(path, ReviewResponse.CorrectWithDifficulty, effectiveDate); // Pass effectiveDate await this.plugin.savePluginData(); // Add save call // Show notification with more informative message new Notice("Review postponed to tomorrow. Note will be easier to recover with a small penalty applied."); // Refresh the sidebar view if available - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); // Update today's notes after skipping the review - await this.updateTodayNotes(true); + this.updateTodayNotes(true); // Continue to the next note and show review modal if (this.todayNotes.length > 0) { @@ -419,15 +410,15 @@ export class ReviewControllerCore implements IReviewController { async processReviewResponse(path: string, response: ReviewResponse | FsrsRating): Promise { const effectiveDate = this.getEffectiveReviewDate(); // Record a normal review (not skipped) with the user's quality rating - const wasRecorded = await this.plugin.dataStorage.reviewScheduleService.recordReview(path, response, false, effectiveDate); - + const wasRecorded = this.plugin.dataStorage.reviewScheduleService.recordReview(path, response, false, effectiveDate); + if (!wasRecorded) { // Note was not due, this is just a preview new Notice("Note previewed, not recorded"); return; } - // await this.plugin.savePluginData(); // Moved save call to the end of the method + // Check for MCQ regeneration based on rating const schedule = this.plugin.dataStorage.reviewScheduleService.schedules[path]; @@ -454,11 +445,10 @@ export class ReviewControllerCore implements IReviewController { // Show notification based on response let responseText: string; - // const schedule = this.plugin.dataStorage.reviewScheduleService.schedules[path]; // Already defined above if (schedule && schedule.schedulingAlgorithm === 'fsrs') { // FSRS response text - switch(response as FsrsRating) { // Cast because we are in FSRS block + switch (response as FsrsRating) { // Cast because we are in FSRS block case FsrsRating.Again: responseText = "Again (1)"; break; case FsrsRating.Hard: responseText = "Hard (2)"; break; case FsrsRating.Good: responseText = "Good (3)"; break; @@ -467,7 +457,7 @@ export class ReviewControllerCore implements IReviewController { } } else { // SM-2 response text - switch(response as ReviewResponse) { // Cast because we are in SM-2 block + switch (response as ReviewResponse) { // Cast because we are in SM-2 block case ReviewResponse.CompleteBlackout: responseText = "Complete Blackout (0)"; break; case ReviewResponse.IncorrectResponse: responseText = "Incorrect Response (1)"; break; case ReviewResponse.IncorrectButFamiliar: responseText = "Incorrect but Familiar (2)"; break; @@ -480,10 +470,10 @@ export class ReviewControllerCore implements IReviewController { new Notice(`Note review recorded: ${responseText}`); // Refresh the sidebar view if available - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); // Update today's notes after recording the review (preserve the index since we're in the middle of navigation) - await this.updateTodayNotes(true); + this.updateTodayNotes(true); // Check if we're in a hierarchical review session const activeSession = this.plugin.dataStorage.getActiveSession(); @@ -491,7 +481,6 @@ export class ReviewControllerCore implements IReviewController { if (activeSession) { // Advance to the next file in the session await this.plugin.dataStorage.advanceActiveSession(); - // await this.plugin.savePluginData(); // Moved save call to the end of the method // Get the next file to review const nextFilePath = this.plugin.dataStorage.getNextSessionFile(); @@ -506,11 +495,6 @@ export class ReviewControllerCore implements IReviewController { } // If not in a session, automatically go to the next note and show review modal else if (this.todayNotes.length > 0) { - // Log the current state for debugging - - // Simply move to the next note in today's notes list (which maintains user's custom order) - this.currentNoteIndex = (this.currentNoteIndex + 1) % this.todayNotes.length; - // Check if we've reviewed all notes (in case we've come full circle) const currentPath = this.todayNotes[this.currentNoteIndex].path; if (currentPath === path) { @@ -518,8 +502,6 @@ export class ReviewControllerCore implements IReviewController { return; } - // Log the new state after determining next note - // After finding the next note, show the review modal if (this.todayNotes.length > 0 && this.currentNoteIndex < this.todayNotes.length) { const nextNotePath = this.todayNotes[this.currentNoteIndex].path; @@ -535,7 +517,7 @@ export class ReviewControllerCore implements IReviewController { } else { new Notice("All caught up! No more notes due for review."); } - + // Save all accumulated plugin data once at the end await this.plugin.savePluginData(); } diff --git a/controllers/review-controller-mcq.ts b/controllers/review-controller-mcq.ts index 324f178..51a8e78 100644 --- a/controllers/review-controller-mcq.ts +++ b/controllers/review-controller-mcq.ts @@ -2,7 +2,7 @@ import { Notice, TFile } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQModal } from '../ui/mcq-modal'; import { ConsolidatedMCQModal } from '../ui/consolidated-mcq-modal'; // Import ConsolidatedMCQModal -import { MCQSet, MCQSession, MCQQuestion } from '../models/mcq'; // Corrected import +import { MCQSet, MCQSession } from '../models/mcq'; import { ReviewSchedule, ReviewResponse, FsrsRating } from '../models/review-schedule'; // Import rating enums and ReviewSchedule import { MCQService } from '../services/mcq-service'; import { IMCQGenerationService } from '../api/mcq-generation-service'; @@ -63,17 +63,17 @@ export class MCQController { } async startMCQReview( - notePath: string, + notePath: string, // Keep the external callback signature for now, but we'll use an internal one for our logic externalOnCompleteCallback?: (path: string, success: boolean) => void ): Promise { if (!this.plugin.settings.enableMCQ) { - new Notice('MCQ feature is disabled in settings.'); + new Notice('MCQ feature is disabled in settings.'); // eslint-disable-line obsidianmd/ui/sentence-case if (externalOnCompleteCallback) externalOnCompleteCallback(notePath, false); return; } if (!this.mcqGenerationService) { - new Notice('MCQ generation service is not available. Check API provider settings.'); + new Notice('MCQ generation service is not available. Check API provider settings.'); // eslint-disable-line obsidianmd/ui/sentence-case return; } @@ -82,14 +82,14 @@ export class MCQController { if (mcqSet && mcqSet.needsQuestionRegeneration) { new Notice('Questions for this note are flagged for regeneration. Generating new set...'); - mcqSet = await this.generateMCQs(notePath, true); + mcqSet = await this.generateMCQs(notePath, true); if (mcqSet) { - mcqSet.needsQuestionRegeneration = false; - this.mcqService.saveMCQSet(mcqSet); + mcqSet.needsQuestionRegeneration = false; + this.mcqService.saveMCQSet(mcqSet); await this.plugin.savePluginData(); } else { new Notice('Failed to regenerate MCQs. Using existing set if available.'); - mcqSet = this.mcqService.getMCQSetForNote(notePath); + mcqSet = this.mcqService.getMCQSetForNote(notePath); } } @@ -102,49 +102,40 @@ export class MCQController { } } - const session: MCQSession = { - notePath, - mcqSetId: `${mcqSet.notePath}_${mcqSet.generatedAt}`, - startedAt: Date.now(), - answers: [], - completed: false, - score: 0, - currentQuestionIndex: 0, // Initialize required property - completedAt: null // Initialize required property (removed duplicate) - }; - // Define the internal callback for MCQModal that includes the score - const internalOnComplete = async (path: string, score: number, completed: boolean) => { - if (completed) { - const schedule = this.plugin.reviewScheduleService.schedules[path]; - if (schedule) { - let rating: ReviewResponse | FsrsRating; - if (schedule.schedulingAlgorithm === 'fsrs') { - rating = this.mapScoreToFsrsRating(score); - new Notice(`MCQ complete for FSRS card. Score: ${(score * 100).toFixed(0)}%. Rating: ${FsrsRating[rating]}(${rating as number}).`); + const internalOnComplete = (path: string, score: number, completed: boolean) => { + void (async () => { + if (completed) { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (schedule) { + let rating: ReviewResponse | FsrsRating; + if (schedule.schedulingAlgorithm === 'fsrs') { + rating = this.mapScoreToFsrsRating(score); + new Notice(`MCQ complete for FSRS card.Score: ${(score * 100).toFixed(0)}%.Rating: ${FsrsRating[rating]} (${rating as number}).`); + } else { + rating = this.mapScoreToSm2Response(score); + new Notice(`MCQ complete for SM - 2 card.Score: ${(score * 100).toFixed(0)}%.Rating: ${ReviewResponse[rating]} (${rating as number}).`); + } + // Process the review using the determined rating + await this.plugin.reviewController.processReviewResponse(path, rating); } else { - rating = this.mapScoreToSm2Response(score); - new Notice(`MCQ complete for SM-2 card. Score: ${(score * 100).toFixed(0)}%. Rating: ${ReviewResponse[rating]}(${rating as number}).`); + new Notice(`MCQ complete.Score: ${(score * 100).toFixed(0)}%.Could not find schedule to update review status.`); } - // Process the review using the determined rating - await this.plugin.reviewController.processReviewResponse(path, rating); } else { - new Notice(`MCQ complete. Score: ${(score * 100).toFixed(0)}%. Could not find schedule to update review status.`); + new Notice(`MCQ session for ${path} was not fully completed.Score(partial): ${(score * 100).toFixed(0)}%.Review not recorded.`); } - } else { - new Notice(`MCQ session for ${path} was not fully completed. Score (partial): ${(score * 100).toFixed(0)}%. Review not recorded.`); - } - // Call the original external callback if it was provided - if (externalOnCompleteCallback) { - externalOnCompleteCallback(path, completed && score >= 0.7); - } + // Call the original external callback if it was provided + if (externalOnCompleteCallback) { + externalOnCompleteCallback(path, completed && score >= 0.7); + } + })(); }; // Pass the new internal callback to the modal constructor - new MCQModal(this.plugin, notePath, mcqSet, internalOnComplete).open(); - } catch (error) { - new Notice('Error starting MCQ review. Please check console for details.'); + 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 if (externalOnCompleteCallback) externalOnCompleteCallback(notePath, false); } } @@ -158,7 +149,7 @@ export class MCQController { */ async generateMCQs(notePath: string, forceRegeneration = false): Promise { if (!this.plugin.settings.enableMCQ || !this.mcqGenerationService) { - new Notice('MCQ feature is disabled or the generation service is not available. Check API provider settings.'); + new Notice('MCQ feature is disabled or the generation service is not available. Check API provider settings.'); // eslint-disable-line obsidianmd/ui/sentence-case return null; } @@ -172,7 +163,7 @@ export class MCQController { } } } - + const file = this.plugin.app.vault.getAbstractFileByPath(notePath); if (!(file instanceof TFile)) { new Notice('Cannot generate MCQs: file not found'); @@ -184,7 +175,7 @@ export class MCQController { if (mcqSet) { this.mcqService.saveMCQSet(mcqSet); - await this.plugin.savePluginData(); + await this.plugin.savePluginData(); new Notice('MCQs generated and saved successfully.'); return mcqSet; } else { @@ -211,11 +202,11 @@ export class MCQController { */ async startConsolidatedMCQReviewForSelectedDate(): Promise { if (!this.plugin.settings.enableMCQ) { - new Notice('MCQ feature is disabled in settings.'); + new Notice('MCQ feature is disabled in settings.'); // eslint-disable-line obsidianmd/ui/sentence-case return; } if (!this.mcqGenerationService) { - new Notice('MCQ generation service is not available. Check API provider settings.'); + new Notice('MCQ generation service is not available. Check API provider settings.'); // eslint-disable-line obsidianmd/ui/sentence-case return; } @@ -245,19 +236,19 @@ export class MCQController { this.mcqService.saveMCQSet(mcqSet); // Overall plugin data save will happen once after the loop } else { - new Notice(`Failed to regenerate MCQs for ${notePath}. Using existing set if available (might be outdated or empty).`); + new Notice(`Failed to regenerate MCQs for ${notePath}.Using existing set if available(might be outdated or empty).`); // mcqSet remains the old one, or null if it didn't exist. } } - + // If no MCQ set exists after potential regeneration, or if it's empty, try to generate one. if (!mcqSet || mcqSet.questions.length === 0) { - new Notice(`No MCQs found or set is empty for ${notePath}. Attempting to generate new set...`); + new Notice(`No MCQs found or set is empty for ${notePath}.Attempting to generate new set...`); const newMcqSet = await this.generateMCQs(notePath, false); // forceRegeneration = false (respects cache if fresh) if (newMcqSet) { mcqSet = newMcqSet; } else { - new Notice(`Failed to generate MCQs for ${notePath}. This note will be skipped in MCQ review.`); + new Notice(`Failed to generate MCQs for ${notePath}.This note will be skipped in MCQ review.`); } } @@ -271,44 +262,43 @@ export class MCQController { notesWithMCQsCount++; } } - + if (mcqSetsForReview.length === 0) { new Notice('No MCQs available or generated for the due notes.'); return; } - + await this.plugin.savePluginData(); // Save any changes from MCQ generation new Notice(`Starting consolidated MCQ review for ${notesWithMCQsCount} note(s) with ${mcqSetsForReview.reduce((sum, s) => sum + s.mcqSet.questions.length, 0)} questions.`); - const onConsolidatedComplete = async ( + const onConsolidatedComplete = ( results: Array<{ path: string; success: boolean; response: ReviewResponse; score?: number }> ) => { - let reviewsProcessed = 0; - for (const result of results) { - const schedule = this.plugin.reviewScheduleService.schedules[result.path]; - if (schedule && typeof result.score === 'number') { - let rating: ReviewResponse | FsrsRating; - if (schedule.schedulingAlgorithm === 'fsrs') { - rating = this.mapScoreToFsrsRating(result.score); - new Notice(`MCQ for ${result.path} (FSRS) - Score: ${(result.score * 100).toFixed(0)}%, Rating: ${FsrsRating[rating]}(${rating as number})`); - } else { // SM-2 - rating = this.mapScoreToSm2Response(result.score); - new Notice(`MCQ for ${result.path} (SM-2) - Score: ${(result.score * 100).toFixed(0)}%, Rating: ${ReviewResponse[rating]}(${rating as number})`); + void (async () => { + let reviewsProcessed = 0; + for (const result of results) { + const schedule = this.plugin.reviewScheduleService.schedules[result.path]; + if (schedule && typeof result.score === 'number') { + let rating: ReviewResponse | FsrsRating; + if (schedule.schedulingAlgorithm === 'fsrs') { + rating = this.mapScoreToFsrsRating(result.score); + new Notice(`MCQ for ${result.path}(FSRS) - Score: ${(result.score * 100).toFixed(0)}%, Rating: ${FsrsRating[rating]} (${rating as number})`); + } else { // SM-2 + rating = this.mapScoreToSm2Response(result.score); + new Notice(`MCQ for ${result.path}(SM - 2) - Score: ${(result.score * 100).toFixed(0)}%, Rating: ${ReviewResponse[rating]} (${rating as number})`); + } + await this.plugin.reviewController.processReviewResponse(result.path, rating); + reviewsProcessed++; } - await this.plugin.reviewController.processReviewResponse(result.path, rating); - reviewsProcessed++; - } else if (schedule) { - // If score is undefined, it might mean the note had no questions or wasn't processed. - // We could use the 'response' from ConsolidatedMCQModal if it's meaningful without a score. - // For now, we only process if a score is present. } - } - if (reviewsProcessed > 0) { - new Notice(`${reviewsProcessed} note review(s) updated based on consolidated MCQ session.`); - } else { - new Notice("No note reviews were updated from the MCQ session."); - } + 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"); + } + })(); }; new ConsolidatedMCQModal(this.plugin, mcqSetsForReview, onConsolidatedComplete).open(); diff --git a/controllers/review-controller.ts b/controllers/review-controller.ts index 6d01099..7071161 100644 --- a/controllers/review-controller.ts +++ b/controllers/review-controller.ts @@ -1,14 +1,10 @@ -import { Notice } from 'obsidian'; import SpaceforgePlugin from '../main'; import { ReviewControllerCore } from './review-controller-core'; import { ReviewNavigationController } from './review-navigation-controller'; -import { ReviewSessionController } from './review-session-controller'; import { ReviewBatchController } from './review-batch-controller'; import { MCQController } from './review-controller-mcq'; // Import MCQController -import { IReviewController, IReviewNavigationController, IReviewBatchController, IReviewSessionController } from './interfaces'; -import { ReviewResponse, FsrsRating } from '../models/review-schedule'; // Added FsrsRating -import { LinkAnalyzer } from '../utils/link-analyzer'; -import { DateUtils } from '../utils/dates'; +import { IReviewController } from './interfaces'; +import { ReviewResponse, FsrsRating } from '../models/review-schedule'; import { MCQService } from '../services/mcq-service'; // Import MCQService /** @@ -53,11 +49,11 @@ export class ReviewController implements IReviewController { this.navigationController = new ReviewNavigationController(plugin); // Initialize navigation controller // Pass the generation service if available if (plugin.mcqGenerationService) { - this.mcqController = new MCQController(plugin, mcqService, plugin.mcqGenerationService); + this.mcqController = new MCQController(plugin, mcqService, plugin.mcqGenerationService); } else { - // Handle case where service might not be initialized (e.g., MCQ disabled) - // Depending on usage, might need null checks later or ensure it's always initialized when enabled. - // this.mcqController = undefined; // Or handle appropriately + // Handle case where service might not be initialized (e.g., MCQ disabled) + // Depending on usage, might need null checks later or ensure it's always initialized when enabled. + // this.mcqController = undefined; // Or handle appropriately } this.batchController = new ReviewBatchController(plugin); // Initialize batch controller } @@ -68,8 +64,8 @@ export class ReviewController implements IReviewController { * * @param preserveCurrentIndex Whether to try to preserve the current note index */ - async updateTodayNotes(preserveCurrentIndex: boolean = false): Promise { - await this.coreController.updateTodayNotes(preserveCurrentIndex); + updateTodayNotes(preserveCurrentIndex = false): void { + this.coreController.updateTodayNotes(preserveCurrentIndex); } /** @@ -123,7 +119,7 @@ export class ReviewController implements IReviewController { * @param path Path to the note file * @param days Number of days to postpone (default: 1) */ - async postponeNote(path: string, days: number = 1): Promise { + async postponeNote(path: string, days = 1): Promise { await this.coreController.postponeNote(path, days); } @@ -183,8 +179,8 @@ export class ReviewController implements IReviewController { * Delegates to core controller. * @param date Timestamp of the date to simulate, or null to use actual Date.now(). */ - public async setReviewDateOverride(date: number | null): Promise { - await this.coreController.setReviewDateOverride(date); + public setReviewDateOverride(date: number | null): void { + this.coreController.setReviewDateOverride(date); } /** @@ -251,8 +247,8 @@ export class ReviewController implements IReviewController { * @param paths Array of note paths to review * @param useMCQ Whether to use MCQs for testing (default: false) */ - async reviewNotes(paths: string[], useMCQ: boolean = false): Promise { - await this.batchController.reviewNotes(paths, useMCQ); + reviewNotes(paths: string[], useMCQ = false): void { + this.batchController.reviewNotes(paths, useMCQ); } /** @@ -262,7 +258,7 @@ export class ReviewController implements IReviewController { * @param paths Array of note paths to postpone * @param days Number of days to postpone (default: 1) */ - async postponeNotes(paths: string[], days: number = 1): Promise { + async postponeNotes(paths: string[], days = 1): Promise { await this.batchController.postponeNotes(paths, days); } @@ -313,7 +309,7 @@ export class ReviewController implements IReviewController { * * @param useMCQ Whether to use MCQs for testing */ - async reviewAllNotesWithMCQ(useMCQ: boolean = true): Promise { - await this.batchController.reviewAllNotesWithMCQ(useMCQ); + reviewAllNotesWithMCQ(useMCQ = true): void { + this.batchController.reviewAllNotesWithMCQ(useMCQ); } } diff --git a/controllers/review-navigation-controller.ts b/controllers/review-navigation-controller.ts index ee288dd..b6ec5e2 100644 --- a/controllers/review-navigation-controller.ts +++ b/controllers/review-navigation-controller.ts @@ -1,8 +1,6 @@ import { Notice, TFile } from 'obsidian'; import SpaceforgePlugin from '../main'; import { IReviewNavigationController } from './interfaces'; -import { LinkAnalyzer } from '../utils/link-analyzer'; -import { DateUtils } from '../utils/dates'; /** * Controller for navigating between notes in review @@ -33,7 +31,7 @@ export class ReviewNavigationController implements IReviewNavigationController { const currentNoteIndex = reviewController.getCurrentNoteIndex(); if (todayNotes.length === 0) { - await reviewController.updateTodayNotes(); + void reviewController.updateTodayNotes(); if (todayNotes.length === 0) { new Notice("No notes due for review today!"); return; @@ -52,11 +50,11 @@ export class ReviewNavigationController implements IReviewNavigationController { if (!reviewController) return; const todayNotes = reviewController.getTodayNotes(); - let currentNoteIndex = reviewController.getCurrentNoteIndex(); + const currentNoteIndex = reviewController.getCurrentNoteIndex(); if (todayNotes.length === 0) { // Force update notes regardless of custom order - await reviewController.updateTodayNotes(false); + void reviewController.updateTodayNotes(false); if (todayNotes.length === 0) { new Notice("No notes due for review today!"); return; @@ -86,7 +84,7 @@ export class ReviewNavigationController implements IReviewNavigationController { const currentFolder = currentFile.parent ? currentFile.parent.path : null; const nextFolder = nextFile.parent ? nextFile.parent.path : null; - if (this.plugin.sessionController && + if (this.plugin.sessionController && this.plugin.sessionController.getDueLinkedNotes(todayNotes[currentNoteIndex].path).includes(nextPath)) { messageType = "linked note"; } else if (currentFolder === nextFolder) { @@ -143,12 +141,12 @@ export class ReviewNavigationController implements IReviewNavigationController { if (!reviewController) return; const todayNotes = reviewController.getTodayNotes(); - let currentNoteIndex = reviewController.getCurrentNoteIndex(); + const currentNoteIndex = reviewController.getCurrentNoteIndex(); if (todayNotes.length === 0) { // Check for custom order when updating notes const hasCustomOrder = this.plugin.reviewScheduleService.customNoteOrder.length > 0; - await reviewController.updateTodayNotes(hasCustomOrder); + void reviewController.updateTodayNotes(hasCustomOrder); if (todayNotes.length === 0) { new Notice("No notes due for review today!"); return; @@ -211,7 +209,6 @@ export class ReviewNavigationController implements IReviewNavigationController { if (!reviewController) return; const todayNotes = reviewController.getTodayNotes(); - let currentNoteIndex = reviewController.getCurrentNoteIndex(); // Find the indices of these notes in todayNotes const index1 = todayNotes.findIndex((n) => n.path === path1); @@ -228,11 +225,11 @@ export class ReviewNavigationController implements IReviewNavigationController { // Update the custom note order in persistent storage const newOrder = newTodayNotes.map(note => note.path); - await this.plugin.reviewScheduleService.updateCustomNoteOrder(newOrder); + this.plugin.reviewScheduleService.updateCustomNoteOrder(newOrder); await this.plugin.savePluginData(); // Add save call // Force refresh the core controller state - await reviewController.updateTodayNotes(true); + void reviewController.updateTodayNotes(true); // Debug log } diff --git a/controllers/review-session-controller.ts b/controllers/review-session-controller.ts index f60ce0c..e2b5eba 100644 --- a/controllers/review-session-controller.ts +++ b/controllers/review-session-controller.ts @@ -1,4 +1,3 @@ -import { Notice } from 'obsidian'; import SpaceforgePlugin from '../main'; import { IReviewSessionController } from './interfaces'; import { LinkAnalyzer } from '../utils/link-analyzer'; @@ -42,7 +41,7 @@ export class ReviewSessionController implements IReviewSessionController { // If we don't have links cached, try to analyze them if (links.length === 0) { - (async () => { + void (async () => { const newLinks = await this.analyzeNoteLinks(notePath); if (newLinks.length > 0) { this.linkedNoteCache.set(notePath, newLinks); @@ -74,9 +73,9 @@ export class ReviewSessionController implements IReviewSessionController { // Store in cache for future use this.linkedNoteCache.set(notePath, links); - + return links; - } catch (error) { + } catch { return []; } } diff --git a/data-storage.ts b/data-storage.ts index aa4d768..cb4f0e6 100644 --- a/data-storage.ts +++ b/data-storage.ts @@ -1,11 +1,9 @@ -import { Notice, TFile, TFolder } from 'obsidian'; -import { ReviewHistoryItem, ReviewResponse, ReviewSchedule, toSM2Quality } from './models/review-schedule'; +import { TFile } from 'obsidian'; import SpaceforgePlugin from './main'; -import { DateUtils } from './utils/dates'; -import { EstimationUtils } from './utils/estimation'; -import { ReviewSession, ReviewSessionStore, generateSessionId, getNextFileInSession, advanceSession, isSessionComplete } from './models/review-session'; -import { LinkAnalyzer } from './utils/link-analyzer'; +import { ReviewHistoryItem, ReviewSchedule, ReviewResponse } from './models/review-schedule'; +import { ReviewSessionStore, ReviewSession } from './models/review-session'; import { MCQSet, MCQSession } from './models/mcq'; +import { EstimationUtils } from './utils/estimation'; import { ReviewScheduleService } from './services/review-schedule-service'; import { ReviewHistoryService } from './services/review-history-service'; import { ReviewSessionService } from './services/review-session-service'; @@ -142,12 +140,10 @@ export class DataStorage { this.reviewScheduleService.schedules = {}; isValid = false; } else { - let invalidCount = 0; for (const path in this.reviewScheduleService.schedules) { const schedule = this.reviewScheduleService.schedules[path]; if (!schedule || typeof schedule !== 'object') { delete this.reviewScheduleService.schedules[path]; - invalidCount++; isValid = false; continue; } @@ -158,7 +154,6 @@ export class DataStorage { !('ease' in s) || typeof s.ease !== 'number') { delete this.reviewScheduleService.schedules[path]; - invalidCount++; isValid = false; } } @@ -198,11 +193,12 @@ export class DataStorage { const hasMCQs = Object.keys(this.mcqService.mcqSets).length > 0; if (noSchedules && hasMCQs) { + // no-op } return isValid; } - public async cleanupNonExistentFiles(): Promise { + public cleanupNonExistentFiles(): boolean { let changesMade = false; if (!this.reviewScheduleService.schedules || typeof this.reviewScheduleService.schedules !== 'object') { this.reviewScheduleService.schedules = {}; @@ -228,12 +224,11 @@ export class DataStorage { preserved: false }; try { - const allSchedules = {...this.reviewScheduleService.schedules}; + const allSchedules = { ...this.reviewScheduleService.schedules }; const allFiles = new Set(); try { - const mdFiles = this.plugin.app.vault.getMarkdownFiles(); - mdFiles.forEach(file => allFiles.add(file.path)); - } catch (listError) { + this.plugin.app.vault.getMarkdownFiles().forEach(file => allFiles.add(file.path)); + } catch { return changesMade; } if (allFiles.size === 0 && beforeCount > 0) { @@ -251,7 +246,8 @@ export class DataStorage { cleanupCount++; changesMade = true; } - } catch (checkError) { + } catch { + // no-op } if (safetyCheck.missingSchedules > 0 && safetyCheck.missingSchedules === safetyCheck.checkedSchedules && @@ -265,6 +261,7 @@ export class DataStorage { } } if (cleanupCount > 0 && !safetyCheck.preserved) { + // no-op } const dataState = { schedules: Object.keys(this.reviewScheduleService.schedules).length, @@ -274,8 +271,10 @@ export class DataStorage { mcqSessions: Object.keys(this.mcqService.mcqSessions || {}).length }; if (dataState.schedules === 0 && (dataState.history > 0 || dataState.reviewSessions > 0 || dataState.mcqSets > 0)) { + // no-op } - } catch (error) { + } catch { + // no-op } return changesMade; // Return whether cleanup occurred @@ -287,13 +286,13 @@ export class DataStorage { // but they now simply call the corresponding method on the service instance. // REMOVED await this.saveData() from all these methods. - async scheduleNoteForReview(path: string, daysFromNow: number = 0): Promise { + async scheduleNoteForReview(path: string, daysFromNow = 0): Promise { await this.reviewScheduleService.scheduleNoteForReview(path, daysFromNow); // await this.saveData(); // Removed } - async recordReview(path: string, response: ReviewResponse, isSkipped: boolean = false): Promise { - return await this.reviewScheduleService.recordReview(path, response, isSkipped); + recordReview(path: string, response: ReviewResponse, isSkipped = false): boolean { + return this.reviewScheduleService.recordReview(path, response, isSkipped); } calculateNewSchedule( @@ -305,8 +304,8 @@ export class DataStorage { // but keeping for potential external use or backward compatibility if needed. // It should probably be moved to ReviewScheduleService if kept. // For now, just calling the service method. - const result = this.reviewScheduleService.calculateNewSchedule(currentInterval, currentEase, response); - return { interval: result.interval, ease: result.ease }; + const result = this.reviewScheduleService.calculateNewSchedule(currentInterval, currentEase, response); + return { interval: result.interval, ease: result.ease }; } async skipNote(path: string, response: ReviewResponse = ReviewResponse.CorrectWithDifficulty): Promise { @@ -314,7 +313,7 @@ export class DataStorage { // await this.saveData(); // Removed } - async postponeNote(path: string, days: number = 1): Promise { + async postponeNote(path: string, days = 1): Promise { await this.reviewScheduleService.postponeNote(path, days); // await this.saveData(); // Removed } @@ -338,7 +337,7 @@ export class DataStorage { try { const content = await this.plugin.app.vault.read(file); return EstimationUtils.estimateReviewTime(file, content); - } catch (error) { + } catch { return 60; // Default 1 minute } } @@ -373,7 +372,7 @@ export class DataStorage { return moreFiles; } - async scheduleNotesInOrder(paths: string[], daysFromNow: number = 0): Promise { + async scheduleNotesInOrder(paths: string[], daysFromNow = 0): Promise { const count = await this.reviewScheduleService.scheduleNotesInOrder(paths, daysFromNow); // if (count > 0) { // Removed save call // await this.saveData(); @@ -381,13 +380,13 @@ export class DataStorage { return count; } - async scheduleSessionForReview(sessionId: string): Promise { + scheduleSessionForReview(sessionId: string): number { // This method calls scheduleNotesInOrder internally, which saves data. // No need to save again here. - return await this.reviewSessionService.scheduleSessionForReview(sessionId); + return this.reviewSessionService.scheduleSessionForReview(sessionId); } - async saveMCQSet(mcqSet: MCQSet): Promise { + saveMCQSet(mcqSet: MCQSet): string { const id = this.mcqService.saveMCQSet(mcqSet); // await this.saveData(); // Removed return id; @@ -397,7 +396,7 @@ export class DataStorage { return this.mcqService.getMCQSetForNote(notePath); } - async saveMCQSession(session: MCQSession): Promise { + saveMCQSession(session: MCQSession): void { this.mcqService.saveMCQSession(session); // await this.saveData(); // Removed } @@ -415,7 +414,7 @@ export class DataStorage { // await this.saveData(); // Removed } - getDueNotesWithCustomOrder(date: number = Date.now(), useCustomOrder: boolean = true): ReviewSchedule[] { + getDueNotesWithCustomOrder(date: number = Date.now(), useCustomOrder = true): ReviewSchedule[] { return this.reviewScheduleService.getDueNotesWithCustomOrder(date, useCustomOrder); } diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c43c2dc --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,69 @@ +import globals from "globals"; +import pluginJs from "@eslint/js"; +import tseslint from "typescript-eslint"; +import obsidianmd from "eslint-plugin-obsidianmd"; +import tsparser from "@typescript-eslint/parser"; + +export default [ + { ignores: ["dist/", "node_modules/", "main.js", ".eslintrc.js", "esbuild.config.mjs", "version-bump.mjs", "eslint-plugin/", "install.js"] }, + + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + + { + files: ["**/*.ts", "**/*.tsx"], + plugins: { + obsidianmd: obsidianmd + }, + languageOptions: { + parser: tsparser, + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir: import.meta.dirname, + }, + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_", caughtErrorsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-empty-function": "off", + "no-unused-vars": "off", + "no-prototype-builtins": "off", + "@typescript-eslint/require-await": "error", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "no-empty": "error", + + // Obsidian Rules + "obsidianmd/commands/no-command-in-command-id": "error", + "obsidianmd/commands/no-command-in-command-name": "error", + "obsidianmd/commands/no-default-hotkeys": "error", + "obsidianmd/commands/no-plugin-id-in-command-id": "error", + "obsidianmd/commands/no-plugin-name-in-command-name": "error", + "obsidianmd/settings-tab/no-manual-html-headings": "error", + "obsidianmd/settings-tab/no-problematic-settings-headings": "error", + "obsidianmd/vault/iterate": "error", + "obsidianmd/detach-leaves": "error", + "obsidianmd/hardcoded-config-path": "error", + "obsidianmd/no-forbidden-elements": "error", + "obsidianmd/no-plugin-as-component": "error", + "obsidianmd/no-sample-code": "error", + "obsidianmd/no-tfile-tfolder-cast": "error", + "obsidianmd/no-view-references-in-plugin": "error", + "obsidianmd/no-static-styles-assignment": "error", + "obsidianmd/object-assign": "error", + "obsidianmd/platform": "error", + "obsidianmd/prefer-file-manager-trash-file": "warn", + "obsidianmd/prefer-abstract-input-suggest": "error", + "obsidianmd/regex-lookbehind": "error", + "obsidianmd/sample-names": "error", + "obsidianmd/validate-manifest": "error", + "obsidianmd/validate-license": ["error"], + "obsidianmd/ui/sentence-case": "warn" + }, + }, +]; diff --git a/main.ts b/main.ts index 2ff3c22..1ebaa04 100644 --- a/main.ts +++ b/main.ts @@ -1,4 +1,4 @@ -import { App, Notice, Plugin, ViewCreator, WorkspaceLeaf, addIcon, TFile, TFolder, normalizePath } from 'obsidian'; +import { Notice, Plugin, TFile, normalizePath, TFolder } from 'obsidian'; import { EventEmitter } from './utils/event-emitter'; import { DataStorage } from './data-storage'; import { ReviewController } from './controllers/review-controller'; @@ -69,9 +69,9 @@ export default class SpaceforgePlugin extends Plugin { // Now services can be initialized, they will use the default settings initially this.reviewScheduleService = new ReviewScheduleService( - this, - this.pluginState.schedules, - this.pluginState.customNoteOrder, + this, + this.pluginState.schedules, + this.pluginState.customNoteOrder, this.pluginState.lastLinkAnalysisTimestamp ?? null, // Coalesce undefined to null this.pluginState.history ); @@ -80,15 +80,15 @@ export default class SpaceforgePlugin extends Plugin { this.mcqService = new MCQService(this.pluginState.mcqSets, this.pluginState.mcqSessions); // Load stored data, which will override the defaults in this.settings and this.pluginState - await this.loadPluginData(); - + await this.loadPluginData(); + // After settings are fully loaded (including from storage), ensure services are updated // This is crucial for FsrsScheduleService to get the correct FSRS parameters. this.reviewScheduleService.updateAlgorithmServicesForSettingsChange(); - + // Initialize PomodoroService after settings are loaded this.pomodoroService = new PomodoroService(this); - + // Initialize CalendarEventService after settings are loaded this.calendarEventService = new CalendarEventService(); const eventsArray = Object.values(this.pluginState.calendarEvents); @@ -121,7 +121,8 @@ export default class SpaceforgePlugin extends Plugin { EstimationUtils.setPlugin(this); this.addIcons(); this.contextMenuHandler.register(); - this.addRibbonIcon('calendar-clock', 'Spaceforge Review', async () => { + // eslint-disable-next-line obsidianmd/ui/sentence-case + this.addRibbonIcon('calendar-clock', 'Spaceforge review', async () => { await this.activateSidebarView(); }); this.addSettingTab(new SpaceforgeSettingTab(this.app, this)); @@ -129,7 +130,8 @@ export default class SpaceforgePlugin extends Plugin { this.addCommand({ id: 'add-selected-file-to-review', - name: 'Add Selected File to Review Schedule (File Explorer)', + // 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]; const viewWithFile = fileExplorerLeaf?.view as { file?: TFile }; @@ -140,46 +142,48 @@ 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."); + new Notice("Selected item is not a markdown file."); // eslint-disable-line obsidianmd/ui/sentence-case } } else { - new Notice("No file selected in file explorer."); + new Notice("No file selected in file explorer"); } } }); - this.registerEvent(this.app.workspace.on('file-open', (file) => { /* ... */ })); + this.registerEvent(this.app.workspace.on('file-open', (_file) => { /* ... */ })); this.registerEvent(this.app.vault.on('delete', async (file) => { if (file instanceof TFile && file.extension === "md") { await this.reviewScheduleService.removeFromReview(file.path); await this.savePluginData(); } - this.getSidebarView()?.refresh(); + void this.getSidebarView()?.refresh(); })); this.registerEvent(this.app.vault.on('rename', async (file, oldPath) => { if (file instanceof TFile && file.extension === "md") { this.reviewScheduleService.handleNoteRename(oldPath, file.path); await this.savePluginData(); } - this.getSidebarView()?.refresh(); + void this.getSidebarView()?.refresh(); })); this.registerInterval(window.setInterval(() => { - this.getSidebarView()?.refresh(); + void this.getSidebarView()?.refresh(); }, 60 * 1000)); // this.app.workspace.onLayoutReady(() => this.activateSidebarView()); // Removed automatic activation this.addStylesheet(); if (this.app.vault.adapter.stat && typeof this.app.vault.adapter.stat === 'function') { - this.cssHotReloadIntervalId = window.setInterval(async () => { - try { - const stats = await this.app.vault.adapter.stat(this.stylesheetPath); - if (stats && (this.lastStylesModTime === null || this.lastStylesModTime < stats.mtime)) { - this.lastStylesModTime = stats.mtime; - this.addStylesheet(); - } - } catch (error) { /* handle error */ } + this.cssHotReloadIntervalId = window.setInterval(() => { + void (async () => { + try { + const stats = await this.app.vault.adapter.stat(this.stylesheetPath); + if (stats && (this.lastStylesModTime === null || this.lastStylesModTime < stats.mtime)) { + this.lastStylesModTime = stats.mtime; + this.addStylesheet(); + } + } catch { /* handle error */ } + })(); }, 1000); this.registerInterval(this.cssHotReloadIntervalId); } @@ -188,20 +192,16 @@ export default class SpaceforgePlugin extends Plugin { this.registerInterval(window.setInterval(() => this.checkForDueNotes(), 5 * 60 * 1000)); } - this.registerInterval(window.setInterval(async () => { - await this.savePluginData(); - // Removed specific openRouterApiKey backup from here, handled by general settings save. + this.registerInterval(window.setInterval(() => { + void (async () => { + await this.savePluginData(); + // Removed specific openRouterApiKey backup from here, handled by general settings save. + })(); }, 5 * 60 * 1000)); - this.beforeUnloadHandler = (event) => { - let existingData = {}; - try { - const loadedData = this.loadData(); - if (loadedData && !(loadedData instanceof Promise)) { - existingData = loadedData; - } else if (loadedData instanceof Promise) { - } - } catch (loadError) { /* handle error */ } + this.beforeUnloadHandler = (_event) => { + const existingData = {}; + // Removed sync loadData call which returns a Promise and cannot be awaited here. try { const reviewData = { @@ -215,23 +215,24 @@ export default class SpaceforgePlugin extends Plugin { version: this.manifest.version }; const combinedData = { ...existingData, reviewData }; - let backupStr = JSON.stringify(combinedData); + const backupStr = JSON.stringify(combinedData); this.app.saveLocalStorage('spaceforge-backup', backupStr); - (async () => { + void (async () => { try { await this.savePluginData(); - } catch (e) { /* handle error */ } + } catch { /* handle error */ } })(); - } catch (error) { + } catch (_error) { 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 }}); + 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); - } catch (minimalError) { /* handle error */ } + } catch { /* handle error */ } } }; // Removed the extra closing parenthesis and semicolon, and moved the declaration. window.addEventListener('beforeunload', this.beforeUnloadHandler); } + // eslint-disable-next-line @typescript-eslint/no-misused-promises async onunload(): Promise { if (this.cssHotReloadIntervalId !== null) { window.clearInterval(this.cssHotReloadIntervalId); @@ -246,13 +247,13 @@ export default class SpaceforgePlugin extends Plugin { } let existingData: Partial = {}; - try { + try { const loaded = await this.loadData(); if (loaded) { existingData = loaded; } } - catch (loadError) { /* handle error */ } + catch { /* handle error */ } try { const reviewData = { @@ -268,11 +269,11 @@ export default class SpaceforgePlugin extends Plugin { const combinedData = { ...existingData, reviewData }; const backupStr = JSON.stringify(combinedData); this.app.saveLocalStorage('spaceforge-backup', backupStr); - } catch (backupError) { /* handle error */ } + } catch { /* handle error */ } try { await this.savePluginData(); - } catch (error) { /* handle error */ } + } catch { /* handle error */ } if (this.pomodoroService) this.pomodoroService.destroy(); } @@ -282,7 +283,7 @@ export default class SpaceforgePlugin extends Plugin { // return (customPath && typeof customPath === 'string' && customPath.trim() !== '') ? customPath.trim() : null; // } - private async _getEffectiveDataPath(): Promise { + private _getEffectiveDataPath(): string | null { if (this.settings?.useCustomDataPath) { const relativePath = this.settings.customDataPath?.trim(); // Get the path if (relativePath && relativePath !== '') { // Only proceed if path is non-empty @@ -292,7 +293,7 @@ export default class SpaceforgePlugin extends Plugin { pathForJson += '/'; } pathForJson += 'data.json'; - + const vaultBasePath = this.app.vault.getRoot().path; // Usually an empty string for vault root let absolutePath = (vaultBasePath ? vaultBasePath + '/' : '') + pathForJson; absolutePath = normalizePath(absolutePath); @@ -300,7 +301,7 @@ export default class SpaceforgePlugin extends Plugin { } else { // useCustomDataPath is true, but customDataPath is empty. // Silently fall back to default plugin storage without a warning. - return null; + return null; } } return null; // useCustomDataPath is false, use default plugin storage @@ -321,13 +322,13 @@ export default class SpaceforgePlugin extends Plugin { if (typeof lsCustomPathRelative === 'string') { this.settings.customDataPath = lsCustomPathRelative; } - + // Now, this.settings.useCustomDataPath and this.settings.customDataPath reflect the user's // most recent choice from the UI (via localStorage) or defaults if never set. let rawLoadedData: SpaceforgePluginData | undefined; // let preliminarySettingsData: Partial | null = null; // No longer needed - const effectivePath = await this._getEffectiveDataPath(); // Uses the now-bootstrapped settings + const effectivePath = this._getEffectiveDataPath(); // Uses the now-bootstrapped settings const defaultPluginDataPath = this.app.vault.configDir + `/plugins/${this.manifest.id}/data.json`; try { @@ -350,10 +351,10 @@ 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 (_migrationReadError) { /* handle error */ } } if (!rawLoadedData) { - new Notice(`Spaceforge: No data file found at custom path ${effectivePath}. New data file will be created on save.`, 3000); + new Notice(`Spaceforge: No data file found at custom path ${effectivePath}. New data file will be created on save.`, 3000); } } } else { @@ -364,12 +365,12 @@ export default class SpaceforgePlugin extends Plugin { let loadedSettings = {}; // Start with empty object if (rawLoadedData?.settings && typeof rawLoadedData.settings === 'object') { // If data was loaded and settings is an object, use its settings - loadedSettings = rawLoadedData.settings; + loadedSettings = rawLoadedData.settings; } // Merge defaults, loaded settings, and prioritize localStorage path settings this.settings = { ...DEFAULT_SETTINGS, ...loadedSettings }; if (typeof lsUseCustomPath === 'boolean') { - this.settings.useCustomDataPath = lsUseCustomPath; + this.settings.useCustomDataPath = lsUseCustomPath; } if (typeof lsCustomPathRelative === 'string') { this.settings.customDataPath = lsCustomPathRelative; @@ -382,12 +383,12 @@ export default class SpaceforgePlugin extends Plugin { } else if (rawLoadedData && !rawLoadedData.pluginState && (rawLoadedData as Partial).schedules) { // Legacy check this.pluginState = { ...this.pluginState, ...(rawLoadedData as Partial) }; } - + // Ensure calendarEvents exists for legacy data if (!this.pluginState.calendarEvents) { this.pluginState.calendarEvents = {}; } - + // Ensure pomodoro state is initialized if not present in loaded data this.pluginState.pomodoroCurrentMode = this.pluginState.pomodoroCurrentMode || DEFAULT_PLUGIN_STATE_DATA.pomodoroCurrentMode; this.pluginState.pomodoroTimeLeftInSeconds = this.pluginState.pomodoroTimeLeftInSeconds || DEFAULT_PLUGIN_STATE_DATA.pomodoroTimeLeftInSeconds; @@ -395,7 +396,7 @@ export default class SpaceforgePlugin extends Plugin { this.pluginState.pomodoroIsRunning = typeof this.pluginState.pomodoroIsRunning === 'boolean' ? this.pluginState.pomodoroIsRunning : DEFAULT_PLUGIN_STATE_DATA.pomodoroIsRunning; - if (this.pluginState.schedules) { + if (this.pluginState.schedules) { // Data migration for schedulingAlgorithm for (const path in this.pluginState.schedules) { if (Object.prototype.hasOwnProperty.call(this.pluginState.schedules, path)) { @@ -403,7 +404,7 @@ export default class SpaceforgePlugin extends Plugin { if (!schedule.schedulingAlgorithm) { schedule.schedulingAlgorithm = 'sm2'; // Default to SM-2 for old schedules // Ensure FSRS-specific data is undefined for these migrated SM-2 cards - schedule.fsrsData = undefined; + schedule.fsrsData = undefined; // Ensure SM-2 specific 'scheduleCategory' has a default if missing if (!schedule.scheduleCategory) { schedule.scheduleCategory = this.settings.useInitialSchedule ? 'initial' : 'spaced'; @@ -428,10 +429,10 @@ export default class SpaceforgePlugin extends Plugin { this.mcqService.mcqSessions = this.pluginState.mcqSessions || {}; this.reviewScheduleService.customNoteOrder = this.pluginState.customNoteOrder || []; this.reviewScheduleService.lastLinkAnalysisTimestamp = typeof this.pluginState.lastLinkAnalysisTimestamp === 'number' ? this.pluginState.lastLinkAnalysisTimestamp : null; - + } catch (error) { console.error("Spaceforge: Error loading data:", error); - + // SIMPLE RECOVERY: Try localStorage backup once, then use defaults try { const backupData = await this.app.loadLocalStorage('spaceforge-backup'); @@ -440,7 +441,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); + new Notice("Spaceforge: Recovered data from backup", 5000); // eslint-disable-line obsidianmd/ui/sentence-case } else { throw new Error("Invalid backup format"); } @@ -451,9 +452,9 @@ 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); + new Notice("Spaceforge: Error loading data, using defaults to prevent crash", 5000); // eslint-disable-line obsidianmd/ui/sentence-case } - + // Repopulate services this.reviewScheduleService.schedules = this.pluginState.schedules || {}; this.reviewHistoryService.history = this.pluginState.history || []; @@ -462,7 +463,7 @@ export default class SpaceforgePlugin extends Plugin { this.mcqService.mcqSessions = this.pluginState.mcqSessions || {}; this.reviewScheduleService.customNoteOrder = this.pluginState.customNoteOrder || []; this.reviewScheduleService.lastLinkAnalysisTimestamp = this.pluginState.lastLinkAnalysisTimestamp ?? null; - + if (this.reviewScheduleService) { this.reviewScheduleService.updateAlgorithmServicesForSettingsChange(); } @@ -483,7 +484,7 @@ export default class SpaceforgePlugin extends Plugin { if (!this.pluginState) { this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA }; } - + const currentPluginState: PluginStateData = { schedules: this.reviewScheduleService.schedules || {}, history: this.reviewHistoryService.history || [], @@ -513,7 +514,7 @@ export default class SpaceforgePlugin extends Plugin { pluginState: this.pluginState, }; - const effectiveSavePath = await this._getEffectiveDataPath(); + const effectiveSavePath = this._getEffectiveDataPath(); const defaultPluginDataPath = this.app.vault.configDir + `/plugins/${this.manifest.id}/data.json`; if (effectiveSavePath) { @@ -524,7 +525,7 @@ export default class SpaceforgePlugin extends Plugin { await this.app.vault.createFolder(dirPathOnly); new Notice(`Spaceforge: Created directory for custom data: ${dirPathOnly}`, 3000); } - + // SIMPLE SAVE: Use Obsidian's standard file operations const file = this.app.vault.getAbstractFileByPath(effectiveSavePath); if (file instanceof TFile) { @@ -541,16 +542,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); + new Notice(`Spaceforge: Successfully saved to custom path, original data file kept as backup for safety`, 5000); // eslint-disable-line obsidianmd/ui/sentence-case } } 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); - } catch (fallbackError) { - new Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations.`, 10000); + 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 } } } else { @@ -558,12 +559,12 @@ export default class SpaceforgePlugin extends Plugin { await this.saveData(dataToSave); // Plugin's internal save // new Notice(`Spaceforge: Data saved to default plugin storage.`, 3000); // Removed notice } - + // 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); + } catch (_error) { + new Notice("Error saving Spaceforge data, check console for details", 5000); // eslint-disable-line obsidianmd/ui/sentence-case } } @@ -571,7 +572,7 @@ export default class SpaceforgePlugin extends Plugin { const existingLeaves = this.app.workspace.getLeavesOfType('spaceforge-review-schedule'); if (existingLeaves.length > 0) { // If a sidebar view already exists, reveal the first one found - this.app.workspace.revealLeaf(existingLeaves[0]); + void this.app.workspace.revealLeaf(existingLeaves[0]); } else { // If no sidebar view exists, create a new one const leaf = this.app.workspace.getRightLeaf(false); // Default to right leaf if creating new @@ -580,61 +581,61 @@ export default class SpaceforgePlugin extends Plugin { type: 'spaceforge-review-schedule', active: true, }); - this.app.workspace.revealLeaf(leaf); // Reveal the newly created leaf + void this.app.workspace.revealLeaf(leaf); // Reveal the newly created leaf } else { - new Notice("Spaceforge: Could not open sidebar view."); + new Notice("Spaceforge: Could not open sidebar view"); // eslint-disable-line obsidianmd/ui/sentence-case } } } addCommands(): void { this.addCommand({ - id: 'spaceforge-next-review-note', - name: 'Next Review Note', + id: 'next-review-note', + name: 'Next review note', callback: () => { - this.navigationController.navigateToNextNote(); + void this.navigationController.navigateToNextNote(); }, }); this.addCommand({ - id: 'spaceforge-previous-review-note', - name: 'Previous Review Note', + id: 'previous-review-note', + name: 'Previous review note', callback: () => { - this.navigationController.navigateToPreviousNote(); + void this.navigationController.navigateToPreviousNote(); }, }); this.addCommand({ - id: 'spaceforge-review-current-note', - name: 'Review Current Note', + id: 'review-current-note', + name: 'Review current note', callback: () => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile && activeFile instanceof TFile && activeFile.extension === 'md') { - this.reviewController.reviewNote(activeFile.path); + void this.reviewController.reviewNote(activeFile.path); } else { - new Notice('No active markdown file to review.'); + new Notice('No active markdown file to review'); // eslint-disable-line obsidianmd/ui/sentence-case } }, }); this.addCommand({ - id: 'spaceforge-add-current-note-to-review', - name: 'Add Current Note to Review Schedule', + id: 'add-current-note-to-review', + name: 'Add current note to review schedule', callback: async () => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile && activeFile instanceof TFile && activeFile.extension === 'md') { await this.reviewScheduleService.scheduleNoteForReview(activeFile.path); await this.savePluginData(); - new Notice(`Added "${activeFile.path}" to review schedule.`); + new Notice(`Added "${activeFile.path}" to review schedule`); } else { - new Notice('No active markdown file to add to review.'); + new Notice('No active markdown file to add to review'); // eslint-disable-line obsidianmd/ui/sentence-case } }, }); this.addCommand({ - id: 'spaceforge-add-current-folder-to-review', - name: "Add Current Note's Folder to Review Schedule", + id: 'add-current-folder-to-review', + name: "Add current note's folder to review schedule", callback: async () => { const activeFile = this.app.workspace.getActiveFile(); if (activeFile && activeFile.parent && activeFile.parent instanceof TFolder) { @@ -642,7 +643,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."); + 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 } }, }); @@ -660,46 +661,46 @@ 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.'); + new Notice('MCQ generation service could not be initialized, check API provider settings in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case } } } - + private createMcqGenerationService(): IMCQGenerationService | undefined { switch (this.settings.mcqApiProvider) { case ApiProvider.OpenRouter: if (!this.settings.openRouterApiKey) { - new Notice('OpenRouter API key is not set in Spaceforge settings.'); + new Notice('OpenRouter API key is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case return undefined; } return new OpenRouterService(this); case ApiProvider.OpenAI: if (!this.settings.openaiApiKey) { - new Notice('OpenAI API key is not set in Spaceforge settings.'); + new Notice('OpenAI API key is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case 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.'); + new Notice('Ollama API URL or model is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case return undefined; } return new OllamaService(this); case ApiProvider.Gemini: if (!this.settings.geminiApiKey) { - new Notice('Gemini API key is not set in Spaceforge settings.'); + new Notice('Gemini API key is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case 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.'); + new Notice('Claude API key or model is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case 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.'); + new Notice('Together AI API key or model is not set in Spaceforge settings'); // eslint-disable-line obsidianmd/ui/sentence-case return undefined; } return new TogetherService(this); @@ -712,10 +713,10 @@ export default class SpaceforgePlugin extends Plugin { } } - async checkForDueNotes(): Promise { /* ... */ } + checkForDueNotes(): void { /* ... */ } private addStylesheet(): void { /* ... */ } - async exportPluginData(): Promise { /* ... */ } - async importPluginData(fileContent: string): Promise { /* ... */ } + exportPluginData(): void { /* ... */ } + importPluginData(_fileContent: string): void { /* ... */ } getSidebarView(): ReviewSidebarView | null { const leaves = this.app.workspace.getLeavesOfType('spaceforge-review-schedule'); diff --git a/models/review-schedule.ts b/models/review-schedule.ts index 6ab3d30..d0e8224 100644 --- a/models/review-schedule.ts +++ b/models/review-schedule.ts @@ -6,38 +6,38 @@ export interface ReviewSchedule { * Path to the note file - unique identifier */ path: string; - + /** * Timestamp of the last review */ lastReviewDate: number | null; - + /** * Timestamp of the next scheduled review */ nextReviewDate: number; - + /** * Ease factor - affects interval growth (higher = longer intervals) * In SM-2, this is a value typically starting at 2.5 (stored as 250 internally) */ ease: number; - + /** * Current interval in days */ interval: number; - + /** * Number of consecutive successful reviews (for backward compatibility) */ consecutive: number; - + /** * Total number of completed reviews (for backward compatibility) */ reviewCount: number; - + /** * SM-2 repetition count - used to determine interval calculation (n) * This is 1 for items in "learning" phase and increments for each successful review @@ -88,54 +88,31 @@ export enum ReviewResponse { * Complete blackout (0) - No recognition at all */ CompleteBlackout = 0, - + /** * Incorrect response (1) - Wrong answer but upon seeing the correct answer, it felt familiar */ IncorrectResponse = 1, - + /** * Incorrect response (2) - Wrong answer but upon seeing the correct answer, it felt very familiar */ IncorrectButFamiliar = 2, - + /** * Correct with difficulty (3) - Correct answer but required significant effort to recall */ CorrectWithDifficulty = 3, - + /** * Correct with hesitation (4) - Correct answer after some hesitation */ CorrectWithHesitation = 4, - + /** * Perfect recall (5) - Correct answer with no hesitation */ - PerfectRecall = 5, - - // Legacy response values explicitly mapped to SM-2 equivalents - // for backward compatibility - - /** - * Legacy "Hard" response - mapped to IncorrectResponse (1) - */ - Hard = 1, - - /** - * Legacy "Fair" response - mapped to CorrectWithDifficulty (3) - */ - Fair = 3, - - /** - * Legacy "Good" response - mapped to CorrectWithHesitation (4) - */ - Good = 4, - - /** - * Legacy "Perfect" response - mapped to PerfectRecall (5) - */ - Perfect = 5 + PerfectRecall = 5 } /** @@ -149,21 +126,8 @@ export function toSM2Quality(response: ReviewResponse): number { if (response >= 0 && response <= 5) { return response; } - - // Map legacy responses to SM-2 quality ratings - switch(response) { - case ReviewResponse.Hard: - return ReviewResponse.IncorrectResponse; - case ReviewResponse.Fair: - return ReviewResponse.CorrectWithDifficulty; - case ReviewResponse.Good: - return ReviewResponse.CorrectWithHesitation; - case ReviewResponse.Perfect: - return ReviewResponse.PerfectRecall; - default: - // Default to fair recall if unknown - return ReviewResponse.CorrectWithDifficulty; - } + // Default fallback + return ReviewResponse.CorrectWithDifficulty; } /** @@ -174,27 +138,27 @@ export interface ReviewHistoryItem { * Path to the note file */ path: string; - + /** * Timestamp of the review */ timestamp: number; - + /** * User's response during review */ response: ReviewResponse; - + /** * Interval at the time of review */ interval: number; - + /** * Ease at the time of review */ ease: number; - + /** * Whether this review was explicitly skipped by the user */ diff --git a/models/review-session.ts b/models/review-session.ts index 07fe6bd..e8f0dce 100644 --- a/models/review-session.ts +++ b/models/review-session.ts @@ -66,7 +66,7 @@ export interface ReviewSessionStore { * @param prefix Prefix for the ID * @returns Unique ID */ -export function generateSessionId(prefix: string = 'session'): string { +export function generateSessionId(prefix = 'session'): string { return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } diff --git a/note.md b/note.md deleted file mode 100644 index f8fadfe..0000000 --- a/note.md +++ /dev/null @@ -1,3 +0,0 @@ -bug postpone all postpones all notes not just one note. from that specific days on entry. -change due date red color to something more subtle - diff --git a/package-lock.json b/package-lock.json index 3bccc0d..0d20686 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,15 +13,20 @@ "ts-fsrs": "^4.7.1" }, "devDependencies": { + "@eslint/js": "^9.39.1", "@types/fs-extra": "^11.0.4", "@types/node": "^16.11.6", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "builtin-modules": "3.3.0", "esbuild": "^0.17.19", + "eslint": "^8.57.1", + "eslint-plugin-obsidianmd": "^0.1.9", + "globals": "^16.5.0", "obsidian": "latest", "tslib": "2.4.0", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "typescript-eslint": "^8.48.1" } }, "node_modules/@codemirror/state": { @@ -36,14 +41,15 @@ } }, "node_modules/@codemirror/view": { - "version": "6.36.5", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.5.tgz", - "integrity": "sha512-cd+FZEUlu3GQCYnguYm3EkhJ8KJVisqqUsCOKedBoAt/d9c76JUUap6U0UrpElln5k6VyrEOYliMuDAKIeDQLg==", + "version": "6.38.8", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", + "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "dev": true, "license": "MIT", "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", + "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } @@ -423,12 +429,11 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.1.tgz", - "integrity": "sha512-KTsJMmobmbrFLe3LDh0PC2FXpcSYJt/MLjlkh/9LEnmKYLSYmT/0EW9JWANjeoemiuZrmogti0tW5Ch+qNUYDw==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "eslint-visitor-keys": "^3.4.3" }, @@ -448,18 +453,57 @@ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -478,15 +522,98 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/json": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/json/-/json-0.14.0.tgz", + "integrity": "sha512-rvR/EZtvUG3p9uqrSmcDJPYSH7atmWr0RnFWN6m917MAPx82+zQgPUmDu0whPFG6XTyM0vB/hR6c1Q63OaYtCQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/core": "^0.17.0", + "@eslint/plugin-kit": "^0.4.1", + "@humanwhocodes/momoa": "^3.3.10", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/config-array": { @@ -496,7 +623,6 @@ "deprecated": "Use @eslint/config-array instead", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", @@ -512,7 +638,6 @@ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=12.22" }, @@ -521,14 +646,38 @@ "url": "https://github.com/sponsors/nzakas" } }, + "node_modules/@humanwhocodes/momoa": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-3.3.10.tgz", + "integrity": "sha512-KWiFQpSAqEIyrTXko3hFNLeQvSK8zXlJQzhhxsyVn58WFRYXST99b3Nqnu+ttOtjds2Pl2grUHGpe2NzhPynuQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@humanwhocodes/object-schema": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", "dev": true, - "license": "BSD-3-Clause", - "peer": true + "license": "BSD-3-Clause" + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@marijn/find-cluster-break": { "version": "1.0.2", @@ -538,6 +687,44 @@ "license": "MIT", "peer": true }, + "node_modules/@microsoft/eslint-plugin-sdl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/eslint-plugin-sdl/-/eslint-plugin-sdl-1.1.0.tgz", + "integrity": "sha512-dxdNHOemLnBhfY3eByrujX9KyLigcNtW8sU+axzWv5nLGcsSBeKW2YYyTpfPo1hV8YPOmIGnfA4fZHyKVtWqBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-plugin-n": "17.10.3", + "eslint-plugin-react": "7.37.3", + "eslint-plugin-security": "1.4.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "eslint": "^9" + } + }, + "node_modules/@microsoft/eslint-plugin-sdl/node_modules/eslint-plugin-security": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-1.4.0.tgz", + "integrity": "sha512-xlS7P2PLMXeqfhyf3NpqbvbnW04kN8M9NtmhpR3XGyOvt/vNKS7XPXT5EDbwKW9vCjWH4PpfQvgD/+JgN0VJKA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^1.1.0" + } + }, + "node_modules/@microsoft/eslint-plugin-sdl/node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ret": "~0.1.10" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -576,6 +763,26 @@ "node": ">= 8" } }, + "node_modules/@pkgr/core": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.2.tgz", + "integrity": "sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/codemirror": { "version": "5.60.8", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", @@ -586,6 +793,17 @@ "@types/tern": "*" } }, + "node_modules/@types/eslint": { + "version": "8.56.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", + "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -611,6 +829,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/jsonfile": { "version": "6.1.4", "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", @@ -639,120 +864,160 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.29.0.tgz", - "integrity": "sha512-kgTsISt9pM53yRFQmLZ4npj99yGl3x3Pl7z4eA66OuTzAGC4bQB5H5fuLwPnqTKU3yyrrg4MIhjF17UYnL4c0w==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "5.29.0", - "@typescript-eslint/type-utils": "5.29.0", - "@typescript-eslint/utils": "5.29.0", - "debug": "^4.3.4", - "functional-red-black-tree": "^1.0.1", - "ignore": "^5.2.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/type-utils": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@typescript-eslint/parser": "^8.48.1", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.29.0.tgz", - "integrity": "sha512-ruKWTv+x0OOxbzIw9nW5oWlUopvP/IQDjB5ZqmTglLIoDTctLlAJpAQFpNPJP/ZI7hTT9sARBosEfaKbcFuECw==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "5.29.0", - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/typescript-estree": "5.29.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.48.1", + "@typescript-eslint/types": "^8.48.1", + "debug": "^4.3.4" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.29.0.tgz", - "integrity": "sha512-etbXUT0FygFi2ihcxDZjz21LtC+Eps9V2xVx09zFoN44RRHPrkMflidGMI+2dUs821zR1tDS6Oc9IXxIjOUZwA==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/visitor-keys": "5.29.0" + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.29.0.tgz", - "integrity": "sha512-JK6bAaaiJozbox3K220VRfCzLa9n0ib/J+FHIwnaV3Enw/TO267qe0pM1b1QrrEuy6xun374XEAsRlA86JJnyg==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/utils": "5.29.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.29.0.tgz", - "integrity": "sha512-X99VbqvAXOMdVyfFmksMy3u8p8yoRGITgU1joBJPzeYa0rhdf5ok9S56/itRoUSh99fiDoMtarSIJXo7H/SnOg==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", "dev": true, "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -760,74 +1025,112 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.29.0.tgz", - "integrity": "sha512-mQvSUJ/JjGBdvo+1LwC+GY2XmSYjK1nAaVw2emp/E61wEVYEyibRHCqm1I1vEKbXCpUKuW4G7u9ZCaZhJbLoNQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/visitor-keys": "5.29.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.29.0.tgz", - "integrity": "sha512-3Eos6uP1nyLOBayc/VUdKZikV90HahXE5Dx9L5YlSd/7ylQPXhLk1BYb29SDgnBnTp+jmSZUU0QxUiyHgW4p7A==", + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", "dev": true, "license": "MIT", "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.29.0", - "@typescript-eslint/types": "5.29.0", - "@typescript-eslint/typescript-estree": "5.29.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" + "@typescript-eslint/project-service": "8.48.1", + "@typescript-eslint/tsconfig-utils": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/visitor-keys": "8.48.1", + "debug": "^4.3.4", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.1.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.29.0.tgz", - "integrity": "sha512-Hpb/mCWsjILvikMQoZIE3voc9wtQcS0A9FUw3h8bhr9UxBdtI/tw1ZDZUOXHXLOVMedKCH5NxyzATwnU78bWCQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.29.0", - "eslint-visitor-keys": "^3.3.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.48.1", + "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.48.1", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/@ungap/structured-clone": { @@ -835,16 +1138,14 @@ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -858,7 +1159,6 @@ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -869,7 +1169,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -887,7 +1186,6 @@ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -898,7 +1196,6 @@ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -914,17 +1211,192 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "license": "Python-2.0", - "peer": true + "license": "Python-2.0" }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/balanced-match": { @@ -932,8 +1404,7 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/brace-expansion": { "version": "1.1.11", @@ -941,25 +1412,11 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -973,13 +1430,62 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -990,7 +1496,6 @@ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1008,7 +1513,6 @@ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1021,14 +1525,20 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true, + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "dev": true, "license": "MIT", "peer": true }, @@ -1038,7 +1548,6 @@ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1048,6 +1557,60 @@ "node": ">= 8" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -1071,20 +1634,42 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { - "path-type": "^4.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/doctrine": { @@ -1093,7 +1678,6 @@ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "esutils": "^2.0.2" }, @@ -1101,6 +1685,222 @@ "node": ">=6.0.0" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", + "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.17.19", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", @@ -1145,7 +1945,6 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, @@ -1160,7 +1959,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -1211,47 +2009,648 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/eslint-compat-utils": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.5.1.tgz", + "integrity": "sha512-3z3vFexKIEnjHE3zCMRo6fn/e44U7T1khUjg+Hp0ZQMCigh28rALD0nPFBcGZuiLC5rLZa2ubQHDRln09JfU2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-depend": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-depend/-/eslint-plugin-depend-1.3.1.tgz", + "integrity": "sha512-1uo2rFAr9vzNrCYdp7IBZRB54LiyVxfaIso0R6/QV3t6Dax6DTbW/EV2Hktf0f4UtmGHK8UyzJWI382pwW04jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "empathic": "^2.0.0", + "module-replacements": "^2.8.0", + "semver": "^7.6.3" + } + }, + "node_modules/eslint-plugin-es-x": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.8.0.tgz", + "integrity": "sha512-7Ds8+wAAoV3T+LAKeu39Y5BzXCrGKrcISfgKEqTS4BDN8SFEDQd0S43jiQ8vIa3wUKD07qitZdfzlenSi8/0qQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/ota-meshi", + "https://opencollective.com/eslint" + ], + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.1.2", + "@eslint-community/regexpp": "^4.11.0", + "eslint-compat-utils": "^0.5.1" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": ">=8" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-json-schema-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-json-schema-validator/-/eslint-plugin-json-schema-validator-5.1.0.tgz", + "integrity": "sha512-ZmVyxRIjm58oqe2kTuy90PpmZPrrKvOjRPXKzq8WCgRgAkidCgm5X8domL2KSfadZ3QFAmifMgGTcVNhZ5ez2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.3.0", + "ajv": "^8.0.0", + "debug": "^4.3.1", + "eslint-compat-utils": "^0.5.0", + "json-schema-migrate": "^2.0.0", + "jsonc-eslint-parser": "^2.0.0", + "minimatch": "^8.0.0", + "synckit": "^0.9.0", + "toml-eslint-parser": "^0.9.0", + "tunnel-agent": "^0.6.0", + "yaml-eslint-parser": "^1.0.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint-plugin-json-schema-validator/node_modules/minimatch": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", + "integrity": "sha512-W0Wvr9HyFXZRGIDgCicunpQ299OKXs9RgZfaukz4qAW/pJhcpUfupc9c+OObPOFueNy8VSrZgEmDtk6Kh4WzDA==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-n": { + "version": "17.10.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-17.10.3.tgz", + "integrity": "sha512-ySZBfKe49nQZWR1yFaA0v/GsH6Fgp8ah6XV0WDz6CN8WO0ek4McMzb7A2xnf4DCYV43frjCygvb9f/wx7UUxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "enhanced-resolve": "^5.17.0", + "eslint-plugin-es-x": "^7.5.0", + "get-tsconfig": "^4.7.0", + "globals": "^15.8.0", + "ignore": "^5.2.4", + "minimatch": "^9.0.5", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": ">=8.23.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-plugin-n/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-n/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-plugin-obsidianmd": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-plugin-obsidianmd/-/eslint-plugin-obsidianmd-0.1.9.tgz", + "integrity": "sha512-/gyo5vky3Y7re4BtT/8MQbHU5Wes4o6VRqas3YmXE7aTCnMsdV0kfzV1GDXJN9Hrsc9UQPoeKUMiapKL0aGE4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@microsoft/eslint-plugin-sdl": "^1.1.0", + "@types/eslint": "8.56.2", + "@types/node": "20.12.12", + "eslint": ">=9.0.0 <10.0.0", + "eslint-plugin-depend": "1.3.1", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-json-schema-validator": "5.1.0", + "eslint-plugin-security": "2.1.1", + "globals": "14.0.0", + "obsidian": "1.8.7", + "typescript": "5.4.5" + }, + "bin": { + "eslint-plugin-obsidian": "dist/lib/index.js" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@eslint/js": "^9.30.1", + "@eslint/json": "0.14.0", + "eslint": ">=9.0.0 <10.0.0", + "obsidian": "1.8.7", + "typescript-eslint": "^8.35.1" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/@types/node": { + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, - "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/eslint-plugin-obsidianmd/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=10" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-obsidianmd/node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.3.tgz", + "integrity": "sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-security": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-security/-/eslint-plugin-security-2.1.1.tgz", + "integrity": "sha512-7cspIGj7WTfR3EhaILzAPcfCo5R9FbeWvbgsPYWivSurTBKW88VQxtP3c4aWMG9Hz/GfJlJVdXEJ3c8LqS+u2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-regex": "^2.1.1" } }, "node_modules/eslint-visitor-keys": { @@ -1267,13 +2666,22 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint/node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, "node_modules/eslint/node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -1291,18 +2699,32 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", @@ -1321,7 +2743,6 @@ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", - "peer": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -1335,7 +2756,6 @@ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=4.0" } @@ -1363,23 +2783,12 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -1389,54 +2798,38 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } + "license": "MIT" }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/fastq": { "version": "1.19.1", @@ -1448,13 +2841,30 @@ "reusify": "^1.0.4" } }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flat-cache": "^3.0.4" }, @@ -1462,26 +2872,12 @@ "node": "^10.12.0 || >=12.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -1499,7 +2895,6 @@ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", @@ -1514,8 +2909,23 @@ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/fs-extra": { "version": "11.3.0", @@ -1536,15 +2946,128 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, - "node_modules/functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, - "license": "MIT" + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } }, "node_modules/glob": { "version": "7.2.3", @@ -1553,7 +3076,6 @@ "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -1575,7 +3097,6 @@ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -1584,41 +3105,46 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", "dev": true, "license": "MIT", "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "define-properties": "^1.2.1", + "gopd": "^1.0.1" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/graceful-fs": { @@ -1632,8 +3158,20 @@ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true, + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/has-flag": { "version": "4.0.0", @@ -1641,11 +3179,81 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1662,7 +3270,6 @@ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -1680,7 +3287,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.8.19" } @@ -1692,7 +3298,6 @@ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1703,8 +3308,157 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/is-extglob": { "version": "2.1.1", @@ -1716,6 +3470,42 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1729,14 +3519,47 @@ "node": ">=0.10.0" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", "dev": true, "license": "MIT", "engines": { - "node": ">=0.12.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-path-inside": { @@ -1745,26 +3568,200 @@ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "argparse": "^2.0.1" }, @@ -1777,24 +3774,87 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-migrate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/json-schema-migrate/-/json-schema-migrate-2.0.0.tgz", + "integrity": "sha512-r38SVTtojDRp4eD6WsCqiE0eNDt4v1WalBXb9cyZYw9ai5cGtBwzRNWjHzJl38w6TxFkXAIA7h+fyX3tnrAFhQ==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "ajv": "^8.0.0" + } + }, + "node_modules/json-schema-migrate/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/json-schema-migrate/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/jsonc-eslint-parser": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jsonc-eslint-parser/-/jsonc-eslint-parser-2.4.2.tgz", + "integrity": "sha512-1e4qoRgnn448pRuMvKGsFFymUCquZV0mpGgOyIKNgD3JVDTsVJyRBGH/Fm0tBb8WsWGgmB1mDe6/yJMQM37DUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.5.0", + "eslint-visitor-keys": "^3.0.0", + "espree": "^9.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } }, "node_modules/jsonfile": { "version": "6.1.0", @@ -1808,13 +3868,28 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "json-buffer": "3.0.1" } @@ -1825,7 +3900,6 @@ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -1840,7 +3914,6 @@ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -1856,31 +3929,29 @@ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", "dev": true, "license": "MIT", "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" + "js-tokens": "^3.0.0 || ^4.0.0" }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">= 0.4" } }, "node_modules/minimatch": { @@ -1889,7 +3960,6 @@ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "brace-expansion": "^1.1.7" }, @@ -1897,6 +3967,23 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/module-replacements": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/module-replacements/-/module-replacements-2.10.1.tgz", + "integrity": "sha512-qkKuLpMHDqRSM676OPL7HUpCiiP3NSxgf8NNR1ga2h/iJLNKTsOSjMEwrcT85DMSti2vmOqxknOVBGWj6H6etQ==", + "dev": true, + "license": "MIT" + }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -1919,8 +4006,130 @@ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/obsidian": { "version": "1.8.7", @@ -1943,7 +4152,6 @@ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "wrappy": "1" } @@ -1954,7 +4162,6 @@ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", @@ -1967,13 +4174,30 @@ "node": ">= 0.8.0" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -1990,7 +4214,6 @@ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -2007,7 +4230,6 @@ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "callsites": "^3.0.0" }, @@ -2021,7 +4243,6 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } @@ -2032,7 +4253,6 @@ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2043,52 +4263,68 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -2114,17 +4350,96 @@ ], "license": "MIT" }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/resolve-from": { @@ -2133,11 +4448,30 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12" + } + }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2156,7 +4490,6 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -2191,6 +4524,92 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-2.1.1.tgz", + "integrity": "sha512-rx+x8AMzKb5Q5lQ95Zoi6ZbJqwCLkqi3XuJXp5P3rT8OEc6sZCJG5AE5dU3lsgRr/F4Bs31jSlVN+j5KrsGu9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "regexp-tree": "~0.1.1" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -2204,13 +4623,61 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -2224,19 +4691,196 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" } }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, "engines": { - "node": ">=8" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/strip-ansi": { @@ -2245,7 +4889,6 @@ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -2253,13 +4896,22 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=8" }, @@ -2268,9 +4920,9 @@ } }, "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", "dev": true, "license": "MIT", "peer": true @@ -2281,7 +4933,6 @@ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -2289,25 +4940,108 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.3.tgz", + "integrity": "sha512-JJoOEKTfL1urb1mDoEblhD9NhEbWmq9jHEMEnxoC4ujUaZ4itA8vKgwkFAyNClgxplLi9tsUKX+EduK0p/l7sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/unts" + } + }, + "node_modules/synckit/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", "dependencies": { - "is-number": "^7.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toml-eslint-parser": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/toml-eslint-parser/-/toml-eslint-parser-0.9.3.tgz", + "integrity": "sha512-moYoCvkNUAPCxSW9jmHmRElhm4tVJpHL8ItC/+uYD0EpPSFXbck7yREz9tNdJVTSpHVod8+HoipcpbQ0oE6gsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" } }, "node_modules/ts-fsrs": { @@ -2319,6 +5053,19 @@ "node": ">=18.0.0" } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, "node_modules/tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", @@ -2326,36 +5073,25 @@ "dev": true, "license": "0BSD" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "tslib": "^1.8.1" + "safe-buffer": "^5.0.1" }, "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + "node": "*" } }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true, - "license": "0BSD" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -2369,7 +5105,6 @@ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, "license": "(MIT OR CC0-1.0)", - "peer": true, "engines": { "node": ">=10" }, @@ -2377,10 +5112,88 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { - "version": "4.7.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", - "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2391,6 +5204,56 @@ "node": ">=4.2.0" } }, + "node_modules/typescript-eslint": { + "version": "8.48.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.48.1.tgz", + "integrity": "sha512-FbOKN1fqNoXp1hIl5KYpObVrp0mCn+CLgn479nmu2IsRMrx2vyv74MmsBLVlhg8qVwNFGbXSp8fh1zp8pEoC2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.48.1", + "@typescript-eslint/parser": "8.48.1", + "@typescript-eslint/typescript-estree": "8.48.1", + "@typescript-eslint/utils": "8.48.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", @@ -2406,7 +5269,6 @@ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "punycode": "^2.1.0" } @@ -2425,7 +5287,6 @@ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, "license": "ISC", - "peer": true, "dependencies": { "isexe": "^2.0.0" }, @@ -2436,13 +5297,101 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -2452,8 +5401,40 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "dev": true, "license": "ISC", - "peer": true + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yaml-eslint-parser": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/yaml-eslint-parser/-/yaml-eslint-parser-1.3.2.tgz", + "integrity": "sha512-odxVsHAkZYYglR30aPYRY4nUGJnoJ2y1ww2HDvZALo0BDETv9kWbi16J52eHs+PWRNmF4ub6nZqfVOeesOvntg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.0.0", + "yaml": "^2.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ota-meshi" + } }, "node_modules/yocto-queue": { "version": "0.1.0", @@ -2461,7 +5442,6 @@ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 61838bd..a95251b 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json", - "install-plugin": "node install.js" + "install-plugin": "node install.js", + "lint": "ESLINT_USE_FLAT_CONFIG=true eslint .", + "typecheck": "tsc -noEmit -skipLibCheck" }, "keywords": [ "obsidian", @@ -17,18 +19,23 @@ "author": "Spaceforge Team", "license": "MIT", "devDependencies": { - "@types/node": "^16.11.6", + "@eslint/js": "^9.39.1", "@types/fs-extra": "^11.0.4", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", + "@types/node": "^16.11.6", + "@typescript-eslint/eslint-plugin": "^8.48.1", + "@typescript-eslint/parser": "^8.48.1", "builtin-modules": "3.3.0", "esbuild": "^0.17.19", + "eslint": "^8.57.1", + "eslint-plugin-obsidianmd": "^0.1.9", + "globals": "^16.5.0", "obsidian": "latest", "tslib": "2.4.0", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "typescript-eslint": "^8.48.1" }, "dependencies": { "fs-extra": "^11.1.1", "ts-fsrs": "^4.7.1" } -} +} \ No newline at end of file diff --git a/services/calendar-event-service.ts b/services/calendar-event-service.ts index e9926f2..daff9fe 100644 --- a/services/calendar-event-service.ts +++ b/services/calendar-event-service.ts @@ -107,7 +107,7 @@ export class CalendarEventService { const eventDate = DateUtils.startOfDay(new Date(event.date)); const start = DateUtils.startOfDay(new Date(startDate)); const end = DateUtils.startOfDay(new Date(endDate)); - + return eventDate >= start && eventDate <= end; }); } @@ -164,7 +164,7 @@ export class CalendarEventService { * @param fromDate Optional start date (defaults to today) * @returns Array of upcoming events */ - getUpcomingEvents(daysAhead: number = 7, fromDate: Date = new Date()): UpcomingEvent[] { + getUpcomingEvents(daysAhead = 7, fromDate: Date = new Date()): UpcomingEvent[] { const today = DateUtils.startOfDay(fromDate); const endDate = DateUtils.addDays(today, daysAhead); const eventsInRange = this.getEventsInRange(today, endDate); @@ -173,7 +173,7 @@ export class CalendarEventService { .map(event => { const eventDate = DateUtils.startOfDay(new Date(event.date)); const daysUntil = Math.floor((eventDate - today) / (24 * 60 * 60 * 1000)); - + return { event, daysUntil, @@ -186,16 +186,16 @@ export class CalendarEventService { if (a.daysUntil !== b.daysUntil) { return a.daysUntil - b.daysUntil; } - + // If same day, sort by time (all-day events first) if (a.event.isAllDay && !b.event.isAllDay) return -1; if (!a.event.isAllDay && b.event.isAllDay) return 1; - + // If both have times, sort by time if (a.event.time && b.event.time) { return a.event.time.localeCompare(b.event.time); } - + return 0; }); } @@ -219,8 +219,8 @@ export class CalendarEventService { const eventDate = DateUtils.startOfDay(new Date(event.date)); const recurrenceEnd = event.recurrenceEndDate ? DateUtils.startOfDay(new Date(event.recurrenceEndDate)) : end; - let currentDate = new Date(eventDate); - + const currentDate = new Date(eventDate); + while (currentDate.getTime() <= Math.min(end, recurrenceEnd)) { if (currentDate.getTime() >= start) { const instance: CalendarEvent = { @@ -269,7 +269,7 @@ export class CalendarEventService { */ searchEvents(query: string): CalendarEvent[] { const lowerQuery = query.toLowerCase(); - return this.getAllEvents().filter(event => + return this.getAllEvents().filter(event => event.title.toLowerCase().includes(lowerQuery) || (event.description && event.description.toLowerCase().includes(lowerQuery)) ); @@ -294,14 +294,14 @@ export class CalendarEventService { try { const events: CalendarEvent[] = JSON.parse(json); let importedCount = 0; - + events.forEach(event => { if (event.id && event.title && event.date) { this.events.set(event.id, event); importedCount++; } }); - + return importedCount; } catch (error) { console.error('Failed to import events:', error); @@ -315,7 +315,7 @@ export class CalendarEventService { * @returns Unique ID string */ private generateId(): string { - return `event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; + return `event-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`; } /** @@ -344,7 +344,7 @@ export class CalendarEventService { [EventCategory.Social]: '#FF69B4', [EventCategory.Other]: '#95A5A6' }; - + return colors[category] || colors[EventCategory.Other]; } } \ No newline at end of file diff --git a/services/fsrs-schedule-service.ts b/services/fsrs-schedule-service.ts index 6d3d50c..873317b 100644 --- a/services/fsrs-schedule-service.ts +++ b/services/fsrs-schedule-service.ts @@ -1,7 +1,7 @@ import { FSRS, FSRSParameters, Card as FsrsLibCard, State as FsrsState, Rating as TsFsrsRating, createEmptyCard as createFsrsLibEmptyCard, ReviewLog as FsrsReviewLog } from 'ts-fsrs'; import { SpaceforgeSettings } from '../models/settings'; import { ReviewSchedule, FsrsRating } from '../models/review-schedule'; -import { DateUtils } from '../utils/dates'; + export class FsrsScheduleService { private fsrsInstance: FSRS; @@ -56,7 +56,7 @@ export class FsrsScheduleService { last_review: fsrsData.last_review ? new Date(fsrsData.last_review) : undefined, }; } - + private mapFsrsLibRatingToTsFsrsRating(rating: FsrsRating): TsFsrsRating { switch (rating) { case FsrsRating.Again: return TsFsrsRating.Again; @@ -76,12 +76,12 @@ export class FsrsScheduleService { const tsFsrsRating = this.mapFsrsLibRatingToTsFsrsRating(rating); const schedulingResult = this.fsrsInstance.repeat(fsrsLibCardToReview, reviewDateTime); - + // Ensure that tsFsrsRating is a valid key for schedulingResult // TsFsrsRating.Manual is a valid enum member but not a result of our mapping. // The keys of schedulingResult are Again, Hard, Good, Easy. const validRatingKey = tsFsrsRating as (TsFsrsRating.Again | TsFsrsRating.Hard | TsFsrsRating.Good | TsFsrsRating.Easy); - + const resultCard = schedulingResult[validRatingKey].card; const resultLog = schedulingResult[validRatingKey].log; diff --git a/services/mcq-service.ts b/services/mcq-service.ts index fd279ea..628be13 100644 --- a/services/mcq-service.ts +++ b/services/mcq-service.ts @@ -68,7 +68,7 @@ export class MCQService { } return null; - } catch (error) { + } catch { return null; } } @@ -112,7 +112,8 @@ export class MCQService { } // Data saving is now handled by main.ts after this method returns - } catch (error) { + } catch { + // no-op } } @@ -151,6 +152,7 @@ export class MCQService { mcqSet.needsQuestionRegeneration = true; this.saveMCQSet(mcqSet); // Updates the in-memory reference; persistence handled by caller } else { + // no-op } } } diff --git a/services/pomodoro-service.ts b/services/pomodoro-service.ts index 2288312..3963d32 100644 --- a/services/pomodoro-service.ts +++ b/services/pomodoro-service.ts @@ -43,7 +43,7 @@ export class PomodoroService { this.state.pomodoroEndTimeMs = Date.now() + this.state.pomodoroTimeLeftInSeconds * 1000; this.startTimerInterval(); this.notifyUpdate(); - this.plugin.savePluginData(); + void this.plugin.savePluginData(); } public stop(): void { // Pause functionality @@ -61,7 +61,7 @@ export class PomodoroService { this.state.pomodoroEndTimeMs = null; this.notifyUpdate(); - this.plugin.savePluginData(); + void this.plugin.savePluginData(); } public resetCurrentSession(): void { @@ -70,9 +70,9 @@ export class PomodoroService { this.state.pomodoroEndTimeMs = null; // Clear end time on reset this.resetTimeForMode(this.state.pomodoroCurrentMode === 'idle' ? 'work' : this.state.pomodoroCurrentMode); this.notifyUpdate(); - this.plugin.savePluginData(); + void this.plugin.savePluginData(); } - + public skipSession(): void { this.stopTimerInterval(); this.state.pomodoroIsRunning = false; // Stop current timer before switching @@ -85,7 +85,7 @@ export class PomodoroService { this.startTimerInterval(); } this.notifyUpdate(); // handleTimerEnd also notifies, but an extra one here is fine - this.plugin.savePluginData(); + void this.plugin.savePluginData(); } public getFormattedTimeLeft(): string { @@ -100,14 +100,14 @@ export class PomodoroService { const currentShort = this.settings.pomodoroShortBreakDuration; const currentLong = this.settings.pomodoroLongBreakDuration; const currentSessions = this.settings.pomodoroSessionsUntilLongBreak; - + const userChangedSettings = work !== currentWork || short !== currentShort || long !== currentLong || sessions !== currentSessions; - + this.settings.pomodoroWorkDuration = work; this.settings.pomodoroShortBreakDuration = short; this.settings.pomodoroLongBreakDuration = long; this.settings.pomodoroSessionsUntilLongBreak = sessions; - + // If user manually changed settings, mark as modified and deactivate estimation if (userChangedSettings) { this.state.pomodoroUserHasModifiedSettings = true; @@ -115,13 +115,13 @@ export class PomodoroService { this.state.pomodoroEstimatedTotalCycles = null; this.state.pomodoroEstimatedWorkSessions = null; } - + // If current session is not running and its mode (which is an active timed mode) had its duration changed, update its timeLeft const activeModesForDurationUpdate: PomodoroMode[] = ['work', 'shortBreak', 'longBreak']; if (!this.state.pomodoroIsRunning && activeModesForDurationUpdate.includes(this.state.pomodoroCurrentMode)) { - this.resetTimeForMode(this.state.pomodoroCurrentMode); + this.resetTimeForMode(this.state.pomodoroCurrentMode); } - this.plugin.savePluginData(); // Save settings and potentially updated state + void this.plugin.savePluginData(); // Save settings and potentially updated state this.notifyUpdate(); } @@ -208,10 +208,10 @@ export class PomodoroService { this.state.pomodoroEstimatedTotalCycles = Math.ceil(pomodorosNeeded / sessionsUntilLongBreak); this.state.pomodoroEstimatedWorkSessions = pomodorosNeeded; this.state.pomodoroIsEstimationActive = true; - - this.plugin.savePluginData(); + + void this.plugin.savePluginData(); this.notifyUpdate(); - + // Return calculation results for UI display return { totalReadingTimeInSeconds, @@ -224,9 +224,9 @@ export class PomodoroService { /** * Get current cycle progress information */ - public getCycleProgress(): { - current: number; - total: number; + public getCycleProgress(): { + current: number; + total: number; workSessionsRemaining: number; totalWorkSessions: number; totalTimeMinutes: number; @@ -248,7 +248,7 @@ export class PomodoroService { let totalBreakTime = 0; let sessionsInCycle = 0; - + for (let i = 0; i < totalWorkSessions; i++) { sessionsInCycle++; if (i < totalWorkSessions - 1) { // Don't add break after last session @@ -280,7 +280,7 @@ export class PomodoroService { this.state.pomodoroEstimatedTotalCycles = null; this.state.pomodoroEstimatedWorkSessions = null; this.state.pomodoroUserHasModifiedSettings = false; - this.plugin.savePluginData(); + void this.plugin.savePluginData(); this.notifyUpdate(); } @@ -327,7 +327,7 @@ export class PomodoroService { } } - private handleTimerEnd(skipped: boolean = false): void { + private handleTimerEnd(skipped = false): void { this.stopTimerInterval(); // Stop the current interval // pomodoroIsRunning might still be true if we auto-transition to a new session @@ -351,7 +351,7 @@ export class PomodoroService { this.state.pomodoroSessionsCompletedInCycle = 0; // Reset for new cycle } } // If currentMode was 'idle' or another unhandled state, nextMode remains 'idle' as initialized. - + const wasRunning = this.state.pomodoroIsRunning; // Capture state before potentially changing it this.switchToMode(nextMode); // This resets timeLeft based on the new mode @@ -360,17 +360,17 @@ export class PomodoroService { // Calculate new end time based on the duration of the *new* mode this.state.pomodoroEndTimeMs = Date.now() + this.state.pomodoroTimeLeftInSeconds * 1000; if (!wasRunning) { // Only start interval if it wasn't already running (e.g., transitioning from idle) - this.startTimerInterval(); + this.startTimerInterval(); } else if (!this.timerInterval) { // Or if interval somehow stopped but should be running - this.startTimerInterval(); + this.startTimerInterval(); } } else { this.state.pomodoroIsRunning = false; this.state.pomodoroEndTimeMs = null; // No end time when idle this.stopTimerInterval(); // Ensure interval is stopped when idle } - - this.plugin.savePluginData(); // Save state changes + + void this.plugin.savePluginData(); // Save state changes this.notifyUpdate(); } @@ -414,7 +414,7 @@ export class PomodoroService { oscillator.start(); oscillator.stop(audioContext.currentTime + 0.5); // Play for 0.5 seconds - } catch (e) { /* handle error */ } + } catch { /* handle error */ } } private notifyUpdate(): void { @@ -458,8 +458,8 @@ export class PomodoroService { this.handleTimerEnd(true); // Treat as skipped to avoid sound, force transition logic } } else if (!this.state.pomodoroIsRunning) { - // If not running, ensure end time is null - this.state.pomodoroEndTimeMs = null; + // If not running, ensure end time is null + this.state.pomodoroEndTimeMs = null; } // If running but no end time (shouldn't happen), maybe log error or reset? // For now, assume state is consistent or gets corrected by start/stop. diff --git a/services/review-schedule-service.ts b/services/review-schedule-service.ts index 8fee01a..f7e76d5 100644 --- a/services/review-schedule-service.ts +++ b/services/review-schedule-service.ts @@ -1,4 +1,4 @@ -import { Notice, TFile, TFolder } from 'obsidian'; +import { Notice, TFile } from 'obsidian'; import { ReviewSchedule, ReviewResponse, toSM2Quality, FsrsRating } from '../models/review-schedule'; // Added FsrsRating import SpaceforgePlugin from '../main'; import { DateUtils } from '../utils/dates'; @@ -57,7 +57,7 @@ export class ReviewScheduleService { this.schedules = schedules; this.customNoteOrder = customNoteOrder; this.lastLinkAnalysisTimestamp = lastLinkAnalysisTimestamp; - this.history = history; + this.history = history; this.fsrsService = new FsrsScheduleService(this.plugin.settings); } @@ -72,18 +72,18 @@ export class ReviewScheduleService { * @param path Path to the note file * @param daysFromNow Days until first review (default: 0, same day) */ - async scheduleNoteForReview(path: string, daysFromNow: number = 0): Promise { + scheduleNoteForReview(path: string, daysFromNow = 0): void { // 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"); + new Notice("Only markdown files can be added to the review schedule."); // eslint-disable-line obsidianmd/ui/sentence-case return; } const now = Date.now(); const todayUTCStart = DateUtils.startOfUTCDay(new Date(now)); const defaultAlgorithm = this.plugin.settings.defaultSchedulingAlgorithm; - + let newSchedule: ReviewSchedule; if (defaultAlgorithm === 'fsrs') { @@ -98,7 +98,7 @@ export class ReviewScheduleService { fsrsData: fsrsData, // SM-2 fields can be undefined or default ease: this.plugin.settings.baseEase, // Keep a base for potential conversion - interval: 0, + interval: 0, consecutive: 0, repetitionCount: 0, scheduleCategory: undefined, // Not applicable to FSRS @@ -143,7 +143,7 @@ export class ReviewScheduleService { this.plugin.events.emit('sidebar-update'); } - new Notice(`Note added to review schedule`); + new Notice(`Note added to review schedule.`); } /** @@ -156,7 +156,7 @@ export class ReviewScheduleService { private isNoteDue(schedule: ReviewSchedule, effectiveReviewDate: number): boolean { const reviewDateObj = new Date(effectiveReviewDate); const effectiveUTCDayEnd = DateUtils.endOfUTCDay(reviewDateObj); - + // Note is due if its nextReviewDate is on or before the effective date // Allow notes due today or earlier to be reviewed return schedule.nextReviewDate <= effectiveUTCDayEnd; @@ -171,15 +171,15 @@ export class ReviewScheduleService { * @param currentReviewDate Optional timestamp for the current review date (simulated or actual) * @returns true if the review was recorded, false if it was just a preview */ - async recordReview(path: string, response: ReviewResponse | FsrsRating, isSkipped: boolean = false, currentReviewDate?: number): Promise { + recordReview(path: string, response: ReviewResponse | FsrsRating, isSkipped = false, currentReviewDate?: number): boolean { const schedule = this.schedules[path]; if (!schedule) return false; const effectiveReviewDate = currentReviewDate || Date.now(); - + // Check if the note is actually due for review const isDue = this.isNoteDue(schedule, effectiveReviewDate); - + if (!isDue) { // Note is not due, this is just a preview - don't record anything return false; @@ -221,50 +221,50 @@ export class ReviewScheduleService { if (schedule.schedulingAlgorithm === 'fsrs') { if (!schedule.fsrsData) { // Should not happen if card is properly initialized - // FSRS service expects a Date object for the review time. - // reviewDateObj (created from effectiveReviewDate) is correct here. - schedule.fsrsData = this.fsrsService.createNewFsrsCardData(reviewDateObj); - } + // FSRS service expects a Date object for the review time. + // reviewDateObj (created from effectiveReviewDate) is correct here. + schedule.fsrsData = this.fsrsService.createNewFsrsCardData(reviewDateObj); + } - // --- START FIX: Determine correct FsrsRating --- - let actualFsrsRating: FsrsRating; - // Check if the response is one of the legacy ReviewResponse values from MCQ modal - if (response === ReviewResponse.Perfect) { // Value 5 - actualFsrsRating = FsrsRating.Easy; // Map to 4 - } else if (response === ReviewResponse.Good) { // Value 4 - actualFsrsRating = FsrsRating.Good; // Map to 3 - } else if (response === ReviewResponse.Fair) { // Value 3 - actualFsrsRating = FsrsRating.Hard; // Map to 2 - } else if (response === ReviewResponse.Hard) { // Value 1 (legacy) - actualFsrsRating = FsrsRating.Again; // Map to 1 - } else if (Object.values(FsrsRating).includes(response as FsrsRating)) { - // If it's already a valid FsrsRating (1, 2, 3, 4), use it directly - actualFsrsRating = response as FsrsRating; - } else { - // Fallback for unexpected values (e.g., SM-2 specific 0, 2, potentially) - // Map based on general difficulty - const quality = toSM2Quality(response as ReviewResponse); - if (quality >= 4) actualFsrsRating = FsrsRating.Easy; // 4, 5 -> Easy - else if (quality === 3) actualFsrsRating = FsrsRating.Good; // 3 -> Good - else if (quality === 2) actualFsrsRating = FsrsRating.Hard; // 2 -> Hard - else actualFsrsRating = FsrsRating.Again; // 0, 1 -> Again - } - // --- END FIX --- + // --- START FIX: Determine correct FsrsRating --- + let actualFsrsRating: FsrsRating; + // Check if the response is one of the legacy ReviewResponse values from MCQ modal + if (response === ReviewResponse.PerfectRecall) { // Value 5 + actualFsrsRating = FsrsRating.Easy; // Map to 4 + } else if (response === ReviewResponse.CorrectWithHesitation) { // Value 4 + actualFsrsRating = FsrsRating.Good; // Map to 3 + } else if (response === ReviewResponse.CorrectWithDifficulty) { // Value 3 + actualFsrsRating = FsrsRating.Hard; // Map to 2 + } else if (response === ReviewResponse.IncorrectResponse) { // Value 1 (legacy) + actualFsrsRating = FsrsRating.Again; // Map to 1 + } else if (Object.values(FsrsRating).includes(response as FsrsRating)) { + // If it's already a valid FsrsRating (1, 2, 3, 4), use it directly + actualFsrsRating = response as FsrsRating; + } else { + // Fallback for unexpected values (e.g., SM-2 specific 0, 2, potentially) + // Map based on general difficulty + const quality = toSM2Quality(response as ReviewResponse); + if (quality >= 4) actualFsrsRating = FsrsRating.Easy; // 4, 5 -> Easy + else if (quality === 3) actualFsrsRating = FsrsRating.Good; // 3 -> Good + else if (quality === 2) actualFsrsRating = FsrsRating.Hard; // 2 -> Hard + else actualFsrsRating = FsrsRating.Again; // 0, 1 -> Again + } + // --- END FIX --- - const { updatedData, nextReviewDate: newNextReviewDateFsrs } = this.fsrsService.recordReview( - schedule.fsrsData, - actualFsrsRating, // Pass the correctly determined FsrsRating - reviewDateObj // Pass the exact moment of review - ); - schedule.fsrsData = updatedData; - schedule.nextReviewDate = newNextReviewDateFsrs; // This is already a UTC timestamp from FSRS + const { updatedData, nextReviewDate: newNextReviewDateFsrs } = this.fsrsService.recordReview( + schedule.fsrsData, + actualFsrsRating, // Pass the correctly determined FsrsRating + reviewDateObj // Pass the exact moment of review + ); + schedule.fsrsData = updatedData; + schedule.nextReviewDate = newNextReviewDateFsrs; // This is already a UTC timestamp from FSRS // SM-2 specific fields are not updated for FSRS cards schedule.interval = updatedData.scheduled_days; // For display consistency if needed schedule.ease = Math.round(updatedData.difficulty * 10); // Approximate for display } else { // SM-2 - let qualityRating = toSM2Quality(response as ReviewResponse); - + const qualityRating = toSM2Quality(response as ReviewResponse); + // Initialize SM-2 fields if they are somehow missing (should not happen for SM-2 cards) schedule.ease = schedule.ease ?? this.plugin.settings.baseEase; schedule.interval = schedule.interval ?? 0; @@ -323,8 +323,8 @@ export class ReviewScheduleService { schedule.consecutive += 1; if (qualityRating >= 3) { // repetitionCount for initial phase should increment if successful, reset if not. - // This is distinct from the SM-2 n. - schedule.repetitionCount = (schedule.repetitionCount || 0) + 1; + // This is distinct from the SM-2 n. + schedule.repetitionCount = (schedule.repetitionCount || 0) + 1; } else { // q < 3 schedule.repetitionCount = 0; // Reset progress in initial steps } @@ -379,193 +379,122 @@ export class ReviewScheduleService { currentInterval: number, currentEase: number, response: ReviewResponse, - repetitionCount: number = 0, - daysLate: number = 0, - isSkipped: boolean = false + repetitionCount = 0, + daysLate = 0, + isSkipped = false ): { interval: number, ease: number, repetitionCount: number } { - // Ensure response is in the valid SM-2 range (0-5) - let qualityRating = toSM2Quality(response); - - // Handle overdue or skipped items according to modified SM-2 algorithm - if (isSkipped || daysLate > 0) { - // Determine effective quality rating: - // - If explicitly skipped, reduce quality by 1 (but not below 0) - // - If overdue, set quality to 0 - // - If both overdue and explicitly skipped, prioritize the skip logic (user choice) - const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0; - - // Convert ease from internal format (250 = 2.5) to SM-2 format (2.5) - let ease = currentEase / 100; - - // Calculate new ease factor using SM-2 formula with the effective quality - ease = ease + (0.1 - (5 - q_eff) * (0.08 + (5 - q_eff) * 0.02)); - - // Apply minimum ease factor (SM-2 specifies 1.3 as the minimum) - ease = Math.max(1.3, ease); - - // Force next review to be tomorrow (interval = 1) regardless of computed interval - // and reset repetition count to 1 - return { - interval: 1, // Force next review to be tomorrow - ease: Math.round(ease * 100), // Convert back to internal format - repetitionCount: 1 // Reset repetition count to 1 - }; - } - - // For normal reviews, use the regular SM-2 implementation - // Convert ease from internal format (250 = 2.5) to SM-2 format (2.5) - let ease = currentEase / 100; - let newRepetitionCount = repetitionCount; - let interval: number; - - // Calculate new ease factor using SM-2 formula - // EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)) - ease = ease + (0.1 - (5 - qualityRating) * (0.08 + (5 - qualityRating) * 0.02)); - - // Apply minimum ease factor (SM-2 specifies 1.3 as the minimum) - ease = Math.max(1.3, ease); - - // If response is less than 3, reset repetition count to 0 (per strict SM-2) - if (qualityRating < 3) { - newRepetitionCount = 0; - interval = 1; // Next interval is 1 day for failed items - } else { - // Increment repetition count for correct responses - newRepetitionCount += 1; - - // Calculate new interval based on SM-2 rules - if (newRepetitionCount === 1) { - interval = 1; - } else if (newRepetitionCount === 2) { - interval = 6; - } else { - // For n > 2, use the formula I_n = I_(n-1) * EF - interval = Math.round(currentInterval * ease); - } - } - - // Apply load balancing if enabled (this is an extension to the algorithm) - if (this.plugin.settings.loadBalance) { - const fuzz = interval > 7 ? Math.min(3, Math.floor(interval * 0.05)) : 0; - interval = interval + Math.random() * fuzz * 2 - fuzz; - } - - // Ensure interval is at least 1 day - interval = Math.max(1, interval); - - // Enforce maximum interval (this is an extension to the algorithm) - interval = Math.min(interval, this.plugin.settings.maximumInterval); - - // Convert ease back to internal format before returning - return { - interval: Math.round(interval), // SM-2 uses whole days - ease: Math.round(ease * 100), - repetitionCount: newRepetitionCount - }; + return this.calculateSM2Schedule( + currentInterval, + currentEase, + toSM2Quality(response), + repetitionCount, + daysLate, + isSkipped + ); } - /** - * Calculate new schedule parameters using the enhanced SM-2 algorithm with lateness penalty - * (This is the core calculation logic used internally by recordReview and skipNote) - * - * @param currentInterval Current interval in days - * @param currentEase Current ease factor (expressed as a number where 2.5 = 250) - * @param qualityRating User's response during review, as a numeric quality rating (0-5) - * @param repetitionCount Current repetition count (n) - * @param daysLate How many days late the review is (0 if on time or early) - * @param isSkipped Whether the item was explicitly skipped by the user - * @returns New interval, ease, and repetition count - */ - private calculateSM2Schedule( - currentInterval: number, - currentEase: number, - qualityRating: number, // Changed parameter type from ReviewResponse to number - repetitionCount: number = 0, - daysLate: number = 0, - isSkipped: boolean = false - ): { interval: number, ease: number, repetitionCount: number } { - // qualityRating is now expected to be a number (0-5) directly. - // The call to toSM2Quality(response) is removed. + /** + * Calculate new schedule parameters using the enhanced SM-2 algorithm with lateness penalty + * (This is the core calculation logic used internally by recordReview and skipNote) + * + * @param currentInterval Current interval in days + * @param currentEase Current ease factor (expressed as a number where 2.5 = 250) + * @param qualityRating User's response during review, as a numeric quality rating (0-5) + * @param repetitionCount Current repetition count (n) + * @param daysLate How many days late the review is (0 if on time or early) + * @param isSkipped Whether the item was explicitly skipped by the user + * @returns New interval, ease, and repetition count + */ + private calculateSM2Schedule( + currentInterval: number, + currentEase: number, + qualityRating: number, // Changed parameter type from ReviewResponse to number + repetitionCount = 0, + daysLate = 0, + isSkipped = false + ): { interval: number, ease: number, repetitionCount: number } { + // qualityRating is now expected to be a number (0-5) directly. + // The call to toSM2Quality(response) is removed. - // Handle overdue or skipped items according to modified SM-2 algorithm - if (isSkipped || daysLate > 0) { - // Determine effective quality rating: - // - If explicitly skipped, reduce quality by 1 (but not below 0) - // - If overdue, set quality to 0 - // - If both overdue and explicitly skipped, prioritize the skip logic (user choice) - const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0; + // Handle overdue or skipped items according to modified SM-2 algorithm + if (isSkipped || daysLate > 0) { + // Determine effective quality rating: + // - If explicitly skipped, reduce quality by 1 (but not below 0) + // - If overdue, set quality to 0 + // - If both overdue and explicitly skipped, prioritize the skip logic (user choice) + const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0; - // Convert ease from internal format (250 = 2.5) to SM-2 format (2.5) - let ease = currentEase / 100; + // Convert ease from internal format (250 = 2.5) to SM-2 format (2.5) + let ease = currentEase / 100; - // Calculate new ease factor using SM-2 formula with the effective quality - ease = ease + (0.1 - (5 - q_eff) * (0.08 + (5 - q_eff) * 0.02)); + // Calculate new ease factor using SM-2 formula with the effective quality + ease = ease + (0.1 - (5 - q_eff) * (0.08 + (5 - q_eff) * 0.02)); - // Apply minimum ease factor (SM-2 specifies 1.3 as the minimum) - ease = Math.max(1.3, ease); + // Apply minimum ease factor (SM-2 specifies 1.3 as the minimum) + ease = Math.max(1.3, ease); - // Force next review to be tomorrow (interval = 1) regardless of computed interval - // and reset repetition count to 1 - const result = { - interval: 1, // Force next review to be tomorrow - ease: Math.round(ease * 100), // Convert back to internal format - repetitionCount: 1 // Reset repetition count to 1 - }; + // Force next review to be tomorrow (interval = 1) regardless of computed interval + // and reset repetition count to 1 + const result = { + interval: 1, // Force next review to be tomorrow + ease: Math.round(ease * 100), // Convert back to internal format + repetitionCount: 1 // Reset repetition count to 1 + }; - return result; - } + return result; + } - // For normal reviews, use the regular SM-2 implementation - // Convert ease from internal format (250 = 2.5) to SM-2 format (2.5) - let ease = currentEase / 100; - let newRepetitionCount = repetitionCount; - let interval: number; + // For normal reviews, use the regular SM-2 implementation + // Convert ease from internal format (250 = 2.5) to SM-2 format (2.5) + let ease = currentEase / 100; + let newRepetitionCount = repetitionCount; + let interval: number; - // Calculate new ease factor using SM-2 formula - // EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)) - ease = ease + (0.1 - (5 - qualityRating) * (0.08 + (5 - qualityRating) * 0.02)); + // Calculate new ease factor using SM-2 formula + // EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)) + ease = ease + (0.1 - (5 - qualityRating) * (0.08 + (5 - qualityRating) * 0.02)); - // Apply minimum ease factor (SM-2 specifies 1.3 as the minimum) - ease = Math.max(1.3, ease); + // Apply minimum ease factor (SM-2 specifies 1.3 as the minimum) + ease = Math.max(1.3, ease); - // If response is less than 3, reset repetition count to 0 (per strict SM-2) - if (qualityRating < 3) { - newRepetitionCount = 0; - interval = 1; // Next interval is 1 day for failed items - } else { - // Increment repetition count for correct responses - newRepetitionCount += 1; + // If response is less than 3, reset repetition count to 0 (per strict SM-2) + if (qualityRating < 3) { + newRepetitionCount = 0; + interval = 1; // Next interval is 1 day for failed items + } else { + // Increment repetition count for correct responses + newRepetitionCount += 1; - // Calculate new interval based on SM-2 rules - if (newRepetitionCount === 1) { - interval = 1; - } else if (newRepetitionCount === 2) { - interval = 6; - } else { - // For n > 2, use the formula I_n = I_(n-1) * EF - interval = Math.round(currentInterval * ease); - } - } + // Calculate new interval based on SM-2 rules + if (newRepetitionCount === 1) { + interval = 1; + } else if (newRepetitionCount === 2) { + interval = 6; + } else { + // For n > 2, use the formula I_n = I_(n-1) * EF + interval = Math.round(currentInterval * ease); + } + } - // Apply load balancing if enabled (this is an extension to the algorithm) - if (this.plugin.settings.loadBalance) { - const fuzz = interval > 7 ? Math.min(3, Math.floor(interval * 0.05)) : 0; - interval = interval + Math.random() * fuzz * 2 - fuzz; - } + // Apply load balancing if enabled (this is an extension to the algorithm) + if (this.plugin.settings.loadBalance) { + const fuzz = interval > 7 ? Math.min(3, Math.floor(interval * 0.05)) : 0; + interval = interval + Math.random() * fuzz * 2 - fuzz; + } - // Ensure interval is at least 1 day - interval = Math.max(1, interval); + // Ensure interval is at least 1 day + interval = Math.max(1, interval); - // Enforce maximum interval (this is an extension to the algorithm) - interval = Math.min(interval, this.plugin.settings.maximumInterval); + // Enforce maximum interval (this is an extension to the algorithm) + interval = Math.min(interval, this.plugin.settings.maximumInterval); - // Convert ease back to internal format before returning - return { - interval: Math.round(interval), // SM-2 uses whole days - ease: Math.round(ease * 100), - repetitionCount: newRepetitionCount - }; - } + // Convert ease back to internal format before returning + return { + interval: Math.round(interval), // SM-2 uses whole days + ease: Math.round(ease * 100), + repetitionCount: newRepetitionCount + }; + } /** @@ -575,7 +504,7 @@ export class ReviewScheduleService { * @param matchExactDate If true, only return notes due exactly on this date (ignoring time). Otherwise, notes due on or before this date. * @returns Array of due note schedules sorted by due date */ - getDueNotes(date: number = Date.now(), matchExactDate: boolean = false): ReviewSchedule[] { + getDueNotes(date: number = Date.now(), matchExactDate = false): ReviewSchedule[] { const targetDate = new Date(date); const targetUTCDayStart = DateUtils.startOfUTCDay(targetDate); const targetUTCDayEnd = DateUtils.endOfUTCDay(targetDate); @@ -601,7 +530,7 @@ export class ReviewScheduleService { * @param days Number of days to look ahead * @returns Array of upcoming review schedules sorted by due date */ - getUpcomingReviews(days: number = 7): ReviewSchedule[] { + getUpcomingReviews(days = 7): ReviewSchedule[] { const now = Date.now(); const futureDate = DateUtils.addDays(now, days); @@ -625,10 +554,10 @@ export class ReviewScheduleService { * @param response Optional user's response to use for penalty calculation * @param currentReviewDate Optional timestamp for the current review date (simulated or actual) */ - async skipNote(path: string, response: ReviewResponse | FsrsRating = ReviewResponse.CorrectWithDifficulty, currentReviewDate?: number): Promise { + skipNote(path: string, response: ReviewResponse | FsrsRating = ReviewResponse.CorrectWithDifficulty, currentReviewDate?: number): void { const schedule = this.schedules[path]; if (!schedule) return; - + const effectiveReviewDate = currentReviewDate || Date.now(); const reviewDateObj = new Date(effectiveReviewDate); @@ -639,7 +568,7 @@ export class ReviewScheduleService { if (!schedule.fsrsData) { // Should not happen schedule.fsrsData = this.fsrsService.createNewFsrsCardData(reviewDateObj); } - const { updatedData, nextReviewDate: newNextReviewDateFsrs, log } = this.fsrsService.skipReview( + const { updatedData, nextReviewDate: newNextReviewDateFsrs } = this.fsrsService.skipReview( schedule.fsrsData, reviewDateObj // Pass exact moment for FSRS skip ); @@ -696,7 +625,7 @@ export class ReviewScheduleService { * @param path Path to the note file * @param days Number of days to postpone (default: 1) */ - async postponeNote(path: string, days: number = 1): Promise { + postponeNote(path: string, days = 1): void { const schedule = this.schedules[path]; if (!schedule) return; @@ -715,7 +644,7 @@ export class ReviewScheduleService { }, 50); // Small delay (e.g., 50ms) } - new Notice(`Review postponed for ${days} day${days !== 1 ? 's' : ''}`); + new Notice(`Review postponed for ${days} day${days !== 1 ? 's' : ''}.`); } /** @@ -724,7 +653,7 @@ export class ReviewScheduleService { * @param path Path to the note file * @returns True if the note was advanced, false otherwise. */ - async advanceNote(path: string): Promise { + advanceNote(path: string): boolean { const schedule = this.schedules[path]; if (!schedule) { return false; @@ -743,7 +672,7 @@ export class ReviewScheduleService { // New potential next review date is one day earlier. // If FSRS, it's one day earlier from its exact time. If SM-2, one day earlier from its UTC midnight. const newPotentialNextReviewTimestamp = DateUtils.addDays(schedule.nextReviewDate, -1); - + // Ensure the new date (if SM-2, its UTC day start) is not before todayUTCMidnight if (schedule.schedulingAlgorithm === 'sm2') { schedule.nextReviewDate = Math.max(todayUTCMidnight, DateUtils.startOfUTCDay(new Date(newPotentialNextReviewTimestamp))); @@ -771,7 +700,7 @@ export class ReviewScheduleService { * * @param path Path to the note file */ - async removeFromReview(path: string): Promise { + removeFromReview(path: string): void { if (this.schedules[path]) { delete this.schedules[path]; @@ -779,7 +708,7 @@ export class ReviewScheduleService { this.customNoteOrder = this.customNoteOrder.filter(p => p !== path); // Data saving is now handled by main.ts after this method returns - new Notice("Note removed from review schedule"); + new Notice("Note removed from review schedule."); // Note: The controller will be notified separately to update its state // This prevents immediate reordering based on link analysis after removal. @@ -793,11 +722,11 @@ export class ReviewScheduleService { /** * Clear all review schedules */ - async clearAllSchedules(): Promise { + clearAllSchedules(): void { this.schedules = {}; this.customNoteOrder = []; // Also clear custom order // Data saving is now handled by main.ts after this method returns - new Notice("All review schedules have been cleared"); + new Notice("All review schedules have been cleared."); // Notify any listeners (for UI updates) if (this.plugin.events) { @@ -807,7 +736,7 @@ export class ReviewScheduleService { // Explicitly update the review controller's state // This dependency might need to be managed differently, e.g., via events if (this.plugin.reviewController) { - await this.plugin.reviewController.updateTodayNotes(); + void this.plugin.reviewController.updateTodayNotes(); } } @@ -824,7 +753,7 @@ export class ReviewScheduleService { try { const content = await this.plugin.app.vault.read(file); return EstimationUtils.estimateReviewTime(file, content); - } catch (error) { + } catch { return 60; // Default 1 minute } } @@ -836,14 +765,14 @@ export class ReviewScheduleService { * @param daysFromNow Days until first review (default: 0, same day) * @returns Number of notes scheduled */ - async scheduleNotesInOrder(paths: string[], daysFromNow: number = 0): Promise { + scheduleNotesInOrder(paths: string[], daysFromNow = 0): number { let count = 0; // Schedule each note in the provided order for (const path of paths) { const file = this.plugin.app.vault.getAbstractFileByPath(path); if (!file || !(file instanceof TFile) || file.extension !== "md" || this.schedules[path]) { - continue; + continue; } const now = Date.now(); @@ -860,9 +789,9 @@ export class ReviewScheduleService { }; } else { // sm2 newSchedule = { - path, lastReviewDate: null, + path, lastReviewDate: null, nextReviewDate: DateUtils.addDays(todayUTCStart, daysFromNow), // UTC midnight - ease: this.plugin.settings.baseEase, interval: daysFromNow, + ease: this.plugin.settings.baseEase, interval: daysFromNow, consecutive: 0, reviewCount: 0, repetitionCount: 0, scheduleCategory: this.plugin.settings.useInitialSchedule ? 'initial' : 'spaced', schedulingAlgorithm: 'sm2', fsrsData: undefined, @@ -878,7 +807,7 @@ export class ReviewScheduleService { } } this.schedules[path] = newSchedule; - + // Add to custom order if not already present if (!this.customNoteOrder.includes(path)) { this.customNoteOrder.push(path); @@ -902,11 +831,11 @@ export class ReviewScheduleService { /** * Update the custom note order - used to maintain user-defined ordering * - * @param order Array of note paths in desired order + * @param newOrder Array of note paths in desired order */ - async updateCustomNoteOrder(order: string[]): Promise { + updateCustomNoteOrder(newOrder: string[]): void { // Filter out duplicate paths and ensure we only store paths that exist in our schedules - const uniqueValidPaths = Array.from(new Set(order)).filter(path => this.schedules[path] !== undefined); + const uniqueValidPaths = Array.from(new Set(newOrder)).filter(path => this.schedules[path] !== undefined); this.customNoteOrder = uniqueValidPaths; // Data saving is now handled by main.ts after this method returns @@ -925,7 +854,7 @@ export class ReviewScheduleService { * @param matchExactDate Passed to getDueNotes to filter by exact date if true. * @returns Array of due note schedules sorted appropriately */ - getDueNotesWithCustomOrder(date: number = Date.now(), useCustomOrder: boolean = true, matchExactDate: boolean = false): ReviewSchedule[] { + getDueNotesWithCustomOrder(date: number = Date.now(), useCustomOrder = true, matchExactDate = false): ReviewSchedule[] { // First, get all due notes using the modified method const dueNotes = this.getDueNotes(date, matchExactDate); @@ -989,7 +918,7 @@ export class ReviewScheduleService { this.customNoteOrder.push(newPath); } } - + // Notify any listeners (for UI updates) // Data saving will be handled by the caller in main.ts @@ -1007,8 +936,7 @@ export class ReviewScheduleService { return 2; } - public async convertAllSm2ToFsrs(): Promise { - let convertedCount = 0; + public convertAllSm2ToFsrs(): void { for (const path in this.schedules) { if (Object.prototype.hasOwnProperty.call(this.schedules, path)) { const schedule = this.schedules[path]; @@ -1018,15 +946,15 @@ export class ReviewScheduleService { const baseDate = schedule.lastReviewDate ? new Date(schedule.lastReviewDate) : new Date(); schedule.fsrsData = this.fsrsService.createNewFsrsCardData(baseDate); // New FSRS cards are typically due 'now' relative to their creation/conversion. - schedule.nextReviewDate = baseDate.getTime(); - + schedule.nextReviewDate = baseDate.getTime(); + // Clear or nullify SM-2 specific fields schedule.ease = this.plugin.settings.baseEase; // Keep a base ease for potential future conversion back schedule.interval = 0; // Reset SM-2 interval schedule.repetitionCount = 0; schedule.consecutive = 0; + schedule.consecutive = 0; schedule.scheduleCategory = undefined; - convertedCount++; } } } @@ -1035,8 +963,7 @@ export class ReviewScheduleService { } } - public async convertAllFsrsToSm2(): Promise { - let convertedCount = 0; + public convertAllFsrsToSm2(): void { for (const path in this.schedules) { if (Object.prototype.hasOwnProperty.call(this.schedules, path)) { const schedule = this.schedules[path]; @@ -1047,7 +974,7 @@ export class ReviewScheduleService { schedule.repetitionCount = 0; schedule.consecutive = 0; schedule.scheduleCategory = this.plugin.settings.useInitialSchedule ? 'initial' : 'spaced'; - + // Set next review date based on SM-2 initial logic, using UTC days const now = Date.now(); const todayUTCStart = DateUtils.startOfUTCDay(new Date(now)); @@ -1057,9 +984,8 @@ export class ReviewScheduleService { nextReview = DateUtils.addDays(todayUTCStart, schedule.interval); } schedule.nextReviewDate = nextReview; - + schedule.fsrsData = undefined; // Clear FSRS data - convertedCount++; } } } diff --git a/services/review-session-service.ts b/services/review-session-service.ts index 00e08ce..1a5b370 100644 --- a/services/review-session-service.ts +++ b/services/review-session-service.ts @@ -78,7 +78,7 @@ export class ReviewSessionService { } return session; - } catch (error) { + } catch { new Notice("Failed to create review session"); return null; } @@ -90,7 +90,7 @@ export class ReviewSessionService { * @param sessionId ID of the session to activate * @returns Whether the session was activated */ - async setActiveSession(sessionId: string | null): Promise { + setActiveSession(sessionId: string | null): boolean { if (sessionId === null) { this.reviewSessions.activeSessionId = null; // Data saving is now handled by main.ts after this method returns @@ -156,7 +156,7 @@ export class ReviewSessionService { * * @returns Whether there are more files to review */ - async advanceActiveSession(): Promise { + advanceActiveSession(): boolean { const session = this.getActiveSession(); if (!session) { return false; @@ -191,7 +191,7 @@ export class ReviewSessionService { * @param sessionId ID of the session * @returns Number of files scheduled */ - async scheduleSessionForReview(sessionId: string): Promise { + scheduleSessionForReview(sessionId: string): number { const session = this.reviewSessions.sessions[sessionId]; if (!session) { return 0; @@ -199,10 +199,10 @@ export class ReviewSessionService { // Access ReviewScheduleService via plugin reference if (!this.plugin.reviewScheduleService) { - return 0; + return 0; } // Call the method on the ReviewScheduleService instance - return await this.plugin.reviewScheduleService.scheduleNotesInOrder(session.hierarchy.traversalOrder); + return this.plugin.reviewScheduleService.scheduleNotesInOrder(session.hierarchy.traversalOrder); } } diff --git a/ui/batch-review-modal.ts b/ui/batch-review-modal.ts index e0cd40c..caf2d17 100644 --- a/ui/batch-review-modal.ts +++ b/ui/batch-review-modal.ts @@ -13,26 +13,26 @@ export class BatchReviewModal extends Modal { plugin: SpaceforgePlugin; notes: ReviewSchedule[]; useMCQ: boolean; - currentIndex: number = 0; + currentIndex = 0; results: Array<{ path: string, success: boolean, response: ReviewResponse, score?: number }> = []; - started: boolean = false; + started = false; allMCQSets: { path: string; mcqSet: MCQSet; fileName: string; }[] = []; - collectingMCQs: boolean = false; + collectingMCQs = false; constructor( app: App, plugin: SpaceforgePlugin, notes: ReviewSchedule[], - useMCQ: boolean = false + useMCQ = false ) { super(app); this.plugin = plugin; @@ -40,25 +40,27 @@ export class BatchReviewModal extends Modal { this.useMCQ = useMCQ; } - async onOpen(): Promise { + onOpen(): void { const { contentEl } = this; this.renderStartScreen(contentEl); } renderStartScreen(contentEl: HTMLElement): void { contentEl.empty(); - new Setting(contentEl).setName("Batch Review").setHeading(); + new Setting(contentEl).setName("Batch review").setHeading(); const infoEl = contentEl.createDiv("batch-review-info"); infoEl.createEl("p", { text: `${this.notes.length} notes scheduled for review` }); - this.estimateAndShowTime(infoEl); + void this.estimateAndShowTime(infoEl); // Explicitly ignore promise to satisfy linter const buttonsEl = contentEl.createDiv("batch-review-buttons"); const startButton = buttonsEl.createEl("button", { - text: this.useMCQ ? "Start MCQ Review" : "Start Manual Review", + text: this.useMCQ ? "Start MCQ review" : "Start manual review", cls: "batch-review-start-button" }); - startButton.addEventListener("click", () => this.startBatchReview()); + startButton.addEventListener("click", () => { + void this.startBatchReview(); + }); const toggleMCQButton = buttonsEl.createEl("button", { - text: this.useMCQ ? "Switch to Manual Review" : "Switch to MCQ Review", + text: this.useMCQ ? "Switch to manual review" : "Switch to MCQ review", cls: "batch-review-toggle-button" }); toggleMCQButton.addEventListener("click", () => { @@ -67,13 +69,13 @@ export class BatchReviewModal extends Modal { }); if (this.useMCQ) { const regenerateButton = buttonsEl.createEl("button", { - text: "Regenerate All MCQs", + text: "Regenerate all MCQs", cls: "batch-review-regenerate-button" }); regenerateButton.addEventListener("click", () => { this.close(); if (this.plugin.batchController) { - this.plugin.batchController.regenerateAllMCQs(); + void this.plugin.batchController.regenerateAllMCQs(); } }); } @@ -111,7 +113,7 @@ export class BatchReviewModal extends Modal { async collectAllMCQs(): Promise { const { contentEl } = this; contentEl.empty(); - new Setting(contentEl).setName("Collecting MCQs").setHeading(); + new Setting(contentEl).setName("Collecting MCQs").setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case 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"); @@ -134,7 +136,7 @@ export class BatchReviewModal extends Modal { try { // generateMCQs now returns the set or null mcqSet = await this.plugin.mcqController.generateMCQs(note.path); - } catch (error) { + } catch { statusEl.setText(`Error generating MCQs for ${fileName}`); await sleep(1000); } @@ -159,23 +161,25 @@ export class BatchReviewModal extends Modal { const consolidatedModal = new ConsolidatedMCQModal( this.plugin, this.allMCQSets, - async (results: Array<{ path: string, success: boolean, response: ReviewResponse, score?: number }>) => { + (results: Array<{ path: string, success: boolean, response: ReviewResponse, score?: number }>) => { this.results = results; - await this.recordAllReviews(results); - this.open(); - this.showSummary(); + void (async () => { + await this.recordAllReviews(results); + this.open(); + this.showSummary(); + })(); } ); consolidatedModal.open(); - } catch (error) { - new Notice("Error showing MCQ review. Falling back to manual review."); + } catch (_error) { + new Notice("Error showing MCQ review. Falling back to manual review."); // eslint-disable-line obsidianmd/ui/sentence-case this.open(); - this.processNextManual(); + void this.processNextManual(); } } else { - new Notice("MCQ controller not available. Falling back to manual review."); + new Notice("MCQ controller not available. Falling back to manual review."); // eslint-disable-line obsidianmd/ui/sentence-case this.open(); - this.processNextManual(); + void this.processNextManual(); } } @@ -194,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(); + new Setting(contentEl).setName("MCQ review in progress").setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case 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); @@ -213,7 +217,7 @@ export class BatchReviewModal extends Modal { if (mcqSet) { // Update the call to startMCQReview - remove the callback argument - this.plugin.mcqController.startMCQReview(note.path /* Removed callback */); + void this.plugin.mcqController.startMCQReview(note.path /* Removed callback */); // NOTE: The logic to handle the result and move to the next note needs to be // re-implemented if this flow is used instead of the consolidated modal. // The original callback logic is commented out below for reference. @@ -237,22 +241,22 @@ export class BatchReviewModal extends Modal { this.processNextMCQ(); // Continue } */ - // Since the callback is removed, we need a way to know when the individual modal closes. - // This flow is now broken and relies on the consolidated modal. - // Fallback or error handling might be needed here if consolidated fails. - this.open(); // Reopen immediately for now, but this won't wait for the MCQ modal. - this.showSummary(); // Go to summary as the flow is broken. + // Since the callback is removed, we need a way to know when the individual modal closes. + // This flow is now broken and relies on the consolidated modal. + // Fallback or error handling might be needed here if consolidated fails. + this.open(); // Reopen immediately for now, but this won't wait for the MCQ modal. + this.showSummary(); // Go to summary as the flow is broken. } else { - new Notice(`Couldn't generate MCQs for ${fileName}, falling back to manual review`); + new Notice(`Couldn't generate MCQs for ${fileName}, falling back to manual review.`); this.open(); - this.processNextManual(); + void this.processNextManual(); } } else { - new Notice("MCQ controller not available, falling back to manual review"); + new Notice("MCQ controller not available, falling back to manual review."); // eslint-disable-line obsidianmd/ui/sentence-case this.open(); - this.processNextManual(); + void this.processNextManual(); } } @@ -269,7 +273,7 @@ export class BatchReviewModal extends Modal { const note = this.notes[this.currentIndex]; const { contentEl } = this; contentEl.empty(); - new Setting(contentEl).setName("Manual Review").setHeading(); + new Setting(contentEl).setName("Manual review").setHeading(); const progressEl = contentEl.createDiv("batch-review-progress"); progressEl.createEl("p", { text: `Note ${this.currentIndex + 1}/${this.notes.length}` }); const file = this.plugin.app.vault.getAbstractFileByPath(note.path); @@ -281,44 +285,44 @@ 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" }); - blackoutButton.addEventListener("click", () => this.recordManualResult(note.path, ReviewResponse.CompleteBlackout)); - const incorrectButton = buttonsContainer.createEl("button", { text: "1: Incorrect Response", cls: "review-button review-button-incorrect" }); - incorrectButton.addEventListener("click", () => this.recordManualResult(note.path, ReviewResponse.IncorrectResponse)); - const incorrectFamiliarButton = buttonsContainer.createEl("button", { text: "2: Incorrect but Familiar", cls: "review-button review-button-incorrect-familiar" }); - incorrectFamiliarButton.addEventListener("click", () => this.recordManualResult(note.path, ReviewResponse.IncorrectButFamiliar)); - const correctDifficultyButton = buttonsContainer.createEl("button", { text: "3: Correct with Difficulty", cls: "review-button review-button-correct-difficulty" }); - correctDifficultyButton.addEventListener("click", () => this.recordManualResult(note.path, ReviewResponse.CorrectWithDifficulty)); - const correctHesitationButton = buttonsContainer.createEl("button", { text: "4: Correct with Hesitation", cls: "review-button review-button-correct-hesitation" }); - correctHesitationButton.addEventListener("click", () => this.recordManualResult(note.path, ReviewResponse.CorrectWithHesitation)); - const perfectRecallButton = buttonsContainer.createEl("button", { text: "5: Perfect Recall", cls: "review-button review-button-perfect-recall" }); - perfectRecallButton.addEventListener("click", () => this.recordManualResult(note.path, ReviewResponse.PerfectRecall)); + const blackoutButton = buttonsContainer.createEl("button", { text: "0: Complete blackout", cls: "review-button review-button-complete-blackout" }); // eslint-disable-line obsidianmd/ui/sentence-case + 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 + 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 + 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 + 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 + 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 + 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", () => { this.currentIndex++; - this.processNextManual(); + void this.processNextManual(); }); } async recordManualResult(path: string, response: ReviewResponse): Promise { this.results.push({ path, success: response >= ReviewResponse.CorrectWithDifficulty, response }); // Assuming 3+ is success - + // Use the data storage method to get the return value const wasRecorded = await this.plugin.dataStorage.recordReview(path, response); - + if (!wasRecorded) { // Note was not due, this is just a preview - new Notice("Note previewed, not recorded"); + new Notice("Note previewed, not recorded."); } - + this.currentIndex++; - this.processNextManual(); + void this.processNextManual(); } showSummary(): void { const { contentEl } = this; contentEl.empty(); - new Setting(contentEl).setName("Batch Review Complete").setHeading(); + new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header'); // eslint-disable-line obsidianmd/ui/sentence-case const statsEl = contentEl.createDiv("batch-review-summary-stats"); const totalNotes = this.results.length; const successfulNotes = this.results.filter(r => r.success).length; @@ -326,7 +330,7 @@ export class BatchReviewModal extends Modal { statsEl.createEl("p", { text: `Completed: ${totalNotes}/${this.notes.length} notes` }); statsEl.createEl("p", { text: `Success rate: ${successRate}% (${successfulNotes}/${totalNotes})`, cls: successRate >= 70 ? "batch-review-success" : "batch-review-needs-improvement" }); const resultsEl = contentEl.createDiv("batch-review-results"); - new Setting(resultsEl).setHeading().setName("Individual Results"); + new Setting(resultsEl).setHeading().setName("Individual results"); const resultsListEl = resultsEl.createDiv("batch-review-results-list"); for (const result of this.results) { @@ -335,7 +339,7 @@ export class BatchReviewModal extends Modal { const fileName = file instanceof TFile ? file.basename : result.path; resultItemEl.createEl("div", { text: fileName, cls: "batch-review-result-filename" }); let responseText: string; let responseClass: string; - switch(result.response) { + switch (result.response) { case ReviewResponse.CompleteBlackout: responseText = "Complete Blackout (0)"; responseClass = "batch-review-complete-blackout"; break; case ReviewResponse.IncorrectResponse: responseText = "Incorrect Response (1)"; responseClass = "batch-review-incorrect"; break; case ReviewResponse.IncorrectButFamiliar: responseText = "Incorrect but Familiar (2)"; responseClass = "batch-review-incorrect-familiar"; break; @@ -352,7 +356,7 @@ export class BatchReviewModal extends Modal { const closeButton = contentEl.createEl("button", { text: "Close", cls: "batch-review-close-button" }); closeButton.addEventListener("click", () => { this.close(); - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); }); } @@ -360,7 +364,7 @@ export class BatchReviewModal extends Modal { const { contentEl } = this; contentEl.empty(); if (this.started && this.currentIndex < this.notes.length && this.results.length > 0) { - new Notice(`Batch review interrupted after ${this.results.length} notes`); + new Notice(`Batch review interrupted after ${this.results.length} notes.`); } } } diff --git a/ui/calendar-view.ts b/ui/calendar-view.ts index 46aff2d..d1b61d6 100644 --- a/ui/calendar-view.ts +++ b/ui/calendar-view.ts @@ -1,7 +1,8 @@ + import { Notice, setIcon } from "obsidian"; import SpaceforgePlugin from "../main"; import { ReviewSchedule } from "../models/review-schedule"; -import { CalendarEvent, EventCategory, EventRecurrence } from "../models/calendar-event"; +import { CalendarEvent } from "../models/calendar-event"; import { DateUtils } from "../utils/dates"; import { EstimationUtils } from "../utils/estimation"; import { UpcomingEvents } from "./upcoming-events"; @@ -32,12 +33,12 @@ export class CalendarView { * Reference to the main plugin */ plugin: SpaceforgePlugin; - + /** * Calendar container element */ containerEl: HTMLElement; - + /** * Current date being displayed */ @@ -59,11 +60,11 @@ export class CalendarView { private calendarGridEl: HTMLElement | null = null; private upcomingEventsEl: HTMLElement | null = null; private upcomingEventsComponent: UpcomingEvents | null = null; - + // Tooltip elements private tooltipEl: HTMLElement | null = null; private tooltipTimeout: number | null = null; - + /** * Initialize calendar view * @@ -75,18 +76,18 @@ export class CalendarView { this.plugin = plugin; this.currentDate = new Date(); } - + /** * Render the calendar view */ async render(): Promise { this.ensureCalendarBaseStructure(); // Ensures header and grid containers exist - + this.updateCalendarHeader(); // Updates month title - + await this.loadReviewsData(); // Preloads review data for the current view await this.loadEventsData(); // Preloads event data for the current view - + if (this.calendarGridEl) { this.renderCalendarGridContent(this.calendarGridEl); // Renders/updates the grid } @@ -108,30 +109,30 @@ export class CalendarView { if (!this.calendarHeaderEl || !calendarContainer.contains(this.calendarHeaderEl)) { this.calendarHeaderEl?.remove(); this.calendarHeaderEl = calendarContainer.createDiv("calendar-header"); - + const prevMonthBtn = this.calendarHeaderEl.createDiv("calendar-nav-btn"); setIcon(prevMonthBtn, "chevron-left"); prevMonthBtn.addEventListener("click", () => { this.currentDate.setMonth(this.currentDate.getMonth() - 1); - this.render(); + void this.render(); }); - + this.monthTitleEl = this.calendarHeaderEl.createDiv("calendar-month-title"); - + const nextMonthBtn = this.calendarHeaderEl.createDiv("calendar-nav-btn"); setIcon(nextMonthBtn, "chevron-right"); nextMonthBtn.addEventListener("click", () => { this.currentDate.setMonth(this.currentDate.getMonth() + 1); - this.render(); + void this.render(); }); - + const todayBtn = this.calendarHeaderEl.createDiv("calendar-today-btn"); todayBtn.setText("Today"); todayBtn.addEventListener("click", () => { this.currentDate = new Date(); - this.render(); + void this.render(); }); - + const addEventBtn = this.calendarHeaderEl.createDiv("calendar-add-event-btn"); setIcon(addEventBtn, "plus"); addEventBtn.addEventListener("click", () => { @@ -151,28 +152,28 @@ export class CalendarView { this.upcomingEventsComponent = new UpcomingEvents(this.upcomingEventsEl, this.plugin); } } - + /** * Update calendar header (month title) */ updateCalendarHeader(): void { if (this.monthTitleEl) { this.monthTitleEl.setText( - this.currentDate.toLocaleString('default', { - month: 'long', - year: 'numeric' + this.currentDate.toLocaleString('default', { + month: 'long', + year: 'numeric' }) ); } } - + /** * Load and organize review data by date */ async loadReviewsData(): Promise { this.reviewsByDate = new Map(); const allSchedules = Object.values(this.plugin.reviewScheduleService.schedules); - + for (const schedule of allSchedules) { const scheduleDueDayStart = DateUtils.startOfUTCDay(new Date(schedule.nextReviewDate)); const dateKey = scheduleDueDayStart.toString(); // Use timestamp as a robust key @@ -184,7 +185,7 @@ export class CalendarView { totalTime: 0 }); } - + const dateReviews = this.reviewsByDate.get(dateKey); if (dateReviews) { dateReviews.notes.push(schedule); @@ -196,9 +197,9 @@ export class CalendarView { /** * Load and organize event data by date */ - async loadEventsData(): Promise { + loadEventsData(): void { this.eventsByDate = new Map(); - + if (!this.plugin.settings.enableCalendarEvents || !this.plugin.calendarEventService) { return; } @@ -206,7 +207,7 @@ export class CalendarView { const { year, month } = this.getCalendarData(); const startDate = new Date(year, month, 1); const endDate = new Date(year, month + 1, 0); // Last day of month - + const eventsInRange = this.plugin.calendarEventService.getEventsInRange( startDate.getTime(), endDate.getTime() @@ -222,14 +223,14 @@ export class CalendarView { events: [] }); } - + const dateEvents = this.eventsByDate.get(dateKey); if (dateEvents) { dateEvents.events.push(event); } } } - + /** * Render or update the calendar grid content * @@ -246,7 +247,7 @@ export class CalendarView { dayHeader.setText(day); }); } - + const { year, month, firstDay, daysInMonth } = this.getCalendarData(); const totalCells = 42; // Standard 6 weeks * 7 days grid let dayCells = Array.from(gridEl.querySelectorAll(".calendar-day")) as HTMLElement[]; @@ -286,32 +287,34 @@ export class CalendarView { const dateReviews = this.reviewsByDate.get(dateKey); const dateEvents = this.eventsByDate.get(dateKey); - + // Render reviews if (dateReviews && dateReviews.notes.length > 0) { dayCell.addClass("has-reviews"); - + const reviewCount = dayCell.createDiv("calendar-review-count"); reviewCount.setText(dateReviews.notes.length.toString()); - + const timeEstimate = dayCell.createDiv("calendar-time-estimate"); timeEstimate.setText(EstimationUtils.formatTime(dateReviews.totalTime)); - - dayCell.addEventListener("click", async () => { - const today = new Date(); - const isClickedDateToday = DateUtils.isSameDay(currentDateObj, today); + + dayCell.addEventListener("click", () => { + // today assignment removed + this.plugin.settings.sidebarViewType = 'list'; this.plugin.clickedDateFromCalendar = currentDateObj; - - await this.plugin.savePluginData(); - const sidebarView = this.plugin.getSidebarView(); - if (sidebarView && typeof sidebarView.refresh === 'function') { - await sidebarView.refresh(); - } else { - this.plugin.app.workspace.requestSaveLayout(); - new Notice("Switched to list view. Sidebar will update."); - } + + void (async () => { + await this.plugin.savePluginData(); + const sidebarView = this.plugin.getSidebarView(); + if (sidebarView && typeof sidebarView.refresh === 'function') { + await sidebarView.refresh(); + } else { + this.plugin.app.workspace.requestSaveLayout(); + new Notice("Switched to list view. Sidebar will update."); + } + })(); }); if (dateReviews.notes.length > 10) dayCell.addClass("heavy-load"); @@ -322,45 +325,45 @@ export class CalendarView { // Render events as small tabs if (dateEvents && dateEvents.events.length > 0) { dayCell.addClass("has-events"); - + const eventsContainer = dayCell.createDiv("calendar-events-container"); - + // Show up to 3 event tabs const eventsToShow = dateEvents.events.slice(0, 3); eventsToShow.forEach(event => { const eventTab = eventsContainer.createDiv("calendar-event-tab"); eventTab.setText(event.title.substring(0, 8) + (event.title.length > 8 ? "..." : "")); - + // Set color based on event category or custom color using CSS custom property const eventColor = this.plugin.calendarEventService?.getEventColor(event) || '#95A5A6'; eventTab.style.setProperty('--event-color', eventColor); - + // Add hover handler for tooltip - eventTab.addEventListener("mouseenter", (e) => { + eventTab.addEventListener("mouseenter", (_e) => { this.showEventTooltip(eventTab, event); }); - - eventTab.addEventListener("mouseleave", (e) => { + + eventTab.addEventListener("mouseleave", (_e) => { this.hideEventTooltip(); }); - + // Add click handler for event eventTab.addEventListener("click", (e) => { e.stopPropagation(); // Prevent day cell click this.showEventDetails(event); }); - + // Add double-click handler for editing eventTab.addEventListener("dblclick", (e) => { e.stopPropagation(); // Prevent day cell click this.openEditEventModal(event); }); }); - + // Show "more" indicator if there are more events if (dateEvents.events.length > 3) { const moreIndicator = eventsContainer.createDiv("calendar-events-more"); - moreIndicator.setText(`+${dateEvents.events.length - 3}`); + moreIndicator.setText(`+ ${dateEvents.events.length - 3} `); } } @@ -377,7 +380,7 @@ export class CalendarView { } } } - + /** * Get calendar data for the current month * @@ -386,16 +389,16 @@ export class CalendarView { getCalendarData(): { year: number, month: number, firstDay: number, daysInMonth: number } { const year = this.currentDate.getFullYear(); const month = this.currentDate.getMonth(); - + // First day of month (0-6, where 0 is Sunday) const firstDay = new Date(year, month, 1).getDay(); - + // Days in month const daysInMonth = new Date(year, month + 1, 0).getDate(); - + return { year, month, firstDay, daysInMonth }; } - + /** * Check if a date is today * @@ -422,29 +425,29 @@ export class CalendarView { // For now, just show a notice with event details // TODO: Create a proper modal for event details const eventDate = new Date(event.date); - const dateStr = eventDate.toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' + const dateStr = eventDate.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' }); - - let details = `📅 ${event.title}\n📆 ${dateStr}`; - + + let details = `📅 ${event.title} \n📆 ${dateStr} `; + if (event.time) { - details += `\n🕐 ${event.time}`; + details += `\n🕐 ${event.time} `; } - + if (event.description) { - details += `\n📝 ${event.description}`; + details += `\n📝 ${event.description} `; } - + if (event.location) { - details += `\n📍 ${event.location}`; + details += `\n📍 ${event.location} `; } - - details += `\n🏷️ ${event.category}`; - + + details += `\n🏷️ ${event.category} `; + new Notice(details, 8000); } @@ -456,9 +459,9 @@ export class CalendarView { this.plugin.app, this.plugin, null, - async (savedEvent) => { + (_savedEvent) => { // Refresh the calendar view after saving - await this.render(); + void this.render(); } ); modal.open(); @@ -470,26 +473,17 @@ export class CalendarView { * @param date Date to pre-fill in the modal */ openCreateEventModalForDate(date: Date): void { - // Create a default event with the selected date - const defaultEvent = { - title: "", - date: date.getTime(), - isAllDay: true, - category: (this.plugin.settings.defaultEventCategory as EventCategory) || EventCategory.Personal, - recurrence: EventRecurrence.None - }; - const modal = new EventModal( this.plugin.app, this.plugin, null, - async (savedEvent) => { + (_savedEvent) => { // Refresh the calendar view after saving - await this.render(); + void this.render(); } ); modal.open(); - + // Pre-fill the date in the modal after it opens window.setTimeout(() => { const dateInput = modal.contentEl.querySelector('input[type="date"]') as HTMLInputElement; @@ -509,9 +503,9 @@ export class CalendarView { this.plugin.app, this.plugin, event, - async (savedEvent) => { + (_savedEvent) => { // Refresh the calendar view after saving - await this.render(); + void this.render(); } ); modal.open(); @@ -522,7 +516,7 @@ export class CalendarView { */ destroy(): void { this.hideEventTooltip(); - + // Clear other references this.calendarHeaderEl = null; this.monthTitleEl = null; @@ -579,54 +573,53 @@ export class CalendarView { // Create tooltip element this.tooltipEl = document.body.createEl("div"); this.tooltipEl.className = "calendar-event-tooltip"; - + // Format event details const eventDate = new Date(event.date); - const dateStr = eventDate.toLocaleDateString(undefined, { - weekday: 'short', - month: 'short', - day: 'numeric' + const dateStr = eventDate.toLocaleDateString(undefined, { + weekday: 'short', + month: 'short', + day: 'numeric' }); - + // Clear existing content this.tooltipEl.empty(); - + // Create tooltip content safely const titleEl = this.tooltipEl.createDiv('tooltip-title'); titleEl.textContent = event.title; - + const dateEl = this.tooltipEl.createDiv('tooltip-date'); - dateEl.textContent = `📅 ${dateStr}`; - + dateEl.textContent = `📅 ${dateStr} `; + if (event.time) { const timeEl = this.tooltipEl.createDiv('tooltip-time'); - timeEl.textContent = `🕐 ${event.time}`; + timeEl.textContent = `🕐 ${event.time} `; } - + if (event.description) { const descEl = this.tooltipEl.createDiv('tooltip-description'); - descEl.textContent = `📝 ${event.description}`; + descEl.textContent = `📝 ${event.description} `; } - + if (event.location) { const locationEl = this.tooltipEl.createDiv('tooltip-location'); - locationEl.textContent = `📍 ${event.location}`; + locationEl.textContent = `📍 ${event.location} `; } - + const categoryEl = this.tooltipEl.createDiv('tooltip-category'); - categoryEl.textContent = `🏷️ ${event.category}`; - + categoryEl.textContent = `🏷️ ${event.category} `; + // Position tooltip const rect = targetEl.getBoundingClientRect(); - const tooltipRect = this.tooltipEl.getBoundingClientRect(); - + // Add to DOM temporarily to get dimensions document.body.appendChild(this.tooltipEl); - + // Calculate position let left = rect.left + (rect.width / 2) - (this.tooltipEl.offsetWidth / 2); let top = rect.bottom + 8; - + // Adjust if tooltip goes off screen if (left < 8) left = 8; if (left + this.tooltipEl.offsetWidth > window.innerWidth - 8) { @@ -635,13 +628,13 @@ export class CalendarView { if (top + this.tooltipEl.offsetHeight > window.innerHeight - 8) { top = rect.top - this.tooltipEl.offsetHeight - 8; } - - this.tooltipEl.style.left = `${left}px`; - this.tooltipEl.style.top = `${top}px`; - + + this.tooltipEl.style.left = `${left} px`; + this.tooltipEl.style.top = `${top} px`; + // Add fade-in animation using CSS class this.tooltipEl.classList.remove('visible'); - + requestAnimationFrame(() => { if (this.tooltipEl) { this.tooltipEl.classList.add('visible'); diff --git a/ui/confirmation-modal.ts b/ui/confirmation-modal.ts new file mode 100644 index 0000000..8886e10 --- /dev/null +++ b/ui/confirmation-modal.ts @@ -0,0 +1,52 @@ +import { App, Modal, Setting } from 'obsidian'; + +export class ConfirmationModal extends Modal { + private title: string; + private message: string; + private onConfirm: () => void | Promise; + private onCancel: () => void | Promise; + + constructor( + app: App, + title: string, + message: string, + onConfirm: () => void | Promise, + onCancel: () => void | Promise = () => { } + ) { + super(app); + this.title = title; + this.message = message; + this.onConfirm = onConfirm; + this.onCancel = onCancel; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + + contentEl.createEl('h2', { text: this.title }); + contentEl.createEl('p', { text: this.message }); + + const buttonContainer = contentEl.createDiv({ cls: 'modal-button-container' }); + + new Setting(buttonContainer) + .addButton(btn => btn + .setButtonText('Cancel') + .onClick(() => { + void this.onCancel(); + this.close(); + })) + .addButton(btn => btn + .setButtonText('Confirm') + .setCta() + .onClick(() => { + void this.onConfirm(); + this.close(); + })); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/ui/consolidated-mcq-modal.ts b/ui/consolidated-mcq-modal.ts index 93b090b..11b2634 100644 --- a/ui/consolidated-mcq-modal.ts +++ b/ui/consolidated-mcq-modal.ts @@ -1,4 +1,4 @@ -import { Modal, Notice, TFile, setIcon, Setting } from 'obsidian'; +import { Modal, Notice, Setting } from 'obsidian'; import SpaceforgePlugin from '../main'; import { MCQSet } from '../models/mcq'; import { ReviewResponse } from '../models/review-schedule'; @@ -12,7 +12,7 @@ export class ConsolidatedMCQModal extends Modal { * Reference to the main plugin */ plugin: SpaceforgePlugin; - + /** * Collection of all MCQ sets */ @@ -21,7 +21,7 @@ export class ConsolidatedMCQModal extends Modal { mcqSet: MCQSet; fileName: string; }[]; - + /** * Callback for when review is completed */ @@ -31,7 +31,7 @@ export class ConsolidatedMCQModal extends Modal { response: ReviewResponse, score?: number }>) => void; - + /** * All questions from all MCQ sets, flattened */ @@ -44,12 +44,12 @@ export class ConsolidatedMCQModal extends Modal { mcqSetId: string; originalIndex: number; }[] = []; - + /** * Current question index */ - currentQuestionIndex: number = 0; - + currentQuestionIndex = 0; + /** * User's answers */ @@ -62,12 +62,12 @@ export class ConsolidatedMCQModal extends Modal { notePath: string; fileName: string; }[] = []; - + /** * Start time for current question */ - questionStartTime: number = 0; - + questionStartTime = 0; + /** * Initialize consolidated MCQ modal * @@ -93,7 +93,7 @@ export class ConsolidatedMCQModal extends Modal { this.plugin = plugin; this.mcqSets = mcqSets; this.onComplete = onComplete; - + // Flatten all questions from all MCQ sets for (const set of mcqSets) { set.mcqSet.questions.forEach((question, index) => { @@ -106,37 +106,37 @@ export class ConsolidatedMCQModal extends Modal { }); }); } - + // Optional: Shuffle questions for better learning // this.allQuestions = this.shuffleArray(this.allQuestions); } - + /** * Called when the modal is opened */ onOpen(): void { const { contentEl } = this; contentEl.empty(); - + // Add a unique class to the modal content for specific styling contentEl.addClass('spaceforge-mcq-modal'); - + // Create header container const headerContainer = contentEl.createDiv('mcq-header-container'); - + // Add title - new Setting(headerContainer).setName('Multiple Choice Review').setHeading(); - + new Setting(headerContainer).setName('Multiple choice review').setHeading(); + // Display progress with questions from all notes, with enhanced styling const progressEl = contentEl.createDiv('mcq-progress'); - + // Calculate progress percentage for dynamic styling const progressPercent = Math.round(((this.currentQuestionIndex + 1) / this.allQuestions.length) * 100); // Set data-progress for proper progress bar display contentEl.setAttribute('data-progress', progressPercent.toString()); - + progressEl.setText(`Question ${this.currentQuestionIndex + 1} of ${this.allQuestions.length}`); - + // Display a progress counter badge const progressCounter = contentEl.createDiv('mcq-progress-counter'); progressCounter.setText(`${this.allQuestions.length} questions from ${this.mcqSets.length} notes`); @@ -144,17 +144,17 @@ export class ConsolidatedMCQModal extends Modal { // Display note information with enhanced styling const noteInfoEl = contentEl.createDiv('mcq-note-info'); noteInfoEl.setText(`Question from: ${this.allQuestions[this.currentQuestionIndex].fileName}`); - + // Start timing this.questionStartTime = Date.now(); - + // Display current question this.displayCurrentQuestion(contentEl); - + // Add keyboard shortcuts this.registerKeyboardShortcuts(); } - + /** * Register keyboard shortcuts for answering questions */ @@ -163,17 +163,17 @@ export class ConsolidatedMCQModal extends Modal { // Get the current question const questionIndex = this.currentQuestionIndex; if (questionIndex >= this.allQuestions.length) return; - + const question = this.allQuestions[questionIndex]; if (!question || !question.choices) return; - + // Handle number keys 1-9 (for choices) const num = parseInt(event.key); if (!isNaN(num) && num >= 1 && num <= question.choices.length) { this.handleAnswer(num - 1); return; } - + // Handle letter keys A-I (for choices) if (event.key.length === 1) { const letterCode = event.key.toUpperCase().charCodeAt(0); @@ -184,10 +184,10 @@ export class ConsolidatedMCQModal extends Modal { } } }; - + // Add event listener document.addEventListener('keydown', keyDownHandler); - + // Clean up when modal is closed this.onClose = () => { document.removeEventListener('keydown', keyDownHandler); @@ -195,7 +195,7 @@ export class ConsolidatedMCQModal extends Modal { contentEl.empty(); }; } - + /** * Display the current question * @@ -203,18 +203,18 @@ export class ConsolidatedMCQModal extends Modal { */ displayCurrentQuestion(containerEl: HTMLElement): void { const questionIndex = this.currentQuestionIndex; - + if (questionIndex >= this.allQuestions.length) { this.completeReview(); return; } - + const question = this.allQuestions[questionIndex]; - + // Verify we have a valid question if (!question || !question.choices || question.choices.length < 2) { - new Notice('Error: Invalid question data. Moving to next question.'); - + new Notice('Error: Invalid question data. Moving to next question.'); // eslint-disable-line obsidianmd/ui/sentence-case + // Skip to next question this.currentQuestionIndex++; if (this.currentQuestionIndex < this.allQuestions.length) { @@ -224,51 +224,51 @@ export class ConsolidatedMCQModal extends Modal { } return; } - + // Clear previous question const questionContainer = containerEl.querySelector('.mcq-question-container'); if (questionContainer) { questionContainer.remove(); } - + // Update note info const noteInfoEl = containerEl.querySelector('.mcq-note-info'); if (noteInfoEl instanceof HTMLElement) { noteInfoEl.setText(`Question from: ${question.fileName}`); } - + // Create question container const newQuestionContainer = containerEl.createDiv('mcq-question-container'); - + // Display question text const questionEl = newQuestionContainer.createDiv('mcq-question-text'); questionEl.setText(question.question); - + // Display choices const choicesContainer = newQuestionContainer.createDiv('mcq-choices-container'); - + question.choices.forEach((choice, index) => { const choiceEl = choicesContainer.createDiv('mcq-choice'); - + // Create choice button const choiceBtn = choiceEl.createEl('button', { cls: 'mcq-choice-btn' }); - + // Add letter label const letterLabel = choiceBtn.createSpan('mcq-choice-letter'); letterLabel.setText(String.fromCharCode(65 + index) + ') '); - + // Add choice text const textSpan = choiceBtn.createSpan('mcq-choice-text'); textSpan.setText(choice || '(Empty choice)'); // Handle empty choices - + choiceBtn.addEventListener('click', () => { this.handleAnswer(index); }); }); } - + /** * Handle user's answer selection * @@ -278,15 +278,15 @@ export class ConsolidatedMCQModal extends Modal { const questionIndex = this.currentQuestionIndex; const question = this.allQuestions[questionIndex]; const isCorrect = selectedIndex === question.correctAnswerIndex; - + // Calculate time to answer const timeToAnswer = (Date.now() - this.questionStartTime) / 1000; - + // Check if this question has been answered before const existingAnswerIndex = this.answers.findIndex( a => a.questionIndex === questionIndex ); - + let answer: { questionIndex: number; selectedAnswerIndex: number; @@ -296,15 +296,15 @@ export class ConsolidatedMCQModal extends Modal { notePath: string; fileName: string; }; - + if (existingAnswerIndex >= 0) { // Update existing answer answer = this.answers[existingAnswerIndex]; - + // Always update selected index and correctness based on the current attempt answer.selectedAnswerIndex = selectedIndex; - answer.correct = isCorrect; - + answer.correct = isCorrect; + // Always update the timing and attempts answer.timeToAnswer = timeToAnswer; answer.attempts += 1; @@ -321,29 +321,29 @@ export class ConsolidatedMCQModal extends Modal { }; this.answers.push(answer); } - + // Highlight the selected answer this.highlightAnswer(selectedIndex, isCorrect); - + // Wait a moment before proceeding window.setTimeout(() => { if (isCorrect) { // Move to next question if correct this.currentQuestionIndex++; this.questionStartTime = Date.now(); - + const { contentEl } = this; - + // Update progress const progressEl = contentEl.querySelector('.mcq-progress'); if (progressEl instanceof HTMLElement) { progressEl.textContent = `Question ${this.currentQuestionIndex + 1} of ${this.allQuestions.length} (${this.allQuestions.length} questions from ${this.mcqSets.length} notes)`; } - + // Update progress percentage for the progress bar const newProgressPercent = Math.round(((this.currentQuestionIndex + 1) / this.allQuestions.length) * 100); contentEl.setAttribute('data-progress', newProgressPercent.toString()); - + // Show next question or complete review if (this.currentQuestionIndex < this.allQuestions.length) { this.displayCurrentQuestion(contentEl); @@ -353,19 +353,19 @@ export class ConsolidatedMCQModal extends Modal { } else { // For incorrect answers, show a hint new Notice("Incorrect answer. Try again to proceed to the next question."); - + // Remove the highlight after a short delay window.setTimeout(() => { const choiceButtons = document.querySelectorAll('.mcq-choice-btn'); if (choiceButtons.length <= selectedIndex) return; - + const selectedBtn = choiceButtons[selectedIndex] as HTMLElement; selectedBtn.classList.remove('mcq-choice-incorrect'); }, 500); } }, 1000); } - + /** * Highlight the selected answer * @@ -374,11 +374,11 @@ export class ConsolidatedMCQModal extends Modal { */ highlightAnswer(selectedIndex: number, isCorrect: boolean): void { const choiceButtons = document.querySelectorAll('.mcq-choice-btn'); - + if (choiceButtons.length <= selectedIndex) return; - + const selectedBtn = choiceButtons[selectedIndex] as HTMLElement; - + // Add highlight to selected answer if (isCorrect) { // For correct answers, highlight it green @@ -388,7 +388,7 @@ export class ConsolidatedMCQModal extends Modal { selectedBtn.classList.add('mcq-choice-incorrect'); } } - + /** * Complete the review and show results */ @@ -401,7 +401,7 @@ export class ConsolidatedMCQModal extends Modal { notePath: string; fileName: string; }> = {}; - + // Ensure all questions have an answer entry, even if not attempted or skipped // This is important for the detailed breakdown later. // We assume that if a question is in allQuestions but not in this.answers, @@ -420,7 +420,7 @@ export class ConsolidatedMCQModal extends Modal { fileName: answer.fileName }; } - + // Each answer corresponds to one question attempt from that note // We need to count unique questions from each note that were answered. // The current logic for totalQuestions in noteScores might be slightly off @@ -435,7 +435,7 @@ export class ConsolidatedMCQModal extends Modal { // Recalculate totalQuestions per note based on allQuestions for (const question of this.allQuestions) { if (!noteScores[question.notePath]) { - noteScores[question.notePath] = { + noteScores[question.notePath] = { totalQuestions: 0, correctAnswers: 0, score: 0, @@ -445,7 +445,7 @@ export class ConsolidatedMCQModal extends Modal { } noteScores[question.notePath].totalQuestions++; } - + // Calculate correct answers based on the final recorded answer for each question const finalAnswersCorrectCount: Record = {}; for (const question of this.allQuestions) { @@ -459,12 +459,12 @@ export class ConsolidatedMCQModal extends Modal { // For scoring, we usually care about first-attempt correctness. // The current logic `answer.correct && answer.attempts <= 1` is fine for scoring. if (answer.correct && answer.attempts <= 1) { - if (noteScores[answer.notePath]) { // Ensure noteScore entry exists + if (noteScores[answer.notePath]) { // Ensure noteScore entry exists noteScores[answer.notePath].correctAnswers++; } } } - + // Calculate scores for (const notePath in noteScores) { const noteScore = noteScores[notePath]; @@ -475,7 +475,7 @@ export class ConsolidatedMCQModal extends Modal { noteScore.score = 0; // No questions for this note, or no answers recorded } } - + // Convert note scores to results for the callback const results: Array<{ path: string; @@ -483,29 +483,29 @@ export class ConsolidatedMCQModal extends Modal { response: ReviewResponse; score?: number; }> = []; - + for (const notePath in noteScores) { const noteScore = noteScores[notePath]; const score = noteScore.score; - + // Determine success and response based on score let success = false; - let response = ReviewResponse.Hard; - + let response = ReviewResponse.IncorrectResponse; + if (score >= 0.9) { success = true; - response = ReviewResponse.Perfect; + response = ReviewResponse.PerfectRecall; } else if (score >= 0.7) { success = true; - response = ReviewResponse.Good; + response = ReviewResponse.CorrectWithHesitation; } else if (score >= 0.5) { success = true; - response = ReviewResponse.Fair; + response = ReviewResponse.CorrectWithDifficulty; } else { success = false; - response = ReviewResponse.Hard; + response = ReviewResponse.IncorrectResponse; } - + results.push({ path: notePath, success, @@ -513,41 +513,41 @@ export class ConsolidatedMCQModal extends Modal { score }); } - + // Call the completion callback this.onComplete(results); - + // Show results const { contentEl } = this; contentEl.empty(); - + // Display results header with stylized heading - const headerEl = new Setting(contentEl).setName('MCQ Review Complete').setHeading().setClass('mcq-review-complete-header'); + new Setting(contentEl).setName('MCQ review complete').setHeading().setClass('mcq-review-complete-header'); // eslint-disable-line obsidianmd/ui/sentence-case // Display overall score with enhanced styling const totalCorrectOverall = this.answers.filter(a => a.correct && a.attempts <= 1).length; const totalQuestionsOverall = this.allQuestions.length; const overallScore = totalQuestionsOverall > 0 ? totalCorrectOverall / totalQuestionsOverall : 0; const scorePercentOverall = Math.round(overallScore * 100); - + const scoreEl = contentEl.createDiv('mcq-score'); const scoreTextEl = scoreEl.createDiv('mcq-score-text'); - scoreTextEl.setText(`Overall Score: ${scorePercentOverall}%`); - + scoreTextEl.setText(`Overall score: ${scorePercentOverall}%`); + // Add performance indicator based on score const performanceIndicator = scoreEl.createDiv('mcq-performance-indicator'); if (scorePercentOverall >= 90) { - performanceIndicator.setText('🎓 Excellent Performance!'); + performanceIndicator.setText('🎓 Excellent performance!'); // eslint-disable-line obsidianmd/ui/sentence-case performanceIndicator.addClass('excellent'); } else if (scorePercentOverall >= 70) { - performanceIndicator.setText('👍 Good Work!'); + performanceIndicator.setText('👍 Good work!'); // eslint-disable-line obsidianmd/ui/sentence-case performanceIndicator.addClass('good'); } else if (scorePercentOverall >= 50) { - performanceIndicator.setText('🔄 Keep Practicing'); + performanceIndicator.setText('🔄 Keep practicing'); // eslint-disable-line obsidianmd/ui/sentence-case performanceIndicator.addClass('needs-improvement'); } else { - performanceIndicator.setText('📚 More Review Recommended'); + performanceIndicator.setText('📚 More review recommended'); // eslint-disable-line obsidianmd/ui/sentence-case performanceIndicator.addClass('review-recommended'); } @@ -557,22 +557,22 @@ export class ConsolidatedMCQModal extends Modal { // Display note scores with enhanced styling const noteScoresEl = contentEl.createDiv('mcq-note-scores'); - const scoreHeading = new Setting(noteScoresEl).setHeading().setName('Scores by Note').settingEl; - + new Setting(noteScoresEl).setHeading().setName('Scores by note'); + // Sort notes by score for better visualization (highest first) const sortedNotes = Object.keys(noteScores).sort((a, b) => noteScores[b].score - noteScores[a].score); - + for (const notePath of sortedNotes) { const noteScore = noteScores[notePath]; if (noteScore.totalQuestions === 0) continue; // Skip notes with no questions in the review set const noteScoreEl = noteScoresEl.createDiv('mcq-note-score'); - + noteScoreEl.createEl('div', { text: noteScore.fileName, cls: 'mcq-note-score-title' }); - + const scorePercent = Math.round(noteScore.score * 100); const scoreTextValueEl = noteScoreEl.createEl('div', { text: `Score: ${scorePercent}% (${noteScore.correctAnswers}/${noteScore.totalQuestions})`, @@ -604,7 +604,7 @@ export class ConsolidatedMCQModal extends Modal { // --- Detailed Question Breakdown --- const breakdownContainer = contentEl.createDiv('mcq-detailed-breakdown'); - new Setting(breakdownContainer).setHeading().setName('Detailed Question Breakdown'); + new Setting(breakdownContainer).setHeading().setName('Detailed question breakdown'); this.allQuestions.forEach((question, index) => { const questionEl = breakdownContainer.createDiv('mcq-breakdown-item'); @@ -647,15 +647,15 @@ export class ConsolidatedMCQModal extends Modal { correctAnswerEl.createSpan({ text: 'Correct answer: ' }); correctAnswerEl.createSpan({ text: question.choices[question.correctAnswerIndex], cls: 'mcq-correct-answer-text' }); }); - + // Create close button - const closeBtn = contentEl.createEl('button', {cls: 'mcq-close-btn', text: 'Close'}); + const closeBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' }); closeBtn.addEventListener('click', () => { this.close(); }); } - + /** * Shuffle an array * diff --git a/ui/context-menu.ts b/ui/context-menu.ts index ed29623..1d9be3a 100644 --- a/ui/context-menu.ts +++ b/ui/context-menu.ts @@ -58,27 +58,27 @@ export class ContextMenuHandler { menu.addItem((item) => { item.setTitle(isScheduled ? "Update review schedule" : "Add to review schedule") - .setIcon("calendar-plus") - .onClick(async () => { - await this.plugin.reviewScheduleService.scheduleNoteForReview(file.path); - await this.plugin.savePluginData(); - }); + .setIcon("calendar-plus") + .onClick(async () => { + this.plugin.reviewScheduleService.scheduleNoteForReview(file.path); + await this.plugin.savePluginData(); + }); }); if (isScheduled) { menu.addItem((item) => { item.setTitle("Review now") - .setIcon("eye") - .onClick(() => this.plugin.reviewController.reviewNote(file.path)); + .setIcon("eye") + .onClick(() => this.plugin.reviewController.reviewNote(file.path)); }); menu.addItem((item) => { item.setTitle("Remove from review") - .setIcon("calendar-minus") - .onClick(async () => { - await this.plugin.reviewScheduleService.removeFromReview(file.path); - await this.plugin.savePluginData(); - }); + .setIcon("calendar-minus") + .onClick(async () => { + this.plugin.reviewScheduleService.removeFromReview(file.path); + await this.plugin.savePluginData(); + }); }); } @@ -86,18 +86,18 @@ export class ContextMenuHandler { if (file.parent) { // Ensure there is a parent folder menu.addItem((item) => { item.setTitle("Add note's folder to review schedule") - .setIcon("folder-plus") // Using a folder icon - .onClick(async () => { - if (file.parent) { // Parent already confirmed, but good for safety - new Notice(`Adding folder "${file.parent.name}" to review schedule...`); - // Ensure file.parent is indeed a TFolder before calling addFolderToReview - if (file.parent instanceof TFolder) { - await this.addFolderToReview(file.parent); // Call the existing folder logic - } else { - new Notice("Error: Parent is not a folder."); - } - } - }); + .setIcon("folder-plus") // Using a folder icon + .onClick(async () => { + if (file.parent) { // Parent already confirmed, but good for safety + new Notice(`Adding folder "${file.parent.name}" to review schedule...`); + // Ensure file.parent is indeed a TFolder before calling addFolderToReview + if (file.parent instanceof TFolder) { + await this.addFolderToReview(file.parent); // Call the existing folder logic + } else { + new Notice("Error: parent is not a folder."); + } + } + }); }); } } @@ -112,12 +112,13 @@ export class ContextMenuHandler { try { menu.addItem((item) => { item.setTitle("Add folder to review") - .setIcon("calendar-plus") - .onClick(() => { - this.addFolderToReview(folder); - }); + .setIcon("calendar-plus") + .onClick(() => { + void this.addFolderToReview(folder); + }); }); - } catch (error) { + } catch { + // no-op } } @@ -146,12 +147,12 @@ export class ContextMenuHandler { }); if (allFiles.length === 0) { - new Notice("No markdown files found in folder."); + new Notice("No Markdown files found in folder."); return; } // Use the LinkAnalyzer to build a hierarchical structure with consistent main file selection - const includeSubfolders = this.plugin.settings.includeSubfolders; + // Use the LinkAnalyzer to build a hierarchical structure with consistent main file selection // First, identify a potential main file that should be used as the starting point let mainFilePath: string | null = null; @@ -197,7 +198,7 @@ export class ContextMenuHandler { } } - let traversalOrder: string[] = []; + const traversalOrder: string[] = []; const visited = new Set(); // Global visited set const processLinksRecursively = async (path: string) => { @@ -210,13 +211,13 @@ export class ContextMenuHandler { const links = await LinkAnalyzer.analyzeNoteLinks( this.plugin.app.vault, path, - false + false ); for (const link of links) { const linkFile = this.plugin.app.vault.getAbstractFileByPath(link); if (!(linkFile instanceof TFile) || linkFile.extension !== 'md') { - continue; + continue; } if (allFiles.some(f => f.path === linkFile.path)) { // Is internal to the overall operation @@ -238,16 +239,16 @@ export class ContextMenuHandler { // Then, iterate through all files (sorted for consistency) to catch other branches or unlinked files. // This ensures that if mainFilePath didn't cover everything, or if there wasn't one, // all top-level items and their linked children within allFiles are processed. - const sortedAllFiles = [...allFiles].sort((a,b) => a.path.localeCompare(b.path)); + const sortedAllFiles = [...allFiles].sort((a, b) => a.path.localeCompare(b.path)); for (const file of sortedAllFiles) { if (!visited.has(file.path)) { await processLinksRecursively(file.path); } } - + // Schedule notes in the traversal order using the service - const count = await this.plugin.reviewScheduleService.scheduleNotesInOrder(traversalOrder); + const count = this.plugin.reviewScheduleService.scheduleNotesInOrder(traversalOrder); if (count > 0) { await this.plugin.savePluginData(); // Add save call } @@ -259,9 +260,9 @@ export class ContextMenuHandler { new Notice(`Added ${count} notes from "${folder.name}" to review schedule, starting with ${startingFileName}`); // Update today's notes in the review controller using our same methodology - await this.plugin.reviewController.updateTodayNotes(); + this.plugin.reviewController.updateTodayNotes(); - } catch (error) { + } catch { new Notice("Error adding folder to review schedule"); } } @@ -281,7 +282,7 @@ export class ContextMenuHandler { ); // Save after session creation if (session) { - await this.plugin.savePluginData(); // Add save call + await this.plugin.savePluginData(); // Add save call } if (!session) { @@ -305,7 +306,7 @@ export class ContextMenuHandler { // We'll add the save call here after scheduleSessionForReview returns. const scheduledCount = await this.plugin.reviewSessionService.scheduleSessionForReview(session.id); if (scheduledCount > 0) { - await this.plugin.savePluginData(); // Add save call + await this.plugin.savePluginData(); // Add save call } // Show success message @@ -314,7 +315,7 @@ export class ContextMenuHandler { // Open the first file for review const file = this.plugin.app.vault.getAbstractFileByPath(firstFilePath); if (file instanceof TFile) { - this.plugin.reviewController.reviewNote(firstFilePath); + void this.plugin.reviewController.reviewNote(firstFilePath); } } else { new Notice("No files found for review in this folder."); diff --git a/ui/event-modal.ts b/ui/event-modal.ts index 552f4e3..2c1ad08 100644 --- a/ui/event-modal.ts +++ b/ui/event-modal.ts @@ -9,7 +9,7 @@ export class EventModal extends Modal { plugin: SpaceforgePlugin; event: CalendarEvent | null; onSave: (event: CalendarEvent) => void; - + // Form elements titleInput: HTMLInputElement; descriptionInput: HTMLTextAreaElement; @@ -21,7 +21,7 @@ export class EventModal extends Modal { isAllDayToggle: HTMLInputElement; recurrenceSelect: HTMLSelectElement; recurrenceEndDateInput: HTMLInputElement; - + constructor(app: App, plugin: SpaceforgePlugin, event: CalendarEvent | null = null, onSave: (event: CalendarEvent) => void) { super(app); this.plugin = plugin; @@ -33,26 +33,26 @@ export class EventModal extends Modal { const { contentEl } = this; contentEl.empty(); contentEl.addClass("event-modal"); - + // Compact header const header = contentEl.createDiv("event-modal-header"); const headerIcon = header.createDiv("event-modal-header-icon"); setIcon(headerIcon, this.event ? "edit" : "calendar-plus"); - + const headerText = header.createDiv("event-modal-header-text"); const titleSetting = new Setting(headerText) .setHeading() - .setName(this.event ? "Edit Event" : "New Event"); + .setName(this.event ? "Edit event" : "New event"); titleSetting.settingEl.addClass("event-modal-title"); - + // Compact form container const formContainer = contentEl.createDiv("event-modal-form"); - + // Title row const titleRow = formContainer.createDiv("event-modal-row"); const titleGroup = titleRow.createDiv("event-modal-field-group"); titleGroup.createEl("label", { text: "Title", cls: "event-modal-label required" }); - this.titleInput = titleGroup.createEl("input", { + this.titleInput = titleGroup.createEl("input", { type: "text", cls: "event-modal-input", placeholder: "Event title..." @@ -66,7 +66,7 @@ export class EventModal extends Modal { const dateTimeRow = formContainer.createDiv("event-modal-row"); const dateGroup = dateTimeRow.createDiv("event-modal-field-group"); dateGroup.createEl("label", { text: "Date", cls: "event-modal-label" }); - this.dateInput = dateGroup.createEl("input", { + this.dateInput = dateGroup.createEl("input", { type: "date", cls: "event-modal-input" }); @@ -77,10 +77,10 @@ export class EventModal extends Modal { this.dateInput.value = new Date().toISOString().split('T')[0]; } this.dateInput.addEventListener("change", () => this.validateForm()); - + const timeGroup = dateTimeRow.createDiv("event-modal-field-group"); timeGroup.createEl("label", { text: "Time", cls: "event-modal-label" }); - this.timeInput = timeGroup.createEl("input", { + this.timeInput = timeGroup.createEl("input", { type: "time", cls: "event-modal-input" }); @@ -92,14 +92,14 @@ export class EventModal extends Modal { // All-day toggle row const allDayRow = formContainer.createDiv("event-modal-row"); const allDayGroup = allDayRow.createDiv("event-modal-field-group"); - this.isAllDayToggle = allDayGroup.createEl("input", { + this.isAllDayToggle = allDayGroup.createEl("input", { type: "checkbox", cls: "event-modal-checkbox" }) as HTMLInputElement; this.isAllDayToggle.checked = this.event?.isAllDay ?? false; this.isAllDayToggle.addEventListener("change", () => this.toggleTimeInput()); - - const allDayLabel = allDayGroup.createEl("label", { + + allDayGroup.createEl("label", { text: "All-day event", cls: "event-modal-checkbox-label" }); @@ -108,7 +108,7 @@ export class EventModal extends Modal { const locationRow = formContainer.createDiv("event-modal-row"); const locationGroup = locationRow.createDiv("event-modal-field-group"); locationGroup.createEl("label", { text: "Location", cls: "event-modal-label" }); - this.locationInput = locationGroup.createEl("input", { + this.locationInput = locationGroup.createEl("input", { type: "text", cls: "event-modal-input", placeholder: "Location (optional)..." @@ -121,34 +121,34 @@ export class EventModal extends Modal { const categoryColorRow = formContainer.createDiv("event-modal-row"); const categoryGroup = categoryColorRow.createDiv("event-modal-field-group"); categoryGroup.createEl("label", { text: "Category", cls: "event-modal-label" }); - this.categorySelect = categoryGroup.createEl("select", { + this.categorySelect = categoryGroup.createEl("select", { cls: "event-modal-select" }); - + Object.values(EventCategory).forEach(category => { - const option = this.categorySelect.createEl("option", { + const option = this.categorySelect.createEl("option", { value: category, text: category.charAt(0).toUpperCase() + category.slice(1) }); this.categorySelect.appendChild(option); }); - + if (this.event?.category) { this.categorySelect.value = this.event.category; } else { this.categorySelect.value = this.plugin.settings.defaultEventCategory || 'personal'; } - + this.categorySelect.addEventListener("change", () => this.updateColorFromCategory()); - + const colorGroup = categoryColorRow.createDiv("event-modal-field-group"); colorGroup.createEl("label", { text: "Color", cls: "event-modal-label" }); const colorPickerContainer = colorGroup.createDiv("event-modal-color-picker-compact"); - this.colorInput = colorPickerContainer.createEl("input", { + this.colorInput = colorPickerContainer.createEl("input", { type: "color", cls: "event-modal-color-input" }) as HTMLInputElement; - + if (this.event?.color) { this.colorInput.value = this.event.color; } else { @@ -159,7 +159,7 @@ export class EventModal extends Modal { const descriptionRow = formContainer.createDiv("event-modal-row event-modal-full-width"); const descriptionGroup = descriptionRow.createDiv("event-modal-field-group"); descriptionGroup.createEl("label", { text: "Description", cls: "event-modal-label" }); - this.descriptionInput = descriptionGroup.createEl("textarea", { + this.descriptionInput = descriptionGroup.createEl("textarea", { cls: "event-modal-textarea", placeholder: "Event details (optional)..." }); @@ -172,31 +172,31 @@ export class EventModal extends Modal { const recurrenceRow = formContainer.createDiv("event-modal-row"); const recurrenceGroup = recurrenceRow.createDiv("event-modal-field-group"); recurrenceGroup.createEl("label", { text: "Recurrence", cls: "event-modal-label" }); - this.recurrenceSelect = recurrenceGroup.createEl("select", { + this.recurrenceSelect = recurrenceGroup.createEl("select", { cls: "event-modal-select" }); - + Object.values(EventRecurrence).forEach(recurrence => { - const option = this.recurrenceSelect.createEl("option", { + const option = this.recurrenceSelect.createEl("option", { value: recurrence, text: this.getRecurrenceDisplayText(recurrence) }); this.recurrenceSelect.appendChild(option); }); - + if (this.event?.recurrence) { this.recurrenceSelect.value = this.event.recurrence; } else { this.recurrenceSelect.value = EventRecurrence.None; } - + this.recurrenceSelect.addEventListener("change", () => this.toggleRecurrenceEndDate()); - + // Recurrence end date row (conditional) const recurrenceEndRow = formContainer.createDiv("event-modal-row event-modal-recurrence-end-row"); const recurrenceEndGroup = recurrenceEndRow.createDiv("event-modal-field-group"); - recurrenceEndGroup.createEl("label", { text: "End Date", cls: "event-modal-label" }); - this.recurrenceEndDateInput = recurrenceEndGroup.createEl("input", { + recurrenceEndGroup.createEl("label", { text: "End date", cls: "event-modal-label" }); + this.recurrenceEndDateInput = recurrenceEndGroup.createEl("input", { type: "date", cls: "event-modal-input" }); @@ -207,27 +207,27 @@ export class EventModal extends Modal { // Enhanced buttons section const buttonContainer = contentEl.createDiv("event-modal-actions"); - - const cancelButton = buttonContainer.createEl("button", { + + const cancelButton = buttonContainer.createEl("button", { text: "Cancel", cls: "event-modal-btn event-modal-btn-secondary" }); cancelButton.addEventListener("click", () => this.close()); - + // Add delete button only for existing events if (this.event) { - const deleteButton = buttonContainer.createEl("button", { + const deleteButton = buttonContainer.createEl("button", { text: "Delete", cls: "event-modal-btn event-modal-btn-danger" }); - deleteButton.addEventListener("click", () => this.deleteEvent()); + deleteButton.addEventListener("click", () => void this.deleteEvent()); } - - const saveButton = buttonContainer.createEl("button", { - text: this.event ? "Update Event" : "Create Event", + + const saveButton = buttonContainer.createEl("button", { + text: this.event ? "Update event" : "Create event", cls: "event-modal-btn event-modal-btn-primary" }); - saveButton.addEventListener("click", () => this.saveEvent()); + saveButton.addEventListener("click", () => void this.saveEvent()); // Initial setup this.validateForm(); @@ -248,7 +248,7 @@ export class EventModal extends Modal { private validateForm(): void { const saveButton = this.contentEl.querySelector(".event-modal-btn-primary") as HTMLButtonElement; const isValid = this.titleInput.value.trim() !== "" && this.dateInput.value !== ""; - + if (saveButton) { saveButton.disabled = !isValid; } @@ -280,11 +280,11 @@ export class EventModal extends Modal { private toggleTimeInput(): void { const isAllDay = this.isAllDayToggle.checked; const timeContainer = this.timeInput.closest(".event-modal-input-group") as HTMLElement; - + if (timeContainer) { timeContainer.style.display = isAllDay ? "none" : "block"; } - + if (isAllDay) { this.timeInput.value = ""; } @@ -307,11 +307,11 @@ export class EventModal extends Modal { private toggleRecurrenceEndDate(): void { const recurrence = this.recurrenceSelect.value as EventRecurrence; const endDateContainer = this.recurrenceEndDateInput.closest(".event-modal-recurrence-end-container") as HTMLElement; - + if (endDateContainer) { endDateContainer.style.display = recurrence === EventRecurrence.None ? "none" : "block"; } - + if (recurrence === EventRecurrence.None) { this.recurrenceEndDateInput.value = ""; } @@ -336,17 +336,17 @@ export class EventModal extends Modal { */ private async deleteEvent(): Promise { if (!this.event) return; - + try { const deleted = this.plugin.calendarEventService?.deleteEvent(this.event.id); - + if (deleted) { new Notice("Event deleted successfully"); await this.plugin.savePluginData(); - + // Trigger calendar refresh through the onSave callback this.onSave(this.event); - + this.close(); } else { new Notice("Failed to delete event"); @@ -396,7 +396,7 @@ export class EventModal extends Modal { recurrence, recurrenceEndDate }; - + const updatedEvent = this.plugin.calendarEventService?.updateEvent(this.event.id, eventData); if (updatedEvent) { this.onSave(updatedEvent); @@ -416,7 +416,7 @@ export class EventModal extends Modal { recurrence, recurrenceEndDate }; - + const newEvent = this.plugin.calendarEventService?.createEvent(eventData); if (newEvent) { this.onSave(newEvent); @@ -426,7 +426,7 @@ export class EventModal extends Modal { // Save plugin data await this.plugin.savePluginData(); - + this.close(); } catch (error) { console.error("Error saving event:", error); diff --git a/ui/mcq-modal.ts b/ui/mcq-modal.ts index 883f9cb..044c53a 100644 --- a/ui/mcq-modal.ts +++ b/ui/mcq-modal.ts @@ -1,6 +1,6 @@ import { Modal, Notice, setIcon, TFile, Setting } from 'obsidian'; // Ensure TFile is imported, Added Setting import SpaceforgePlugin from '../main'; -import { MCQQuestion, MCQSet, MCQAnswer, MCQSession } from '../models/mcq'; +import { MCQSet, MCQAnswer, MCQSession } from '../models/mcq'; /** * Modal for displaying and answering MCQs @@ -10,11 +10,11 @@ export class MCQModal extends Modal { notePath: string; mcqSet: MCQSet; session: MCQSession; - questionStartTime: number = 0; - isFreshGeneration: boolean = false; + questionStartTime = 0; + isFreshGeneration = false; // Modified callback to include score private onCompleteCallback: ((path: string, score: number, completed: boolean) => void) | null; - private selectedAnswerIndex: number = -1; + private selectedAnswerIndex = -1; constructor( plugin: SpaceforgePlugin, @@ -45,37 +45,39 @@ export class MCQModal extends Modal { contentEl.empty(); contentEl.addClass('spaceforge-mcq-modal'); const headerContainer = contentEl.createDiv('mcq-header-container'); - new Setting(headerContainer).setName('Multiple Choice Review').setHeading(); + new Setting(headerContainer).setName('Multiple choice review').setHeading(); if (!this.isFreshGeneration) { const refreshBtn = headerContainer.createDiv('mcq-refresh-btn'); setIcon(refreshBtn, 'refresh-cw'); refreshBtn.setAttribute('aria-label', 'Generate new questions'); - refreshBtn.addEventListener('click', async () => { + refreshBtn.addEventListener('click', () => { this.close(); - // Use mcqGenerationService - const mcqGenerationService = this.plugin.mcqGenerationService; - if (mcqGenerationService && this.plugin.mcqController) { - const file = this.plugin.app.vault.getAbstractFileByPath(this.notePath); - // Check if file is TFile before reading - if (file instanceof TFile) { - const content = await this.plugin.app.vault.read(file); - const newMcqSet = await mcqGenerationService.generateMCQs(this.notePath, content, this.plugin.settings); - if (newMcqSet) { - // Save via mcqService, then save plugin data - this.plugin.mcqService.saveMCQSet(newMcqSet); - await this.plugin.savePluginData(); - const newModal = new MCQModal(this.plugin, this.notePath, newMcqSet, this.onCompleteCallback); - newModal.open(); + void (async () => { + // Use mcqGenerationService + const mcqGenerationService = this.plugin.mcqGenerationService; + if (mcqGenerationService && this.plugin.mcqController) { + const file = this.plugin.app.vault.getAbstractFileByPath(this.notePath); + // Check if file is TFile before reading + if (file instanceof TFile) { + const content = await this.plugin.app.vault.read(file); + const newMcqSet = await mcqGenerationService.generateMCQs(this.notePath, content, this.plugin.settings); + if (newMcqSet) { + // Save via mcqService, then save plugin data + this.plugin.mcqService.saveMCQSet(newMcqSet); + await this.plugin.savePluginData(); + const newModal = new MCQModal(this.plugin, this.notePath, newMcqSet, this.onCompleteCallback); + newModal.open(); + } else { + new Notice('Failed to regenerate MCQs.'); + } } else { - new Notice('Failed to regenerate MCQs.'); + new Notice('Could not find note file to regenerate MCQs.'); } } else { - new Notice('Could not find note file to regenerate MCQs.'); + new Notice('MCQ generation service not available.'); // eslint-disable-line obsidianmd/ui/sentence-case } - } else { - new Notice('MCQ generation service not available.'); - } + })(); }); } @@ -146,7 +148,7 @@ export class MCQModal extends Modal { } if (processAnswer && this.selectedAnswerIndex !== -1) { - if (this.selectedAnswerIndex < choiceButtons.length) { + if (this.selectedAnswerIndex < choiceButtons.length) { const button = choiceButtons[this.selectedAnswerIndex] as HTMLElement; button.classList.add('mcq-key-pressed'); window.setTimeout(() => { @@ -155,8 +157,8 @@ export class MCQModal extends Modal { this.selectedAnswerIndex = -1; }, 150); } else { - this.handleAnswer(this.selectedAnswerIndex); - this.selectedAnswerIndex = -1; + this.handleAnswer(this.selectedAnswerIndex); + this.selectedAnswerIndex = -1; } } }; @@ -170,27 +172,27 @@ export class MCQModal extends Modal { this.scope.register([], i.toString(), keyDownHandler); } for (let i = 0; i < 9; i++) { // A-I - this.scope.register([], String.fromCharCode(65 + i), keyDownHandler); + this.scope.register([], String.fromCharCode(65 + i), keyDownHandler); } // Cleanup handled by Modal's onClose automatically if using this.scope.register // Original onClose logic for saving partial progress: const originalOnClose = this.onClose; this.onClose = () => { - originalOnClose.call(this); + originalOnClose.call(this); if (!this.session.completed && this.session.answers.length > 0) { // Session was closed prematurely but some answers were given - this.session.completedAt = Date.now(); + this.session.completedAt = Date.now(); this.calculateScore(); // Calculate score based on answers so far this.plugin.mcqService.saveMCQSession(this.session); - this.plugin.savePluginData(); + void this.plugin.savePluginData(); if (this.onCompleteCallback) { // Indicate it was not fully completed if onCompleteCallback expects a 'completed' flag - this.onCompleteCallback(this.notePath, this.session.score, false); + this.onCompleteCallback(this.notePath, this.session.score, false); } } else if (!this.session.completed && this.onCompleteCallback) { // Closed before any answers or completion - this.onCompleteCallback(this.notePath, 0, false); + this.onCompleteCallback(this.notePath, 0, false); } }; } @@ -203,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.'); + new Notice('Error: Invalid question data. Moving to next question.'); // eslint-disable-line obsidianmd/ui/sentence-case this.session.currentQuestionIndex++; if (this.session.currentQuestionIndex < this.mcqSet.questions.length) { this.displayCurrentQuestion(containerEl); @@ -222,12 +224,12 @@ export class MCQModal extends Modal { const existingAnswer = this.session.answers.find(a => a.questionIndex === questionIndex); if (existingAnswer && existingAnswer.attempts >= 2) { const skipContainer = newQuestionContainer.createDiv('mcq-skip-container'); - const skipButton = skipContainer.createEl('button', { text: 'Show Answer & Continue', cls: 'mcq-skip-button' }); + const skipButton = skipContainer.createEl('button', { text: 'Show answer & continue', cls: 'mcq-skip-button' }); skipButton.addEventListener('click', () => { const correctIndex = question.correctAnswerIndex; const correctAnswerDisplay = newQuestionContainer.createDiv('mcq-correct-answer-display'); const correctLabel = correctAnswerDisplay.createDiv({ cls: 'sf-mcq-correct-label' }); - correctLabel.setText('Correct Answer:'); + correctLabel.setText('Correct answer:'); const correctText = correctAnswerDisplay.createDiv({ cls: 'sf-mcq-correct-text' }); correctText.setText(String.fromCharCode(65 + correctIndex) + ') ' + question.choices[correctIndex]); @@ -246,7 +248,7 @@ export class MCQModal extends Modal { } const continueBtn = correctAnswerDisplay.createEl('button', { - text: 'Continue to Next Question', + text: 'Continue to next question', cls: 'mcq-continue-button' }); continueBtn.addEventListener('click', () => { @@ -337,9 +339,9 @@ export class MCQModal extends Modal { this.session.completedAt = Date.now(); // Save via mcqService this.plugin.mcqService.saveMCQSession(this.session); - this.plugin.savePluginData(); // Persist + void this.plugin.savePluginData(); // Persist - new Setting(contentEl).setName('Review Complete').setHeading(); + new Setting(contentEl).setName('Review complete').setHeading(); const scoreEl = contentEl.createDiv('mcq-score'); const scoreTextEl = scoreEl.createDiv('mcq-score-text'); const scorePercentage = this.session.score; // Score is 0-1 @@ -391,7 +393,7 @@ export class MCQModal extends Modal { // Removed the old score indicator text (🎯 Excellent, etc.) const resultsEl = contentEl.createDiv('mcq-results'); - new Setting(resultsEl).setHeading().setName('Question Results'); + new Setting(resultsEl).setHeading().setName('Question results'); if (this.session.answers.length === 0) { resultsEl.createDiv({ cls: 'mcq-no-answers', text: 'No questions were answered in this session.' }); } else { @@ -428,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) { /* Error displaying answer result: ${error} */ } }); } const closeBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' }); @@ -439,9 +441,9 @@ export class MCQModal extends Modal { } this.close(); }); - } catch (error) { - new Setting(contentEl).setName('Error Completing Session').setHeading(); - contentEl.createEl('p', { text: 'There was an error completing the MCQ session. Please try again.' }); + } catch (_error) { + 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 const errorCloseBtn = contentEl.createEl('button', { cls: 'mcq-close-btn', text: 'Close' }); errorCloseBtn.addEventListener('click', () => this.close()); } @@ -508,12 +510,12 @@ function getFsrsRatingText(rating: number): string { // Helper function to map SM-2 rating number to text function getSm2RatingText(rating: number): string { switch (rating) { - case 0: return "Complete Blackout"; - case 1: return "Incorrect Response"; - case 2: return "Incorrect But Familiar"; - case 3: return "Correct With Difficulty"; - case 4: return "Correct With Hesitation"; - case 5: return "Perfect Recall"; + case 0: return "Complete blackout"; + case 1: return "Incorrect response"; + case 2: return "Incorrect but familiar"; + case 3: return "Correct with difficulty"; + case 4: return "Correct with hHesitation"; + case 5: return "Perfect recall"; default: return "Unknown"; } } \ No newline at end of file diff --git a/ui/review-modal.ts b/ui/review-modal.ts index 048bc84..61904dd 100644 --- a/ui/review-modal.ts +++ b/ui/review-modal.ts @@ -1,8 +1,8 @@ import { App, Modal, Notice, TFile, setIcon, Setting } from 'obsidian'; import SpaceforgePlugin from '../main'; -import { ReviewResponse, ReviewSchedule, FsrsRating } from '../models/review-schedule'; // Added FsrsRating +import { ReviewResponse, FsrsRating } from '../models/review-schedule'; // Added FsrsRating import { State as FsrsState } from 'ts-fsrs'; // For displaying FSRS state name -import { MCQController } from '../controllers/review-controller-mcq'; // Import MCQController for type hinting + /** * Modal for reviewing a note @@ -19,7 +19,7 @@ export class ReviewModal extends Modal { onOpen(): void { const { contentEl } = this; - new Setting(contentEl).setName("Review Note").setHeading(); + new Setting(contentEl).setName("Review note").setHeading(); const buttonsContainer = contentEl.createDiv("review-buttons-container"); const schedule = this.plugin.reviewScheduleService.schedules[this.path]; @@ -29,7 +29,7 @@ export class ReviewModal extends Modal { const createFsrsButton = (text: string, clsSuffix: string, rating: FsrsRating) => { const button = buttonsContainer.createEl("button", { text, cls: `review-button review-button-fsrs-${clsSuffix}` }); button.addEventListener("click", () => { - this.plugin.reviewController.processReviewResponse(this.path, rating); + void this.plugin.reviewController.processReviewResponse(this.path, rating); this.close(); }); }; @@ -42,33 +42,33 @@ export class ReviewModal extends Modal { const createSm2Button = (text: string, cls: string, response: ReviewResponse) => { const button = buttonsContainer.createEl("button", { text, cls }); button.addEventListener("click", () => { - this.plugin.reviewController.processReviewResponse(this.path, response); + void this.plugin.reviewController.processReviewResponse(this.path, response); this.close(); }); }; - createSm2Button("0: Complete Blackout", "review-button review-button-complete-blackout", ReviewResponse.CompleteBlackout); - createSm2Button("1: Incorrect Response", "review-button review-button-incorrect", ReviewResponse.IncorrectResponse); - createSm2Button("2: Incorrect but Familiar", "review-button review-button-incorrect-familiar", ReviewResponse.IncorrectButFamiliar); - createSm2Button("3: Correct with Difficulty", "review-button review-button-correct-difficulty", ReviewResponse.CorrectWithDifficulty); - createSm2Button("4: Correct with Hesitation", "review-button review-button-correct-hesitation", ReviewResponse.CorrectWithHesitation); - createSm2Button("5: Perfect Recall", "review-button review-button-perfect-recall", ReviewResponse.PerfectRecall); + createSm2Button("0: Complete blackout", "review-button review-button-complete-blackout", ReviewResponse.CompleteBlackout); + createSm2Button("1: Incorrect response", "review-button review-button-incorrect", ReviewResponse.IncorrectResponse); + createSm2Button("2: Incorrect but familiar", "review-button review-button-incorrect-familiar", ReviewResponse.IncorrectButFamiliar); + createSm2Button("3: Correct with difficulty", "review-button review-button-correct-difficulty", ReviewResponse.CorrectWithDifficulty); + createSm2Button("4: Correct with hesitation", "review-button review-button-correct-hesitation", ReviewResponse.CorrectWithHesitation); + createSm2Button("5: Perfect recall", "review-button review-button-perfect-recall", ReviewResponse.PerfectRecall); } buttonsContainer.createEl("div", { cls: "review-button-separator" }); // Postpone Button - const postponeButton = buttonsContainer.createEl("button", { text: "Postpone to Tomorrow", cls: "review-button review-button-postpone" }); + const postponeButton = buttonsContainer.createEl("button", { text: "Postpone to tomorrow", cls: "review-button review-button-postpone" }); postponeButton.addEventListener("click", () => { - this.plugin.reviewController.skipReview(this.path); // skipReview handles postponement + void this.plugin.reviewController.skipReview(this.path); // skipReview handles postponement this.close(); }); // Skip/Next Button - const skipButton = buttonsContainer.createEl("button", { text: "Skip/Next", cls: "review-button review-button-skip" }); - skipButton.addEventListener("click", async () => { + const skipButton = buttonsContainer.createEl("button", { text: "Skip/next", cls: "review-button review-button-skip" }); + skipButton.addEventListener("click", () => { this.close(); if (this.plugin.navigationController) { - await this.plugin.navigationController.navigateToNextNoteWithoutRating(); + void this.plugin.navigationController.navigateToNextNoteWithoutRating(); } }); @@ -76,15 +76,16 @@ export class ReviewModal extends Modal { if (this.plugin.settings.enableMCQ) { buttonsContainer.createEl("div", { cls: "review-button-separator" }); + 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"); + const textSpan = mcqButton.createSpan("mcq-button-text"); textSpan.setText("Test with MCQs"); // eslint-disable-line obsidianmd/ui/sentence-case mcqButton.addEventListener("click", () => { const mcqController = this.plugin.mcqController; if (mcqController) { // Call startMCQReview without the callback - mcqController.startMCQReview(this.path); + void mcqController.startMCQReview(this.path); // The logic for handling completion should ideally be managed elsewhere, // perhaps by observing an event or checking state after the modal closes, // or the MCQModal itself could trigger the review processing upon completion. @@ -96,10 +97,10 @@ export class ReviewModal extends Modal { const initializedMcqController = this.plugin.mcqController; if (initializedMcqController) { // Call startMCQReview without the callback - initializedMcqController.startMCQReview(this.path); + void initializedMcqController.startMCQReview(this.path); this.close(); } else { - new Notice("MCQ feature could not be initialized. Please check settings."); + new Notice("MCQ feature could not be initialized. Please check settings."); // eslint-disable-line obsidianmd/ui/sentence-case } } }); @@ -109,19 +110,21 @@ 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"); + const refreshTextSpan = refreshMcqButton.createSpan("mcq-button-text"); refreshTextSpan.setText("Generate new MCQs"); // eslint-disable-line obsidianmd/ui/sentence-case - refreshMcqButton.addEventListener("click", async () => { + refreshMcqButton.addEventListener("click", () => { if (mcqController) { new Notice("Generating new MCQs..."); - const success = await mcqController.generateMCQs(this.path, true); // Force regeneration - if (success) { - // Start review with the newly generated MCQs, without callback - mcqController.startMCQReview(this.path); - this.close(); - } else { - new Notice("Failed to generate new MCQs"); - } + void (async () => { + const success = await mcqController.generateMCQs(this.path, true); // Force regeneration + if (success) { + // Start review with the newly generated MCQs, without callback + void mcqController.startMCQReview(this.path); + this.close(); + } else { + new Notice("Failed to generate new MCQs."); + } + })(); } }); } @@ -147,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" }); + infoText.createEl("p", { text: `Algorithm: FSRS`, cls: "review-phase-fsrs" }); // eslint-disable-line obsidianmd/ui/sentence-case 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" }); + infoText.createEl("p", { text: `Algorithm: SM-2`, cls: "review-phase-sm2" }); // eslint-disable-line obsidianmd/ui/sentence-case let phaseText: string; let phaseClass: string; if (schedule.scheduleCategory === 'initial') { const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length; diff --git a/ui/settings-tab.ts b/ui/settings-tab.ts index 38093fc..2c8fd3c 100644 --- a/ui/settings-tab.ts +++ b/ui/settings-tab.ts @@ -1,9 +1,9 @@ - -import { App, Notice, PluginSettingTab, Setting, setIcon, TextAreaComponent } from 'obsidian'; // Added TextAreaComponent +import { App, Notice, PluginSettingTab, Setting, setIcon } from 'obsidian'; +import { ConfirmationModal } from './confirmation-modal'; import SpaceforgePlugin from '../main'; // Import ApiProvider, DEFAULT_SETTINGS, SpaceforgeSettings, and MCQQuestionAmountMode -import { ApiProvider, DEFAULT_SETTINGS, SpaceforgeSettings, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; -import { SpaceforgePluginData, DEFAULT_APP_DATA, DEFAULT_PLUGIN_STATE_DATA } from '../models/plugin-data'; // Import data structures +import { ApiProvider, DEFAULT_SETTINGS, MCQQuestionAmountMode, MCQDifficulty } from '../models/settings'; +import { SpaceforgePluginData, DEFAULT_PLUGIN_STATE_DATA } from '../models/plugin-data'; // Import data structures /** * Settings tab for the plugin @@ -13,7 +13,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { * Reference to the main plugin */ plugin: SpaceforgePlugin; - + /** * Initialize settings tab * @@ -24,26 +24,26 @@ export class SpaceforgeSettingTab extends PluginSettingTab { super(app, plugin); this.plugin = plugin; } - + /** * Display settings */ display(): void { const { containerEl } = this; containerEl.empty(); - + // containerEl.createEl('h2', { text: 'Spaceforge Settings' }); // Avoid top-level headings - + // Add a utility for creating collapsible sections const createCollapsible = (title: string, iconName: string, defaultOpen = true) => { // Create section container const sectionContainer = containerEl.createEl('div', { cls: 'sf-settings-section' }); - + // Create heading using Setting.setHeading() const headingSetting = new Setting(sectionContainer) .setName(title) .setHeading(); - + // Add icon if provided if (iconName) { const iconEl = headingSetting.settingEl.createEl('span', { cls: 'sf-settings-icon' }); @@ -51,24 +51,24 @@ export class SpaceforgeSettingTab extends PluginSettingTab { // Insert icon before the name headingSetting.nameEl.prepend(iconEl); } - + // Add collapse indicator - const collapseIndicator = headingSetting.settingEl.createEl('span', { + const collapseIndicator = headingSetting.settingEl.createEl('span', { cls: 'sf-settings-collapse-indicator', text: defaultOpen ? '▾' : '▸' }); headingSetting.nameEl.appendChild(collapseIndicator); - + // Create content container - const contentContainer = sectionContainer.createEl('div', { + const contentContainer = sectionContainer.createEl('div', { cls: 'sf-settings-section-content' }); - + // Initially hide if not defaultOpen if (!defaultOpen) { contentContainer.classList.add('sf-hidden'); } - + // Add click handler for toggling headingSetting.settingEl.addEventListener('click', () => { const isVisible = contentContainer.classList.contains('sf-visible'); @@ -76,16 +76,16 @@ export class SpaceforgeSettingTab extends PluginSettingTab { contentContainer.classList.toggle('sf-visible'); collapseIndicator.textContent = isVisible ? '▸' : '▾'; }); - + return contentContainer; }; - + // We'll rely on the CSS in the styles.css file instead of inline styles - + // Create action buttons for global data operations const createActionButtons = () => { const actionsContainer = containerEl.createEl('div', { cls: 'sf-settings-actions' }); - + // Export all data button const exportBtn = actionsContainer.createEl('button', { text: 'Export all data', cls: 'sf-btn sf-btn-primary' }); exportBtn.addEventListener('click', () => { @@ -120,38 +120,38 @@ export class SpaceforgeSettingTab extends PluginSettingTab { }; const dataJson = JSON.stringify(dataToExport, null, 2); const blob = new Blob([dataJson], { type: 'application/json' }); - + const a = document.body.createEl('a'); a.href = URL.createObjectURL(blob); a.download = 'spaceforge-data.json'; // Changed filename document.body.appendChild(a); a.click(); document.body.removeChild(a); - + new Notice('All plugin data exported successfully'); }); - + // Import all data button const importBtn = actionsContainer.createEl('button', { text: 'Import all data', cls: 'sf-btn sf-btn-primary' }); importBtn.addEventListener('click', () => { const input = document.body.createEl('input'); input.type = 'file'; input.accept = 'application/json'; - + input.onchange = async (e: Event) => { const file = (e.target as HTMLInputElement).files?.[0]; if (file) { try { const text = await file.text(); const importedFullData = JSON.parse(text) as SpaceforgePluginData; - + if (!importedFullData || typeof importedFullData.settings !== 'object' || typeof importedFullData.pluginState !== 'object') { throw new Error('Invalid data file format. Expected settings and pluginState properties.'); } - + // Apply imported settings, ensuring all defaults are present this.plugin.settings = { ...DEFAULT_SETTINGS, ...importedFullData.settings }; - + // Apply imported plugin state, ensuring all defaults are present this.plugin.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA, ...importedFullData.pluginState }; @@ -163,7 +163,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.mcqService.mcqSessions = this.plugin.pluginState.mcqSessions || {}; this.plugin.reviewScheduleService.customNoteOrder = this.plugin.pluginState.customNoteOrder || []; this.plugin.reviewScheduleService.lastLinkAnalysisTimestamp = typeof this.plugin.pluginState.lastLinkAnalysisTimestamp === 'number' ? this.plugin.pluginState.lastLinkAnalysisTimestamp : null; - + // Pomodoro state update this.plugin.pomodoroService?.onSettingsChanged(); // To re-evaluate durations if they changed from settings this.plugin.pomodoroService?.reinitializeTimerFromState(); // To correctly start/stop timer based on imported state @@ -173,143 +173,31 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.initializeMCQComponents(); await this.plugin.savePluginData(); // Save the fully imported data - + this.display(); // Refresh settings display - + new Notice('All plugin data imported successfully. Plugin may require a reload for all changes to take effect.'); } catch (error) { - new Notice(`Failed to import data: ${error.message}`); + new Notice(`Failed to import data: ${error.message} `); } } }; input.click(); }); - + // Reset to defaults button const resetBtn = actionsContainer.createEl('button', { text: 'Reset to defaults', cls: 'sf-btn sf-btn-warning' }); - resetBtn.addEventListener('click', async () => { - const confirmed = confirm('Are you sure you want to reset all plugin data (settings and state) to defaults? This cannot be undone.'); - - if (confirmed) { - // Apply default settings and default plugin state - this.plugin.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); - this.plugin.pluginState = JSON.parse(JSON.stringify(DEFAULT_PLUGIN_STATE_DATA)); - - // Repopulate services with defaults - this.plugin.reviewScheduleService.schedules = this.plugin.pluginState.schedules; - this.plugin.reviewHistoryService.history = this.plugin.pluginState.history; - this.plugin.reviewSessionService.reviewSessions = this.plugin.pluginState.reviewSessions; - this.plugin.mcqService.mcqSets = this.plugin.pluginState.mcqSets; - this.plugin.mcqService.mcqSessions = this.plugin.pluginState.mcqSessions; - this.plugin.reviewScheduleService.customNoteOrder = this.plugin.pluginState.customNoteOrder; - this.plugin.reviewScheduleService.lastLinkAnalysisTimestamp = this.plugin.pluginState.lastLinkAnalysisTimestamp ?? null; - - this.plugin.pomodoroService?.onSettingsChanged(); // Reset pomodoro service state based on default settings - this.plugin.pomodoroService?.reinitializeTimerFromState(); // Reset pomodoro service timer based on default state - - - // Re-initialize MCQ components based on default settings - this.plugin.initializeMCQComponents(); - - await this.plugin.savePluginData(); // Save the complete default data - - this.display(); // Refresh settings display - - new Notice('All plugin data reset to defaults.'); - } - }); - - // Clear Reviews Data button - const clearReviewsBtn = actionsContainer.createEl('button', { - text: 'Clear all reviews', - cls: 'sf-btn sf-btn-danger' // Use existing danger button styling - }); - clearReviewsBtn.addEventListener('click', async () => { - const confirmed = confirm('Are you sure you want to clear all review schedules, history, and session data? This action cannot be undone.'); - if (confirmed) { - try { - // Reset schedule-related parts of pluginState - this.plugin.pluginState.schedules = { ...DEFAULT_PLUGIN_STATE_DATA.schedules }; - this.plugin.pluginState.history = [...DEFAULT_PLUGIN_STATE_DATA.history]; - this.plugin.pluginState.reviewSessions = { ...DEFAULT_PLUGIN_STATE_DATA.reviewSessions }; - this.plugin.pluginState.customNoteOrder = [...DEFAULT_PLUGIN_STATE_DATA.customNoteOrder]; - - // Update services - this.plugin.reviewScheduleService.schedules = this.plugin.pluginState.schedules; - this.plugin.reviewHistoryService.history = this.plugin.pluginState.history; - this.plugin.reviewSessionService.reviewSessions = this.plugin.pluginState.reviewSessions; - this.plugin.reviewScheduleService.customNoteOrder = this.plugin.pluginState.customNoteOrder; - - // Explicitly update the review controller's state - if (this.plugin.reviewController) { - await this.plugin.reviewController.updateTodayNotes(); - } - - // Emit an event that the sidebar might be listening to - if (this.plugin.events) { - this.plugin.events.emit('sidebar-update'); - } - - // Save the cleared data first - await this.plugin.savePluginData(); - - new Notice('All review data cleared successfully.'); - - // Then refresh UI elements - this.display(); // Refresh settings UI - this.plugin.getSidebarView()?.refresh(); - - } catch (error) { - new Notice('Failed to clear review data. Check console for details.'); - } - } - }); - - // Clear Events Data button - const clearEventsBtn = actionsContainer.createEl('button', { - text: 'Clear all events', - cls: 'sf-btn sf-btn-danger' - }); - clearEventsBtn.addEventListener('click', async () => { - const confirmed = confirm('Are you sure you want to clear all calendar events? This action cannot be undone.'); - if (confirmed) { - try { - // Reset calendar events in pluginState - this.plugin.pluginState.calendarEvents = { ...DEFAULT_PLUGIN_STATE_DATA.calendarEvents }; - - // Update calendar service if it exists - if (this.plugin.calendarEventService) { - this.plugin.calendarEventService.initialize([]); - } - - // Save the cleared data - await this.plugin.savePluginData(); - - new Notice('All calendar events cleared successfully.'); - - // Then refresh UI elements - this.display(); // Refresh settings UI - this.plugin.getSidebarView()?.refresh(); - - } catch (error) { - new Notice('Failed to clear calendar events. Check console for details.'); - } - } - }); - - // Clear All Data button - const clearAllBtn = actionsContainer.createEl('button', { - text: 'Clear all data', - cls: 'sf-btn sf-btn-danger' - }); - clearAllBtn.addEventListener('click', async () => { - const confirmed = confirm('Are you sure you want to clear ALL plugin data (reviews, events, MCQs, Pomodoro state)? This action cannot be undone.'); - if (confirmed) { - try { - // Reset all plugin state to defaults + resetBtn.addEventListener('click', () => { + new ConfirmationModal( + this.app, + 'Reset to Defaults', + 'Are you sure you want to reset all plugin data (settings and state) to defaults? This cannot be undone.', + async () => { + // Apply default settings and default plugin state + this.plugin.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS)); this.plugin.pluginState = JSON.parse(JSON.stringify(DEFAULT_PLUGIN_STATE_DATA)); - // Update all services + // Repopulate services with defaults this.plugin.reviewScheduleService.schedules = this.plugin.pluginState.schedules; this.plugin.reviewHistoryService.history = this.plugin.pluginState.history; this.plugin.reviewSessionService.reviewSessions = this.plugin.pluginState.reviewSessions; @@ -318,43 +206,170 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.reviewScheduleService.customNoteOrder = this.plugin.pluginState.customNoteOrder; this.plugin.reviewScheduleService.lastLinkAnalysisTimestamp = this.plugin.pluginState.lastLinkAnalysisTimestamp ?? null; - // Update calendar service if it exists - if (this.plugin.calendarEventService) { - this.plugin.calendarEventService.initialize([]); - } + this.plugin.pomodoroService?.onSettingsChanged(); // Reset pomodoro service state based on default settings + this.plugin.pomodoroService?.reinitializeTimerFromState(); // Reset pomodoro service timer based on default state - // Update Pomodoro service - this.plugin.pomodoroService?.onSettingsChanged(); - this.plugin.pomodoroService?.reinitializeTimerFromState(); - // Explicitly update the review controller's state - if (this.plugin.reviewController) { - await this.plugin.reviewController.updateTodayNotes(); - } + // Re-initialize MCQ components based on default settings + this.plugin.initializeMCQComponents(); - // Emit an event that the sidebar might be listening to - if (this.plugin.events) { - this.plugin.events.emit('sidebar-update'); - } + await this.plugin.savePluginData(); // Save the complete default data - // Save the cleared data - await this.plugin.savePluginData(); - - new Notice('All plugin data cleared successfully.'); + this.display(); // Refresh settings display - // Then refresh UI elements - this.display(); // Refresh settings UI - this.plugin.getSidebarView()?.refresh(); - - } catch (error) { - new Notice('Failed to clear all data. Check console for details.'); + new Notice('All plugin data reset to defaults.'); } - } + ).open(); }); - + + // Clear Reviews Data button + const clearReviewsBtn = actionsContainer.createEl('button', { + text: 'Clear all reviews', + cls: 'sf-btn sf-btn-danger' // Use existing danger button styling + }); + clearReviewsBtn.addEventListener('click', () => { + new ConfirmationModal( + this.app, + 'Clear All Reviews', + 'Are you sure you want to clear all review schedules, history, and session data? This action cannot be undone.', + async () => { + try { + // Reset schedule-related parts of pluginState + this.plugin.pluginState.schedules = { ...DEFAULT_PLUGIN_STATE_DATA.schedules }; + this.plugin.pluginState.history = [...DEFAULT_PLUGIN_STATE_DATA.history]; + this.plugin.pluginState.reviewSessions = { ...DEFAULT_PLUGIN_STATE_DATA.reviewSessions }; + this.plugin.pluginState.customNoteOrder = [...DEFAULT_PLUGIN_STATE_DATA.customNoteOrder]; + + // Update services + this.plugin.reviewScheduleService.schedules = this.plugin.pluginState.schedules; + this.plugin.reviewHistoryService.history = this.plugin.pluginState.history; + this.plugin.reviewSessionService.reviewSessions = this.plugin.pluginState.reviewSessions; + this.plugin.reviewScheduleService.customNoteOrder = this.plugin.pluginState.customNoteOrder; + + // Explicitly update the review controller's state + if (this.plugin.reviewController) { + this.plugin.reviewController.updateTodayNotes(); + } + + // Emit an event that the sidebar might be listening to + if (this.plugin.events) { + this.plugin.events.emit('sidebar-update'); + } + + // Save the cleared data first + await this.plugin.savePluginData(); + + new Notice('All review data cleared successfully.'); + + // Then refresh UI elements + this.display(); // Refresh settings UI + void this.plugin.getSidebarView()?.refresh(); + + } catch (_error) { + new Notice('Failed to clear review data. Check console for details.'); + } + } + ).open(); + }); + + // Clear Events Data button + const clearEventsBtn = actionsContainer.createEl('button', { + text: 'Clear all events', + cls: 'sf-btn sf-btn-danger' + }); + clearEventsBtn.addEventListener('click', () => { + new ConfirmationModal( + this.app, + 'Clear All Events', + 'Are you sure you want to clear all calendar events? This action cannot be undone.', + async () => { + try { + // Reset calendar events in pluginState + this.plugin.pluginState.calendarEvents = { ...DEFAULT_PLUGIN_STATE_DATA.calendarEvents }; + + // Update calendar service if it exists + if (this.plugin.calendarEventService) { + this.plugin.calendarEventService.initialize([]); + } + + // Save the cleared data + await this.plugin.savePluginData(); + + new Notice('All calendar events cleared successfully.'); + + // Then refresh UI elements + this.display(); // Refresh settings UI + void this.plugin.getSidebarView()?.refresh(); + + } catch (_error) { + new Notice('Failed to clear calendar events. Check console for details.'); + } + } + ).open(); + }); + + // Clear All Data button + const clearAllBtn = actionsContainer.createEl('button', { + text: 'Clear all data', + cls: 'sf-btn sf-btn-danger' + }); + clearAllBtn.addEventListener('click', () => { + new ConfirmationModal( + this.app, + 'Clear All Data', + 'Are you sure you want to clear ALL plugin data (reviews, events, MCQs, Pomodoro state)? This action cannot be undone.', + async () => { + try { + // Reset all plugin state to defaults + this.plugin.pluginState = JSON.parse(JSON.stringify(DEFAULT_PLUGIN_STATE_DATA)); + + // Update all services + this.plugin.reviewScheduleService.schedules = this.plugin.pluginState.schedules; + this.plugin.reviewHistoryService.history = this.plugin.pluginState.history; + this.plugin.reviewSessionService.reviewSessions = this.plugin.pluginState.reviewSessions; + this.plugin.mcqService.mcqSets = this.plugin.pluginState.mcqSets; + this.plugin.mcqService.mcqSessions = this.plugin.pluginState.mcqSessions; + this.plugin.reviewScheduleService.customNoteOrder = this.plugin.pluginState.customNoteOrder; + this.plugin.reviewScheduleService.lastLinkAnalysisTimestamp = this.plugin.pluginState.lastLinkAnalysisTimestamp ?? null; + + // Update calendar service if it exists + if (this.plugin.calendarEventService) { + this.plugin.calendarEventService.initialize([]); + } + + // Update Pomodoro service + this.plugin.pomodoroService?.onSettingsChanged(); + this.plugin.pomodoroService?.reinitializeTimerFromState(); + + // Explicitly update the review controller's state + if (this.plugin.reviewController) { + this.plugin.reviewController.updateTodayNotes(); + } + + // Emit an event that the sidebar might be listening to + if (this.plugin.events) { + this.plugin.events.emit('sidebar-update'); + } + + // Save the cleared data + await this.plugin.savePluginData(); + + new Notice('All plugin data cleared successfully.'); + + // Then refresh UI elements + this.display(); // Refresh settings UI + void this.plugin.getSidebarView()?.refresh(); + + } catch (_error) { + new Notice('Failed to clear all data. Check console for details.'); + } + } + ).open(); + }); + return actionsContainer; }; - + // ========= SPACED REPETITION SECTION ========= const spacedRepSection = createCollapsible('Spaced repetition', 'calendar-clock', true); @@ -362,12 +377,14 @@ export class SpaceforgeSettingTab extends PluginSettingTab { // Changed to h3 and removed sf-settings-subsection class for potentially better contrast new Setting(spacedRepSection).setName('Algorithm configuration').setHeading(); - const algoSelectionSetting = new Setting(spacedRepSection) + const algorithmContainer = spacedRepSection.createEl('div', { cls: 'sf-settings-subsection' }); // Define algorithmContainer + + new Setting(algorithmContainer) .setName('Default scheduling algorithm') .setDesc('Choose the default algorithm for newly created notes.') .addDropdown(dropdown => dropdown - .addOption('sm2', 'SM-2') - .addOption('fsrs', 'FSRS') + .addOption('sm2', 'SM-2') // eslint-disable-line obsidianmd/ui/sentence-case + .addOption('fsrs', 'FSRS') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.defaultSchedulingAlgorithm) .onChange(async (value: 'sm2' | 'fsrs') => { this.plugin.settings.defaultSchedulingAlgorithm = value; @@ -383,37 +400,37 @@ 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'); + sm2Summary.setText('SM-2 parameters'); // eslint-disable-line obsidianmd/ui/sentence-case sm2ParamsContainer.open = false; // Initially closed new Setting(sm2ParamsContainer) - .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).') + .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 .addSlider(slider => slider - .setLimits(130, 500, 10) + .setLimits(130, 500, 10) .setValue(this.plugin.settings.baseEase) .setDynamicTooltip() .onChange(async (value: number) => { this.plugin.settings.baseEase = value; await this.plugin.savePluginData(); })); - + new Setting(sm2ParamsContainer) - .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.') + .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 .addToggle(toggle => toggle .setValue(this.plugin.settings.useInitialSchedule) .onChange(async (value: boolean) => { this.plugin.settings.useInitialSchedule = value; await this.plugin.savePluginData(); - this.display(); + this.display(); })); if (this.plugin.settings.useInitialSchedule) { new Setting(sm2ParamsContainer) - .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.') + .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 .addText(text => text .setValue(this.plugin.settings.initialScheduleCustomIntervals.join(', ')) .onChange(async (value: string) => { @@ -422,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); + new Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5000); // eslint-disable-line obsidianmd/ui/sentence-case text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(', ')); } })); } - + new Setting(sm2ParamsContainer) - .setName('SM-2: Maximum interval (days)') - .setDesc('Longest possible interval between SM-2 reviews.') + .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 .addText(text => text .setValue(this.plugin.settings.maximumInterval.toString()) .onChange(async (value: string) => { @@ -440,10 +457,10 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); } })); - + new Setting(sm2ParamsContainer) - .setName('SM-2: Load balancing') - .setDesc('Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.') + .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 .addToggle(toggle => toggle .setValue(this.plugin.settings.loadBalance) .onChange(async (value: boolean) => { @@ -455,14 +472,14 @@ 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'); + fsrsSummary.setText('FSRS parameters'); // eslint-disable-line obsidianmd/ui/sentence-case fsrsParamsContainer.open = false; // Initially closed new Setting(fsrsParamsContainer) .setName('Request retention') .setDesc('Desired recall probability (0.7-0.99, default: 0.9). Higher values mean more frequent reviews.') .addText(text => text - .setValue(this.plugin.settings.fsrsParameters?.request_retention?.toString() ?? DEFAULT_SETTINGS.fsrsParameters.request_retention!.toString()) + .setValue(this.plugin.settings.fsrsParameters?.request_retention?.toString() ?? DEFAULT_SETTINGS.fsrsParameters.request_retention?.toString() ?? '0.9') .onChange(async (value) => { const numValue = parseFloat(value); if (!isNaN(numValue) && numValue >= 0.7 && numValue <= 0.99) { @@ -470,15 +487,15 @@ 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."); + new Notice("FSRS request retention must be between 0.7 and 0.99."); // eslint-disable-line obsidianmd/ui/sentence-case } })); new Setting(fsrsParamsContainer) .setName('Maximum interval (days)') - .setDesc('Longest possible interval FSRS will schedule.') + .setDesc('Longest possible interval FSRS will schedule.') // eslint-disable-line obsidianmd/ui/sentence-case .addText(text => text - .setValue(this.plugin.settings.fsrsParameters?.maximum_interval?.toString() ?? DEFAULT_SETTINGS.fsrsParameters.maximum_interval!.toString()) + .setValue(this.plugin.settings.fsrsParameters?.maximum_interval?.toString() ?? DEFAULT_SETTINGS.fsrsParameters.maximum_interval?.toString() ?? '36500') .onChange(async (value) => { const numValue = parseInt(value); if (!isNaN(numValue) && numValue > 0) { @@ -486,15 +503,15 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange(); } else { - new Notice("FSRS Maximum Interval must be a positive number."); + new Notice("FSRS maximum interval must be a positive number."); // eslint-disable-line obsidianmd/ui/sentence-case } })); - + new Setting(fsrsParamsContainer) .setName('Learning steps (minutes)') .setDesc('Comma-separated initial learning intervals in minutes (e.g., 1,10 for 1m, 10m).') .addText(text => text - .setValue(this.plugin.settings.fsrsParameters?.learning_steps?.join(',') ?? DEFAULT_SETTINGS.fsrsParameters.learning_steps!.join(',')) + .setValue(this.plugin.settings.fsrsParameters?.learning_steps?.join(',') ?? DEFAULT_SETTINGS.fsrsParameters.learning_steps?.join(',') ?? '') .onChange(async (value) => { const steps = value.split(',').map(s => parseInt(s.trim())).filter(n => !isNaN(n) && n > 0); if (steps.length > 0) { // Allow empty to use FSRS internal defaults if any @@ -502,18 +519,18 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange(); } else if (value.trim() === "") { // Allow clearing to use FSRS defaults - this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, learning_steps: [] }; - await this.plugin.savePluginData(); - this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange(); - }else { - new Notice("FSRS Learning Steps must be valid comma-separated numbers > 0, or empty."); + this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, learning_steps: [] }; + 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 Setting(fsrsParamsContainer) .setName('Enable fuzz') - .setDesc('Add slight randomness to FSRS intervals (recommended).') + .setDesc('Add slight randomness to FSRS intervals (recommended).') // eslint-disable-line obsidianmd/ui/sentence-case .addToggle(toggle => toggle - .setValue(this.plugin.settings.fsrsParameters?.enable_fuzz ?? DEFAULT_SETTINGS.fsrsParameters.enable_fuzz!) + .setValue(this.plugin.settings.fsrsParameters?.enable_fuzz ?? DEFAULT_SETTINGS.fsrsParameters.enable_fuzz ?? false) .onChange(async (value) => { this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, enable_fuzz: value }; await this.plugin.savePluginData(); @@ -521,9 +538,9 @@ export class SpaceforgeSettingTab extends PluginSettingTab { })); new Setting(fsrsParamsContainer) .setName('Enable short term scheduling') - .setDesc('Use FSRS short-term memory model (affects initial learning steps).') + .setDesc('Use FSRS short-term memory model (affects initial learning steps).') // eslint-disable-line obsidianmd/ui/sentence-case .addToggle(toggle => toggle - .setValue(this.plugin.settings.fsrsParameters?.enable_short_term ?? DEFAULT_SETTINGS.fsrsParameters.enable_short_term!) + .setValue(this.plugin.settings.fsrsParameters?.enable_short_term ?? DEFAULT_SETTINGS.fsrsParameters.enable_short_term ?? false) .onChange(async (value) => { this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, enable_short_term: value }; await this.plugin.savePluginData(); @@ -531,11 +548,11 @@ 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.') + .setDesc('FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.') // eslint-disable-line obsidianmd/ui/sentence-case .addTextArea(text => { text.inputEl.rows = 3; text.inputEl.addClass('sf-full-width-textarea'); - text.setValue(this.plugin.settings.fsrsParameters?.w?.join(',') ?? DEFAULT_SETTINGS.fsrsParameters.w!.join(',')) + text.setValue(this.plugin.settings.fsrsParameters?.w?.join(',') ?? DEFAULT_SETTINGS.fsrsParameters.w?.join(',') ?? '') .onChange(async (value) => { const weights = value.split(',').map(s => parseFloat(s.trim())); if (weights.length === 17 && weights.every(n => !isNaN(n))) { @@ -543,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."); + new Notice("FSRS weights must be a comma-separated list of 17 valid numbers."); // eslint-disable-line obsidianmd/ui/sentence-case } }); }); @@ -556,42 +573,50 @@ export class SpaceforgeSettingTab extends PluginSettingTab { conversionContainer.open = false; // Initially closed new Setting(conversionContainer) - .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.') + .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 .addButton(button => button - .setButtonText('Convert SM-2 to FSRS') + .setButtonText('Convert SM-2 to FSRS') // eslint-disable-line obsidianmd/ui/sentence-case .setCta() // Call to action style - .onClick(async () => { - const confirmed = confirm('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.'); - if (confirmed) { - new Notice('Converting SM-2 cards to FSRS... This may take a moment.'); - await this.plugin.reviewScheduleService.convertAllSm2ToFsrs(); - await this.plugin.savePluginData(); - new Notice('All SM-2 cards have been converted to FSRS.'); - this.display(); // Refresh settings tab - } + .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 + async () => { + new Notice('Converting SM-2 cards to FSRS... This may take a moment.'); // eslint-disable-line obsidianmd/ui/sentence-case + 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 + this.display(); // Refresh settings tab + } + ).open(); })); new Setting(conversionContainer) - .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.') + .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 .addButton(button => button - .setButtonText('Convert FSRS to SM-2') + .setButtonText('Convert FSRS to SM-2') // eslint-disable-line obsidianmd/ui/sentence-case .setCta() - .onClick(async () => { - const confirmed = confirm('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.'); - if (confirmed) { - new Notice('Converting FSRS cards to SM-2... This may take a moment.'); - await this.plugin.reviewScheduleService.convertAllFsrsToSm2(); - await this.plugin.savePluginData(); - new Notice('All FSRS cards have been converted to SM-2.'); - this.display(); // Refresh settings tab - } + .onClick(() => { + new ConfirmationModal( + this.app, + '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 + 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 + this.display(); // Refresh settings tab + } + ).open(); })); - + // ========= INTERFACE SECTION ========= const interfaceSection = createCollapsible('Interface & behavior', 'settings', false); // Closed by default - + new Setting(interfaceSection) .setName("Display") .setHeading() @@ -607,9 +632,9 @@ export class SpaceforgeSettingTab extends PluginSettingTab { .onChange(async (value: 'list' | 'calendar') => { this.plugin.settings.sidebarViewType = value; await this.plugin.savePluginData(); - this.plugin.getSidebarView()?.refresh(); + void this.plugin.getSidebarView()?.refresh(); })); - + new Setting(interfaceSection) .setName('Show navigation notifications') .setDesc('Display notifications when moving between notes') @@ -619,7 +644,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.showNavigationNotifications = value; await this.plugin.savePluginData(); })); - + new Setting(interfaceSection) .setName("Review behavior") .setHeading() @@ -634,7 +659,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.includeSubfolders = value; await this.plugin.savePluginData(); })); - + new Setting(interfaceSection) .setName('Notification time') .setDesc('Minutes before due time to notify about upcoming reviews (0 to disable)') @@ -647,9 +672,9 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); } })); - + new Setting(interfaceSection) - .setName('Reading speed (WPM)') + .setName('Reading speed (WPM)') // eslint-disable-line obsidianmd/ui/sentence-case .setDesc('Words per minute for estimating review time') .addSlider(slider => slider .setLimits(100, 500, 10) @@ -659,9 +684,10 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.readingSpeed = value; await this.plugin.savePluginData(); })); - - interfaceSection.createEl('div', { cls: 'sf-setting-explain', - text: 'Average adults read 200-250 WPM for regular content, 100-150 WPM for technical content' + + 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 }); new Setting(interfaceSection) @@ -679,7 +705,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); this.display(); // Refresh to show/hide dependent settings })); - + if (this.plugin.settings.enableNavigationCommands) { new Setting(interfaceSection) .setName('Command to execute') @@ -705,7 +731,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { modifiers, key, }; - this.plugin.savePluginData(); + void this.plugin.savePluginData(); document.removeEventListener('keydown', keydownHandler, { capture: true }); this.display(); } @@ -725,22 +751,22 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); })); } - + // ========= MCQ SECTION ========= const mcqSection = createCollapsible('Multiple choice questions', 'newspaper', false); // Closed by default - + new Setting(mcqSection) - .setName('Enable MCQ feature') + .setName('Enable MCQ feature') // eslint-disable-line obsidianmd/ui/sentence-case .setDesc('Use AI-generated multiple-choice questions to test your knowledge') .addToggle(toggle => toggle .setValue(this.plugin.settings.enableMCQ) .onChange(async (value: boolean) => { this.plugin.settings.enableMCQ = value; await this.plugin.savePluginData(); - + // Reinitialize MCQ components this.plugin.initializeMCQComponents(); - + // Update the API settings backup when enabling/disabling MCQ try { const apiSettings = { @@ -749,13 +775,14 @@ export class SpaceforgeSettingTab extends PluginSettingTab { openRouterModel: this.plugin.settings.openRouterModel }; this.plugin.app.saveLocalStorage('spaceforge-api-settings', JSON.stringify(apiSettings)); - } catch (e) { + } catch { + // no-op } - + // Refresh the MCQ settings visibility this.display(); })); - + // Only show other MCQ settings if the feature is enabled if (this.plugin.settings.enableMCQ) { new Setting(mcqSection) @@ -815,7 +842,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { .onChange(async (value: string) => { this.plugin.settings.openRouterModel = value; await this.plugin.savePluginData(); - // Removed specific localStorage backup here + // Removed specific localStorage backup here })); } else if (provider === ApiProvider.OpenAI) { new Setting(mcqSection) @@ -851,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') + .setPlaceholder('http://localhost:11434') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.ollamaApiUrl) .onChange(async (value: string) => { this.plugin.settings.ollamaApiUrl = value; await this.plugin.savePluginData(); })); new Setting(mcqSection) - .setName('Ollama model') - .setDesc('Name of the Ollama model to use (e.g., llama3, mistral)') + .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 .addText(text => text - .setPlaceholder('Enter Ollama model name') + .setPlaceholder('Enter Ollama model name') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.ollamaModel) .onChange(async (value: string) => { this.plugin.settings.ollamaModel = value; @@ -872,11 +899,11 @@ export class SpaceforgeSettingTab extends PluginSettingTab { .setName("Gemini configuration") .setHeading() .setClass("sf-settings-subsection-provider-header"); - new Setting(mcqSection) + new Setting(mcqSection) .setName('Gemini API key') - .setDesc('Your Google AI Gemini API key.') + .setDesc('Your Google AI Gemini API key.') // eslint-disable-line obsidianmd/ui/sentence-case .addText(text => text - .setPlaceholder('Enter your Gemini API key') + .setPlaceholder('Enter your Gemini API key') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.geminiApiKey) .onChange(async (value: string) => { this.plugin.settings.geminiApiKey = value; @@ -886,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') + .setPlaceholder('Enter Gemini model name') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.geminiModel) .onChange(async (value: string) => { this.plugin.settings.geminiModel = value; @@ -899,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.') + .setDesc('Your Anthropic Claude API key.') // eslint-disable-line obsidianmd/ui/sentence-case .addText(text => text - .setPlaceholder('Enter your Claude API key') + .setPlaceholder('Enter your Claude API key') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.claudeApiKey) .onChange(async (value: string) => { this.plugin.settings.claudeApiKey = value; @@ -911,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') + .setPlaceholder('Enter Claude model name') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.claudeModel) .onChange(async (value: string) => { this.plugin.settings.claudeModel = value; @@ -924,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.') + .setDesc('Your Together AI API key.') // eslint-disable-line obsidianmd/ui/sentence-case .addText(text => text - .setPlaceholder('Enter your Together AI API key') + .setPlaceholder('Enter your Together AI API key') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.togetherApiKey) .onChange(async (value: string) => { this.plugin.settings.togetherApiKey = value; @@ -936,14 +963,14 @@ 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') + .setPlaceholder('Enter Together AI model identifier') // eslint-disable-line obsidianmd/ui/sentence-case .setValue(this.plugin.settings.togetherModel) .onChange(async (value: string) => { this.plugin.settings.togetherModel = value; await this.plugin.savePluginData(); })); } - + // Question generation settings (common to all providers) new Setting(mcqSection) .setName("Question generation (common)") @@ -955,8 +982,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab { .setName('Question amount mode') .setDesc('How to determine the number of questions per note.') .addDropdown(dropdown => dropdown - .addOption(MCQQuestionAmountMode.Fixed, 'Fixed Number') - .addOption(MCQQuestionAmountMode.WordsPerQuestion, 'Per X Words') + .addOption(MCQQuestionAmountMode.Fixed, 'Fixed number') + .addOption(MCQQuestionAmountMode.WordsPerQuestion, 'Per X words') .setValue(this.plugin.settings.mcqQuestionAmountMode) .onChange(async (value: MCQQuestionAmountMode) => { this.plugin.settings.mcqQuestionAmountMode = value; @@ -967,7 +994,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { // Conditionally display settings based on the mode if (this.plugin.settings.mcqQuestionAmountMode === MCQQuestionAmountMode.Fixed) { new Setting(mcqSection) - .setName('Questions per note (Fixed)') + .setName('Questions per note (fixed)') .setDesc('Number of questions to generate for each note.') .addSlider(slider => slider .setLimits(1, 10, 1) @@ -992,11 +1019,11 @@ export class SpaceforgeSettingTab extends PluginSettingTab { } else { new Notice("Words per question must be a positive number."); // Optionally reset to previous value or default - text.setValue(this.plugin.settings.mcqWordsPerQuestion.toString()); + text.setValue(this.plugin.settings.mcqWordsPerQuestion.toString()); } })); } - + // This setting is now conditional above // new Setting(mcqSection) // .setName('Questions per note') @@ -1009,7 +1036,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { // this.plugin.settings.mcqQuestionsPerNote = value; // await this.plugin.savePluginData(); // })); - + new Setting(mcqSection) .setName('Choices per question') .setDesc('Number of answer choices for each question') @@ -1021,15 +1048,15 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.mcqChoicesPerQuestion = value; await this.plugin.savePluginData(); })); - + // Use CSS grid for the two dropdowns side by side const mcqFormattingGrid = mcqSection.createEl('div', { cls: 'sf-setting-grid' }); - + // First item in grid const promptTypeContainer = mcqFormattingGrid.createEl('div'); new Setting(promptTypeContainer) .setName('Prompt type') - .setDesc('Format for MCQ generation') + .setDesc('Format for MCQ generation') // eslint-disable-line obsidianmd/ui/sentence-case .addDropdown(dropdown => dropdown .addOption('basic', 'Basic') .addOption('detailed', 'Detailed') @@ -1038,11 +1065,11 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.mcqPromptType = value; await this.plugin.savePluginData(); })); - + // Second item in grid const difficultyContainer = mcqFormattingGrid.createEl('div'); new Setting(difficultyContainer) - .setName('MCQ difficulty') + .setName('MCQ difficulty') // eslint-disable-line obsidianmd/ui/sentence-case .setDesc('Complexity level') .addDropdown(dropdown => dropdown .addOption(MCQDifficulty.Basic, 'Basic recall') @@ -1052,13 +1079,13 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.mcqDifficulty = value; await this.plugin.savePluginData(); })); - + // Scoring settings new Setting(mcqSection) .setName("Scoring") .setHeading() .setClass("sf-settings-subsection-header"); - + new Setting(mcqSection) .setName('Time deduction amount') .setDesc('Score penalty for slow answers (0-1)') @@ -1070,7 +1097,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.mcqTimeDeductionAmount = value; await this.plugin.savePluginData(); })); - + new Setting(mcqSection) .setName('Time deduction threshold') .setDesc('Apply penalty after this many seconds') @@ -1091,11 +1118,11 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.mcqDeductFullMarkOnFirstFailure = value; await this.plugin.savePluginData(); })); - + // Add collapsible section for system prompts const systemPromptsContainer = mcqSection.createEl('details', { cls: 'sf-system-prompts-container' }); systemPromptsContainer.createEl('summary', { text: 'System prompts (advanced)', cls: 'sf-settings-subsection' }); - + // Basic prompt textarea systemPromptsContainer.createEl('div', { text: 'Basic difficulty prompt', cls: 'sf-prompt-label' }); const basicTextarea = systemPromptsContainer.createEl('textarea', { @@ -1105,14 +1132,14 @@ export class SpaceforgeSettingTab extends PluginSettingTab { }, cls: 'prompt-textarea' }); - + // Set value and add change handler basicTextarea.value = this.plugin.settings.mcqBasicSystemPrompt; - basicTextarea.addEventListener('change', async () => { + basicTextarea.addEventListener('change', () => { this.plugin.settings.mcqBasicSystemPrompt = basicTextarea.value; - await this.plugin.savePluginData(); + void this.plugin.savePluginData(); }); - + // Advanced prompt textarea systemPromptsContainer.createEl('div', { text: 'Advanced difficulty prompt', cls: 'sf-prompt-label' }); const advancedTextarea = systemPromptsContainer.createEl('textarea', { @@ -1122,12 +1149,12 @@ export class SpaceforgeSettingTab extends PluginSettingTab { }, cls: 'prompt-textarea' }); - + // Set value and add change handler advancedTextarea.value = this.plugin.settings.mcqAdvancedSystemPrompt; - advancedTextarea.addEventListener('change', async () => { + advancedTextarea.addEventListener('change', () => { this.plugin.settings.mcqAdvancedSystemPrompt = advancedTextarea.value; - await this.plugin.savePluginData(); + void this.plugin.savePluginData(); }); // Advanced Question Behavior subsection @@ -1136,7 +1163,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { .setHeading() .setClass("sf-settings-subsection-header"); - const regenerationSetting = new Setting(mcqSection) + new Setting(mcqSection) .setName('Enable question regeneration on rating') .setDesc('Automatically regenerate questions for a note if its review rating meets or exceeds a specified value.') .addToggle(toggle => toggle @@ -1145,13 +1172,13 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.settings.enableQuestionRegenerationOnRating = value; await this.plugin.savePluginData(); // Refresh display to show/hide dependent setting - this.display(); + this.display(); })); if (this.plugin.settings.enableQuestionRegenerationOnRating) { new Setting(mcqSection) - .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)') + .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 .addSlider(slider => slider .setLimits(0, 5, 1) .setValue(this.plugin.settings.minSm2RatingForQuestionRegeneration) @@ -1162,8 +1189,8 @@ export class SpaceforgeSettingTab extends PluginSettingTab { })); new Setting(mcqSection) - .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)') + .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 .addSlider(slider => slider .setLimits(1, 4, 1) // FSRS ratings are 1-4 .setValue(this.plugin.settings.minFsrsRatingForQuestionRegeneration) @@ -1177,25 +1204,25 @@ export class SpaceforgeSettingTab extends PluginSettingTab { } else { // 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.' + mcqDisabledMessage.createEl('p', { + text: 'Multiple Choice Questions are currently disabled. Enable it to configure durations and notifications.' // eslint-disable-line obsidianmd/ui/sentence-case }); } - + // ========= POMODORO TIMER SECTION ========= const pomodoroSection = createCollapsible('Pomodoro timer', 'timer', false); // Added timer icon new Setting(pomodoroSection) .setName('Enable pomodoro timer') - .setDesc('Show the Pomodoro timer in the sidebar.') + .setDesc('Show the Pomodoro timer in the sidebar.') // eslint-disable-line obsidianmd/ui/sentence-case .addToggle(toggle => toggle .setValue(this.plugin.settings.pomodoroEnabled) .onChange(async (value: boolean) => { this.plugin.settings.pomodoroEnabled = value; await this.plugin.savePluginData(); // Notify the service and refresh UI elements - this.plugin.pomodoroService?.onSettingsChanged(); - this.plugin.getSidebarView()?.refresh(); // Refresh sidebar to show/hide timer + this.plugin.pomodoroService?.onSettingsChanged(); + void this.plugin.getSidebarView()?.refresh(); // Refresh sidebar to show/hide timer this.display(); // Refresh settings tab to show/hide dependent settings })); @@ -1220,7 +1247,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab { this.plugin.pomodoroService?.onSettingsChanged(); // Notify service of potential duration change } })); - + new Setting(pomodoroSection) .setName('Short break duration') .setDesc('Length of a short break.') @@ -1281,67 +1308,71 @@ export class SpaceforgeSettingTab extends PluginSettingTab { await this.plugin.savePluginData(); })); } 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.' - }); + 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 + }); } // ========= DATA MANAGEMENT SECTION ========= const dataSection = createCollapsible('Data Management', 'database', false); - - const defaultPath = this.plugin.app.vault.configDir + `/plugins/${this.plugin.manifest.id}/data.json`; + + const defaultPath = this.plugin.app.vault.configDir + `/ plugins / ${this.plugin.manifest.id}/data.json`; let locationDesc = `Default: ${defaultPath}`; if (this.plugin.settings.useCustomDataPath && this.plugin.settings.customDataPath) { - const customPath = this.plugin.settings.customDataPath.endsWith('/data.json') - ? this.plugin.settings.customDataPath + const customPath = this.plugin.settings.customDataPath.endsWith('/data.json') + ? this.plugin.settings.customDataPath : `${this.plugin.settings.customDataPath}/data.json`; locationDesc = `Custom: ${customPath}`; } - + new Setting(dataSection) .setName('Current data location') .setDesc(locationDesc) .addExtraButton(button => button .setIcon('info') - .setTooltip('Current location of your Spaceforge data') + .setTooltip('Current location of your Spaceforge data') // eslint-disable-line obsidianmd/ui/sentence-case .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`}` : `Your data is stored at: ${defaultPath}`; - + new Notice(message, 8000); })); // Check if old default data file exists when using custom path if (this.plugin.settings.useCustomDataPath) { const oldFile = this.plugin.app.vault.getAbstractFileByPath(defaultPath); - + if (oldFile) { new Setting(dataSection) .setName('Legacy data file found') .setDesc(`A data file exists at the default location. You can safely delete it or keep it as a backup.`) .addButton(button => button - .setButtonText('Keep as Backup') + .setButtonText('Keep as backup') .setIcon('save') .onClick(() => { new Notice('Legacy data file kept as backup. You can delete it manually when ready.', 5000); })) .addButton(button => button - .setButtonText('Delete Legacy File') + .setButtonText('Delete legacy file') .setIcon('trash') .setWarning() - .onClick(async () => { - const confirmed = confirm('Are you sure you want to delete the legacy data file? Make sure your custom data path is working correctly before proceeding.'); - if (confirmed) { - try { - await this.plugin.app.vault.delete(oldFile); - new Notice('Legacy data file deleted successfully.', 3000); - this.display(); // Refresh settings - } catch (error) { - new Notice('Failed to delete legacy file: ' + error.message, 5000); + .onClick(() => { + new ConfirmationModal( + this.plugin.app, + 'Delete Legacy Data File', + 'Are you sure you want to delete the legacy data file? Make sure your custom data path is working correctly before proceeding.', + async () => { + try { + await this.plugin.app.fileManager.trashFile(oldFile); + new Notice('Legacy data file deleted successfully.', 3000); + this.display(); // Refresh settings + } catch (error) { + new Notice('Failed to delete legacy file: ' + error.message, 5000); + } } - } + ).open(); })); } } @@ -1354,26 +1385,26 @@ export class SpaceforgeSettingTab extends PluginSettingTab { container.empty(); // Clear previous content if (algorithm === 'sm2') { - container.createEl('h4', { text: 'About the Modified SM-2 Algorithm' }); - container.createEl('p', { + new Setting(container).setName('About the modified SM-2 algorithm').setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case + 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.' + 'When you rate your recall quality from 0-5, the algorithm adjusts the interval and difficulty (ease factor) accordingly.' }); container.createEl('p', { 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.' }); - sm2List.createEl('li', { text: 'Postponed items: Explicitly moved to tomorrow with a one-step quality penalty.' }); + 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: '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 const thead = ratingsTable.createTHead(); const tbody = ratingsTable.createTBody(); const headerRow = thead.insertRow(); headerRow.createEl('th', { text: 'Rating (0-5)' }); headerRow.createEl('th', { text: 'Description' }); - headerRow.createEl('th', { text: 'Effect on Interval' }); + headerRow.createEl('th', { text: 'Effect on interval' }); const row1 = tbody.insertRow(); row1.createEl('td', { text: '0-2' }); @@ -1395,19 +1426,19 @@ export class SpaceforgeSettingTab extends PluginSettingTab { row4.createEl('td', { text: 'Perfect recall' }); row4.createEl('td', { text: 'Largest increase' }); } else if (algorithm === 'fsrs') { - container.createEl('h4', { text: 'About the FSRS Algorithm' }); - 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.' - }); + new Setting(container).setName('About the FSRS algorithm').setHeading(); // eslint-disable-line obsidianmd/ui/sentence-case container.createEl('p', { - text: 'Key concepts in FSRS:' + 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 }); const fsrsList = container.createEl('ul'); - 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).' }); + 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 const ratingsTable = container.createEl('table', { cls: 'sf-ratings-table' }); const thead = ratingsTable.createTHead(); @@ -1415,25 +1446,25 @@ export class SpaceforgeSettingTab extends PluginSettingTab { const headerRow = thead.insertRow(); headerRow.createEl('th', { text: 'Rating (1-4)' }); headerRow.createEl('th', { text: 'Description' }); - headerRow.createEl('th', { text: 'Effect on Stability/Difficulty' }); + headerRow.createEl('th', { text: 'Effect on stability/difficulty' }); const row1 = tbody.insertRow(); - row1.createEl('td', { text: '1 (Again)' }); + row1.createEl('td', { text: '1 (Again)' }); // eslint-disable-line obsidianmd/ui/sentence-case 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)' }); + row2.createEl('td', { text: '2 (Hard)' }); // eslint-disable-line obsidianmd/ui/sentence-case 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)' }); + row3.createEl('td', { text: '3 (Good)' }); // eslint-disable-line obsidianmd/ui/sentence-case row3.createEl('td', { text: 'Recalled correctly' }); row3.createEl('td', { text: 'Standard increase in stability' }); const row4 = tbody.insertRow(); - row4.createEl('td', { text: '4 (Easy)' }); + row4.createEl('td', { text: '4 (Easy)' }); // eslint-disable-line obsidianmd/ui/sentence-case row4.createEl('td', { text: 'Recalled easily' }); row4.createEl('td', { text: 'Largest increase in stability' }); } diff --git a/ui/sidebar-view.ts b/ui/sidebar-view.ts index e31e94f..540be4d 100644 --- a/ui/sidebar-view.ts +++ b/ui/sidebar-view.ts @@ -1,11 +1,11 @@ -import { ItemView, Menu, Notice, TFile, WorkspaceLeaf, setIcon, Setting } from "obsidian"; +import { ItemView, Notice, WorkspaceLeaf, Setting } from "obsidian"; import SpaceforgePlugin from "../main"; import { PomodoroUIManager } from "./sidebar/pomodoro-ui-manager"; import { NoteItemRenderer } from "./sidebar/note-item-renderer"; import { ListViewRenderer } from "./sidebar/list-view-renderer"; // Import ListViewRenderer import { ReviewSchedule } from "../models/review-schedule"; import { DateUtils } from "../utils/dates"; -import { EstimationUtils } from "../utils/estimation"; + import { CalendarView } from "./calendar-view"; /** @@ -28,7 +28,7 @@ export class ReviewSidebarView extends ItemView { public activeListBaseDate: Date | null = null; selectedNotes: string[] = []; private lastSelectedNotePath: string | null = null; - private lastScrollPosition: number = 0; + private lastScrollPosition = 0; private expandedUpcomingDayKey: string | null = null; // State for upcoming section private resizeObserver: ResizeObserver | null = null; @@ -52,7 +52,7 @@ export class ReviewSidebarView extends ItemView { } getViewType(): string { return "spaceforge-review-schedule"; } - getDisplayText(): string { return "Spaceforge Review"; } + getDisplayText(): string { return "Spaceforge Review"; } // eslint-disable-line obsidianmd/ui/sentence-case getIcon(): string { return "calendar-clock"; } async onOpen(): Promise { @@ -64,15 +64,16 @@ export class ReviewSidebarView extends ItemView { }); // Ensure structure and render on first open - await this.refresh(); + await this.refresh(); } - async onClose(): Promise { + onClose(): Promise { if (this.resizeObserver) { this.resizeObserver.disconnect(); } this.plugin.events.off('sidebar-update', this.refresh.bind(this)); // No need to explicitly unregister pomodoro-update as the manager handles its own logic + return Promise.resolve(); } private ensureBaseStructure(): void { @@ -86,26 +87,30 @@ export class ReviewSidebarView extends ItemView { if (!this.persistentHeaderEl) { this.persistentHeaderEl = this.mainContainer.createDiv("review-header"); - new Setting(this.persistentHeaderEl).setHeading().setName("Review Schedule"); + new Setting(this.persistentHeaderEl).setHeading().setName("Review schedule"); const viewToggle = this.persistentHeaderEl.createDiv("review-view-toggle"); const listViewBtn = viewToggle.createDiv("review-view-btn"); listViewBtn.setText("List"); - listViewBtn.addEventListener("click", async () => { + listViewBtn.addEventListener("click", () => { if (this.plugin.settings.sidebarViewType === 'list') return; this.plugin.settings.sidebarViewType = 'list'; this.activeListBaseDate = null; - await this.plugin.savePluginData(); - await this.refresh(); + void (async () => { + await this.plugin.savePluginData(); + await this.refresh(); + })(); }); const calendarViewBtn = viewToggle.createDiv("review-view-btn"); calendarViewBtn.setText("Calendar"); - calendarViewBtn.addEventListener("click", async () => { + calendarViewBtn.addEventListener("click", () => { if (this.plugin.settings.sidebarViewType === 'calendar') return; this.plugin.settings.sidebarViewType = 'calendar'; - await this.plugin.savePluginData(); - await this.refresh(); + void (async () => { + await this.plugin.savePluginData(); + await this.refresh(); + })(); }); } this.updateViewToggleButtonsState(); @@ -114,8 +119,8 @@ export class ReviewSidebarView extends ItemView { this.listViewContentEl = this.mainContainer.createDiv("list-view-content"); } // Ensure managers are initialized *after* their container exists - if (!this.pomodoroUIManager) { - this.pomodoroUIManager = new PomodoroUIManager(this.plugin); + if (!this.pomodoroUIManager) { + this.pomodoroUIManager = new PomodoroUIManager(this.plugin); } if (!this.listViewRenderer) { this.listViewRenderer = new ListViewRenderer(this.plugin, this.pomodoroUIManager, this.noteItemRenderer, { @@ -126,10 +131,10 @@ export class ReviewSidebarView extends ItemView { setExpandedUpcomingDayKey: (key) => { this.expandedUpcomingDayKey = key; }, getLastSelectedNotePath: () => this.lastSelectedNotePath, setLastSelectedNotePath: (path) => { this.lastSelectedNotePath = path; }, - refreshSidebarView: this.refresh.bind(this) + refreshSidebarView: this.refresh.bind(this) }); } - + if (!this.calendarViewContentEl) { this.calendarViewContentEl = this.mainContainer.createDiv("calendar-view-content"); } @@ -152,10 +157,10 @@ export class ReviewSidebarView extends ItemView { async refresh(): Promise { // Capture the state of activeListBaseDate *before* any potential changes in this refresh cycle. const previousActiveListBaseDateEpoch = this.activeListBaseDate ? DateUtils.startOfUTCDay(this.activeListBaseDate) : null; - + // Determine the target date for this refresh. It defaults to the current activeListBaseDate // unless a calendar click provides a new date. - let newTargetDate: Date | null = this.activeListBaseDate; + let newTargetDate: Date | null = this.activeListBaseDate; if (this.plugin.clickedDateFromCalendar) { newTargetDate = this.plugin.clickedDateFromCalendar; @@ -180,7 +185,7 @@ export class ReviewSidebarView extends ItemView { this.activeListBaseDate = newTargetDate; // Update the sidebar's primary date state reviewDateChanged = true; } - + // Synchronize the ReviewController's date override with the sidebar's `activeListBaseDate`. // `this.activeListBaseDate` is now the definitive date context for the sidebar's list view. const currentControllerOverrideEpoch = this.plugin.reviewController.getCurrentReviewDateOverride(); @@ -204,7 +209,7 @@ export class ReviewSidebarView extends ItemView { let storedScrollPosition = this.lastScrollPosition; // Use the stored state if (this.mainContainer) { // Update storedScrollPosition if the container is currently scrolled - storedScrollPosition = this.mainContainer.scrollTop; + storedScrollPosition = this.mainContainer.scrollTop; } this.updateViewToggleButtonsState(); @@ -213,14 +218,14 @@ export class ReviewSidebarView extends ItemView { // Apply scroll position *after* rendering is complete if (this.mainContainer) { requestAnimationFrame(() => { - if (this.mainContainer) this.mainContainer.scrollTop = storedScrollPosition; + if (this.mainContainer) this.mainContainer.scrollTop = storedScrollPosition; this.lastScrollPosition = storedScrollPosition; // Update state after applying }); } } private async showCorrectViewPane(): Promise { - if (this.plugin.settings.sidebarViewType === 'calendar') { + if (this.plugin.settings.sidebarViewType === 'calendar') { if (this.listViewContentEl) this.listViewContentEl.hide(); if (this.calendarViewContentEl) { this.calendarViewContentEl.show(); @@ -231,7 +236,7 @@ export class ReviewSidebarView extends ItemView { if (this.listViewContentEl) { // Check if container exists this.listViewContentEl.show(); // Now delegate rendering to the ListViewRenderer - await this.renderListViewContent(this.listViewContentEl); + await this.renderListViewContent(this.listViewContentEl); } } } @@ -243,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."); + container.setText("Error: Could not render list view. Renderer not ready."); // eslint-disable-line obsidianmd/ui/sentence-case } } @@ -254,15 +259,15 @@ export class ReviewSidebarView extends ItemView { // container is this.calendarViewContentEl, which is persistent. // CalendarView instance is now also persistent and initialized in ensureBaseStructure. // It manages its own content within this.calendarViewContentEl. - + // No longer empty the container here, CalendarView manages its own content. // No longer create a temporary wrapper here. // CalendarView instance is already created. if (this.calendarView) { // Should always exist due to ensureBaseStructure - await this.calendarView.render(); + await this.calendarView.render(); } else { - container.setText("Error: Could not render calendar view. CalendarView not ready."); + container.setText("Error: Could not render calendar view. CalendarView not ready."); // eslint-disable-line obsidianmd/ui/sentence-case return; // Avoid further errors if calendarView is somehow null } @@ -278,9 +283,9 @@ export class ReviewSidebarView extends ItemView { }); if (this.containerEl) { - resizeObserver.observe(this.containerEl); - this.resizeObserver = resizeObserver; - this.updateCalendarContainerClass(); + resizeObserver.observe(this.containerEl); + this.resizeObserver = resizeObserver; + this.updateCalendarContainerClass(); } } @@ -302,19 +307,19 @@ export class ReviewSidebarView extends ItemView { // --- Methods moved out --- // --- Existing methods kept for view lifecycle / state --- - + /** * Move a note up in the list * (Existing Method - Consider moving to controller) */ async moveNoteUp(dateStr: string, note: ReviewSchedule): Promise { if (!this.listViewRenderer) { - return; + return; } - const notes = await this.listViewRenderer.groupNotesByDate( + const notes = this.listViewRenderer.groupNotesByDate( this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true), false - ); + ); const dateNotes = notes[dateStr]; if (!dateNotes) return; @@ -324,8 +329,8 @@ export class ReviewSidebarView extends ItemView { const path1 = dateNotes[index].path; const path2 = dateNotes[index - 1].path; await this.plugin.reviewController.swapNotes(path1, path2); - await this.refresh(); - new Notice(`Moved note up`); + await this.refresh(); + new Notice(`Moved note up.`); } /** @@ -333,13 +338,13 @@ export class ReviewSidebarView extends ItemView { * (Existing Method - Consider moving to controller) */ async moveNoteDown(dateStr: string, note: ReviewSchedule): Promise { - if (!this.listViewRenderer) { - return; - } - const notes = await this.listViewRenderer.groupNotesByDate( + if (!this.listViewRenderer) { + return; + } + const notes = this.listViewRenderer.groupNotesByDate( this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true), false - ); + ); const dateNotes = notes[dateStr]; if (!dateNotes) return; @@ -349,15 +354,15 @@ export class ReviewSidebarView extends ItemView { const path1 = dateNotes[index].path; const path2 = dateNotes[index + 1].path; await this.plugin.reviewController.swapNotes(path1, path2); - await this.refresh(); - new Notice(`Moved note down`); + await this.refresh(); + new Notice(`Moved note down.`); } /** * Group notes by their folder * (Existing Method - Consider moving to utility/service) */ - async groupNotesByFolder(notes: ReviewSchedule[]): Promise> { + groupNotesByFolder(notes: ReviewSchedule[]): Record { const grouped: Record = {}; for (const note of notes) { const file = this.plugin.app.vault.getAbstractFileByPath(note.path); @@ -374,14 +379,14 @@ export class ReviewSidebarView extends ItemView { // const isPomodoroOpen = this.pomodoroUIManager ? this.pomodoroUIManager.getIsPomodoroSectionOpen() : false; // Removed // Capture scroll position just before saving state if (this.mainContainer) { - this.lastScrollPosition = this.mainContainer.scrollTop; + this.lastScrollPosition = this.mainContainer.scrollTop; } return { activeListBaseDateISO: this.activeListBaseDate ? this.activeListBaseDate.toISOString() : null, // isPomodoroSectionOpen: isPomodoroOpen, // Removed expandedUpcomingDayKey: this.expandedUpcomingDayKey, selectedNotes: this.selectedNotes, - lastScrollPosition: this.lastScrollPosition, + lastScrollPosition: this.lastScrollPosition, sidebarViewType: this.plugin.settings.sidebarViewType, }; } @@ -400,14 +405,14 @@ export class ReviewSidebarView extends ItemView { } // Refresh the view first to ensure elements and managers are created/initialized - await this.refresh(); + await this.refresh(); // Now that elements exist (due to refresh), set the state for components like Pomodoro // Ensure pomodoroUIManager is initialized before setting state // if (this.pomodoroUIManager) { // Logic related to isPomodoroSectionOpen removed - // this.pomodoroUIManager.setIsPomodoroSectionOpen(state.isPomodoroSectionOpen ?? false); - // No need to call updatePomodoroUI here, as refresh -> showCorrectViewPane -> renderListViewContent -> render (in ListViewRenderer) - // should call pomodoroUIManager.updatePomodoroUI() if the section is enabled. + // this.pomodoroUIManager.setIsPomodoroSectionOpen(state.isPomodoroSectionOpen ?? false); + // No need to call updatePomodoroUI here, as refresh -> showCorrectViewPane -> renderListViewContent -> render (in ListViewRenderer) + // should call pomodoroUIManager.updatePomodoroUI() if the section is enabled. // } else { // } diff --git a/ui/sidebar/list-view-renderer.ts b/ui/sidebar/list-view-renderer.ts index 51dd830..64b4b11 100644 --- a/ui/sidebar/list-view-renderer.ts +++ b/ui/sidebar/list-view-renderer.ts @@ -1,10 +1,12 @@ -import { App, TFile, Notice, Setting } from "obsidian"; +import { Notice, Setting } from "obsidian"; import SpaceforgePlugin from "../../main"; import { ReviewSchedule } from "../../models/review-schedule"; import { DateUtils } from "../../utils/dates"; import { EstimationUtils } from "../../utils/estimation"; import { PomodoroUIManager } from "./pomodoro-ui-manager"; + import { NoteItemRenderer } from "./note-item-renderer"; +import { ConfirmationModal } from "../confirmation-modal"; /** * Manages the rendering of the list view within the sidebar. @@ -68,9 +70,8 @@ export class ListViewRenderer { // --- Review Buttons & Pomodoro --- // Pass this.plugin.reviewController.getTodayNotes() to ensure Pomodoro uses notes for the selected date context - const notesForPomodoro = this.plugin.reviewController.getTodayNotes(); - await this._ensureAndUpdateReviewButtonsSection(container, notesForPomodoro, selectedNotes); - + await this._ensureAndUpdateReviewButtonsSection(container, this.plugin.reviewController.getTodayNotes(), selectedNotes); + // --- "All Caught Up" Message --- this._ensureAndUpdateAllCaughtUpMessage(container, dueNotesForStats, activeListBaseDate); @@ -85,12 +86,12 @@ export class ListViewRenderer { notesToGroup = dueNotesForStats; // Use already fetched due notes } - const groupedNotes = await this.groupNotesByDate(notesToGroup, shouldIncludeFutureInGrouping); + const groupedNotes = this.groupNotesByDate(notesToGroup, shouldIncludeFutureInGrouping); const sortedDateKeys = this.getSortedDateKeys(groupedNotes); // --- Render Sections for Main List --- await this._ensureAndUpdateDateSections(container, sortedDateKeys, groupedNotes); - + // --- Active Session Section --- this._ensureAndUpdateActiveSessionSection(container); @@ -101,7 +102,7 @@ export class ListViewRenderer { const existingUpcomingSection = container.querySelector(".review-upcoming-section"); if (existingUpcomingSection) existingUpcomingSection.remove(); } - + this.updateBulkActionButtonsVisibility(container); // Ensure bulk actions visibility is correct at the end } @@ -124,14 +125,14 @@ export class ListViewRenderer { // statsCountEl.setText(`${dueNotesForStats.length} notes - ${EstimationUtils.formatTime(totalTime)}${overdueNotes.length > 0 ? ` (${overdueNotes.length} overdue)` : ''}`); // } - private async _ensureAndUpdateReviewButtonsSection(container: HTMLElement, notesForDisplay: ReviewSchedule[], selectedNotes: string[]): Promise { + 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; if (!pomodoroContainer) { pomodoroContainer = container.createDiv("sidebar-pomodoro-section"); - const pomodoroSectionContainerEl = pomodoroContainer.createDiv("sidebar-pomodoro-button-container"); + pomodoroContainer.createDiv("pomodoro-section-container"); } - + // Update Pomodoro section visibility and content if (this.pomodoroUIManager && pomodoroContainer) { const pomodoroSectionContainerEl = pomodoroContainer.querySelector(".sidebar-pomodoro-button-container") as HTMLElement; @@ -141,9 +142,9 @@ export class ListViewRenderer { pomodoroContainer.classList.remove('sf-hidden'); this.pomodoroUIManager.showPomodoroSection(true); this.pomodoroUIManager.updatePomodoroUI(); - + // Automatically calculate estimation for current notes - this.pomodoroUIManager.calculateAndDisplayEstimation(); + void this.pomodoroUIManager.calculateAndDisplayEstimation(); } else { pomodoroContainer.classList.add('sf-hidden'); this.pomodoroUIManager.showPomodoroSection(false); @@ -160,73 +161,84 @@ export class ListViewRenderer { reviewButtonsContainer = container.createDiv("review-buttons-container"); // Create all buttons once const navButtonsContainer = reviewButtonsContainer.createDiv("review-nav-buttons"); - const prevNoteBtn = navButtonsContainer.createEl("button", { text: "Previous", title: "Navigate to Previous Note", cls: "review-all-button" }); - prevNoteBtn.addEventListener("click", () => { this.plugin.reviewController.navigateToPreviousNote(); }); - const nextNoteBtn = navButtonsContainer.createEl("button", { text: "Next", title: "Navigate to Next Note", cls: "review-all-button" }); - nextNoteBtn.addEventListener("click", () => { this.plugin.reviewController.navigateToNextNote(); }); + const prevNoteBtn = navButtonsContainer.createEl("button", { text: "Previous", title: "Navigate to previous note", cls: "review-all-button" }); + prevNoteBtn.addEventListener("click", () => { void this.plugin.reviewController.navigateToPreviousNote(); }); + const nextNoteBtn = navButtonsContainer.createEl("button", { text: "Next", title: "Navigate to next note", cls: "review-all-button" }); + nextNoteBtn.addEventListener("click", () => { void this.plugin.reviewController.navigateToNextNote(); }); + + const reviewCurrentBtn = reviewButtonsContainer.createEl("button", { text: "Review current note", title: "Review the currently open note if it's due", cls: "review-all-button" }); + reviewCurrentBtn.addEventListener("click", () => { void this.plugin.reviewController.reviewCurrentNote(); }); + const reviewAllBtn = reviewButtonsContainer.createEl("button", { text: "Review all", title: "Start reviewing all due notes", cls: "review-all-button" }); + reviewAllBtn.addEventListener("click", () => { void this.plugin.reviewController.reviewAllTodaysNotes(); }); - const reviewCurrentBtn = reviewButtonsContainer.createEl("button", { text: "Review Current Note", title: "Review the currently open note if it's due", cls: "review-all-button" }); - reviewCurrentBtn.addEventListener("click", () => { this.plugin.reviewController.reviewCurrentNote(); }); - const reviewAllBtn = reviewButtonsContainer.createEl("button", { text: "Review All", title: "Start Reviewing All Due Notes", cls: "review-all-button" }); - reviewAllBtn.addEventListener("click", () => { this.plugin.reviewController.reviewAllTodaysNotes(); }); - if (this.plugin.settings.enableMCQ) { - const reviewAllMCQBtn = reviewButtonsContainer.createEl("button", { text: "Review All with MCQs", cls: "review-all-mcq-button" }); + const reviewAllMCQBtn = reviewButtonsContainer.createEl("button", { text: "Review all with MCQs", cls: "review-all-mcq-button" }); reviewAllMCQBtn.addEventListener("click", () => { this.plugin.reviewController.reviewAllNotesWithMCQ(true); }); } } reviewButtonsContainer.classList.remove('sf-hidden'); - + // Bulk action buttons let bulkActionButtons = container.querySelector(".review-bulk-actions") as HTMLElement; if (!bulkActionButtons) { bulkActionButtons = container.createDiv("review-bulk-actions"); - const reviewSelectedBtn = bulkActionButtons.createEl("button", { text: "Review Selected", cls: "review-bulk-button" }); - reviewSelectedBtn.addEventListener("click", async () => { - await this.plugin.reviewController.reviewNotes(this.getSelectedNotes(), false); // Use getter + const reviewSelectedBtn = bulkActionButtons.createEl("button", { text: "Review selected", cls: "review-bulk-button" }); + reviewSelectedBtn.addEventListener("click", () => { + void this.plugin.reviewController.reviewNotes(this.getSelectedNotes(), false); // Use getter this.setSelectedNotes([]); - await this.refreshSidebarView(); + void this.refreshSidebarView(); }); - const advanceSelectedBtn = bulkActionButtons.createEl("button", { text: "Advance Selected", cls: "review-bulk-button review-bulk-advance" }); - advanceSelectedBtn.addEventListener("click", async () => { + const advanceSelectedBtn = bulkActionButtons.createEl("button", { text: "Advance selected", cls: "review-bulk-button review-bulk-advance" }); + advanceSelectedBtn.addEventListener("click", () => { const pathsToAdvance = [...this.getSelectedNotes()]; if (pathsToAdvance.length === 0) { new Notice("No notes selected to advance."); return; } // The controller's advanceNotes will handle eligibility and notices - await this.plugin.reviewController.advanceNotes(pathsToAdvance); - this.setSelectedNotes([]); // Clear selection after action - // advanceNotes in controller already calls savePluginData and refreshSidebarView (via updateTodayNotes) - // A direct refresh here might be redundant but ensures UI update if controller logic changes. - await this.refreshSidebarView(); + void (async () => { + await this.plugin.reviewController.advanceNotes(pathsToAdvance); + this.setSelectedNotes([]); // Clear selection after action + // advanceNotes in controller already calls savePluginData and refreshSidebarView (via updateTodayNotes) + // A direct refresh here might be redundant but ensures UI update if controller logic changes. + await this.refreshSidebarView(); + })(); }); - const postponeSelectedBtn = bulkActionButtons.createEl("button", { text: "Postpone Selected", cls: "review-bulk-button review-bulk-postpone" }); - postponeSelectedBtn.addEventListener("click", async () => { + const postponeSelectedBtn = bulkActionButtons.createEl("button", { text: "Postpone selected", cls: "review-bulk-button review-bulk-postpone" }); + postponeSelectedBtn.addEventListener("click", () => { const pathsToPostpone = [...this.getSelectedNotes()]; if (pathsToPostpone.length === 0) { new Notice("No notes selected to postpone."); return; } this.setSelectedNotes([]); - await this.plugin.reviewController.postponeNotes(pathsToPostpone); - // postponeNotes in controller handles save and refresh. - await this.refreshSidebarView(); - // Notice is handled by controller/service for postpone. + void (async () => { + await this.plugin.reviewController.postponeNotes(pathsToPostpone); + // postponeNotes in controller handles save and refresh. + await this.refreshSidebarView(); + // Notice is handled by controller/service for postpone. + })(); }); - const removeSelectedBtn = bulkActionButtons.createEl("button", { text: "Remove Selected", cls: "review-bulk-button review-bulk-remove" }); - removeSelectedBtn.addEventListener("click", async () => { + const removeSelectedBtn = bulkActionButtons.createEl("button", { text: "Remove selected", cls: "review-bulk-button review-bulk-remove" }); + removeSelectedBtn.addEventListener("click", () => { const pathsToRemove = [...this.getSelectedNotes()]; // Use getter - const confirmed = confirm(`Remove ${pathsToRemove.length} selected notes from review schedule?`); - if (!confirmed) return; - this.setSelectedNotes([]); - await this.plugin.reviewController.removeNotes(pathsToRemove); - await this.plugin.savePluginData(); - await this.refreshSidebarView(); - new Notice(`Removed ${pathsToRemove.length} selected notes.`); + new ConfirmationModal( + this.plugin.app, + 'Remove Selected Notes', + `Remove ${pathsToRemove.length} selected notes from review schedule?`, + () => { + this.setSelectedNotes([]); + void (async () => { + await this.plugin.reviewController.removeNotes(pathsToRemove); + await this.plugin.savePluginData(); + await this.refreshSidebarView(); + new Notice(`Removed ${pathsToRemove.length} selected notes.`); + })(); + } + ).open(); }); } this.updateBulkActionButtonsVisibility(container); // Update visibility based on current selection @@ -234,7 +246,7 @@ export class ListViewRenderer { } else if (reviewButtonsContainer) { reviewButtonsContainer.classList.add('sf-hidden'); const bulkActionButtons = container.querySelector(".review-bulk-actions") as HTMLElement; - if (bulkActionButtons) bulkActionButtons.style.display = 'none'; + if (bulkActionButtons) bulkActionButtons.toggleClass('sf-hidden', true); } } @@ -252,29 +264,29 @@ export class ListViewRenderer { } else if (anchor) { container.appendChild(caughtUpEl); } else { - container.prepend(caughtUpEl); // Fallback + container.prepend(caughtUpEl); // Fallback } } caughtUpEl.setText("All caught up! No notes due for review."); - caughtUpEl.style.display = ''; + caughtUpEl.toggleClass('sf-hidden', false); } else if (caughtUpEl) { - caughtUpEl.style.display = 'none'; + caughtUpEl.toggleClass('sf-hidden', true); } } - + private async _ensureAndUpdateDateSections(container: HTMLElement, sortedDateKeys: string[], groupedNotes: Record): Promise { const existingSectionElements = Array.from(container.querySelectorAll(".review-date-section")) as HTMLElement[]; - const dataKeysInDom = new Set(existingSectionElements.map(el => el.dataset.dateKey).filter(Boolean)); const dataKeysFromData = new Set(sortedDateKeys); let notesDisplayed = false; // Remove stale sections for (const sectionEl of existingSectionElements) { - if (!dataKeysFromData.has(sectionEl.dataset.dateKey!)) { + const key = sectionEl.dataset.dateKey; + if (key && !dataKeysFromData.has(key)) { sectionEl.remove(); } } - + // Update existing or create new sections for (const dateStr of sortedDateKeys) { const notesForSection = groupedNotes[dateStr]; @@ -291,41 +303,45 @@ export class ListViewRenderer { const headerRow = dateSectionEl.createDiv("review-date-header"); const headerContainer = headerRow.createDiv("review-date-header-container"); headerContainer.createEl("h3"); // Placeholder for heading - + // "Advance All" button for future sections const todayStart = DateUtils.startOfDay(new Date()); // Returns timestamp - const sectionDateKeyIsFuture = !["Due notes", "Today"].includes(dateStr) && - (notesForSection[0] && DateUtils.startOfDay(new Date(notesForSection[0].nextReviewDate)) > todayStart); + const sectionDateKeyIsFuture = !["Due notes", "Today"].includes(dateStr) && + (notesForSection[0] && DateUtils.startOfDay(new Date(notesForSection[0].nextReviewDate)) > todayStart); if (sectionDateKeyIsFuture) { - const advanceAllBtn = headerContainer.createEl("button", { text: "Advance All", cls: "review-date-action-button review-date-advance-all" }); + const advanceAllBtn = headerContainer.createEl("button", { text: "Advance all", cls: "review-date-action-button review-date-advance-all" }); advanceAllBtn.title = `Advance all notes in this section by 1 day`; - advanceAllBtn.addEventListener("click", async () => { + advanceAllBtn.addEventListener("click", () => { const currentNotesForSection = groupedNotes[dateStr] || []; if (currentNotesForSection.length === 0) return; - const confirmed = confirm(`Advance all ${currentNotesForSection.length} notes from "${dateStr}" by 1 day? (Only future notes will be affected)`); - if (!confirmed) return; - - const paths = currentNotesForSection.map(note => note.path); - await this.plugin.reviewController.advanceNotes(paths); - // Notices and refresh are handled by advanceNotes in controller. - // await this.refreshSidebarView(); // May be redundant + new ConfirmationModal( + this.plugin.app, + 'Advance All Notes', + `Advance all ${currentNotesForSection.length} notes from "${dateStr}" by 1 day? (Only future notes will be affected)`, + () => { + const paths = currentNotesForSection.map(note => note.path); + void this.plugin.reviewController.advanceNotes(paths); + } + ).open(); }); } - const postponeAllBtn = headerContainer.createEl("button", { text: "Postpone All", cls: "review-date-action-button review-date-postpone-all" }); + const postponeAllBtn = headerContainer.createEl("button", { text: "Postpone all", cls: "review-date-action-button review-date-postpone-all" }); postponeAllBtn.title = `Postpone all notes in this section by 1 day`; - postponeAllBtn.addEventListener("click", async () => { + postponeAllBtn.addEventListener("click", () => { const currentNotesForSection = groupedNotes[dateStr] || []; if (currentNotesForSection.length === 0) return; const daysToPostpone = 1; - const confirmed = confirm(`Postpone all ${currentNotesForSection.length} notes from "${dateStr}" by ${daysToPostpone} day(s)?`); - if (!confirmed) return; - - const paths = currentNotesForSection.map(note => note.path); - await this.plugin.reviewController.postponeNotes(paths, daysToPostpone); - // Notices and refresh are handled by postponeNotes in controller. - // await this.refreshSidebarView(); // May be redundant + new ConfirmationModal( + this.plugin.app, + 'Postpone All Notes', + `Postpone all ${currentNotesForSection.length} notes from "${dateStr}" by ${daysToPostpone} day(s)?`, + () => { + const paths = currentNotesForSection.map(note => note.path); + void this.plugin.reviewController.postponeNotes(paths, daysToPostpone); + } + ).open(); }); headerRow.createSpan("review-date-time"); // Placeholder for time @@ -336,11 +352,10 @@ export class ListViewRenderer { notesContainerEl = dateSectionEl.createDiv("review-notes-container"); } } - + // Update header content dateSectionEl.removeClass("review-date-section-overdue"); - const actualTodayStart = DateUtils.startOfDay(new Date()); - const isDefaultTodayView = !this.getActiveListBaseDate(); + // const isDefaultTodayView = !this.getActiveListBaseDate(); // Unused // A section is considered "overdue" if its category key is "Due notes". // This applies whether in default view (actual overdue notes) @@ -385,7 +400,8 @@ export class ListViewRenderer { // - If viewing a specific past date from calendar (isDefaultTodayView = false), // "Due notes" might be the category for notes *on* that day. // In this case, overdue days relative to that selected past date will be 0. - const referenceDateForDiff = isDefaultTodayView ? todayActualStart : DateUtils.startOfDay(this.getActiveListBaseDate()!); + const baseDate = this.getActiveListBaseDate(); + const referenceDateForDiff = baseDate ? DateUtils.startOfDay(baseDate) : todayActualStart; return Math.floor((referenceDateForDiff - new Date(note.nextReviewDate).getTime()) / (24 * 60 * 60 * 1000)); }); const maxDays = Math.max(0, ...daysDiff.filter(d => d >= 0 && !isNaN(d))); // Ensure positive days and filter NaN @@ -395,15 +411,15 @@ export class ListViewRenderer { overdueBadge = dateHeading.createSpan("review-overdue-badge sf-overdue-badge"); } overdueBadge.setText(` (${maxDays} ${maxDays === 1 ? 'day' : 'days'} overdue)`); - overdueBadge.style.display = ''; + overdueBadge.toggleClass('sf-hidden', false); } else if (overdueBadge) { - overdueBadge.style.display = 'none'; // No positive overdue days + overdueBadge.toggleClass('sf-hidden', true); // No positive overdue days } } else if (overdueBadge) { - overdueBadge.style.display = 'none'; // No overdue notes in section + overdueBadge.toggleClass('sf-hidden', true); // No overdue notes in section } } else if (overdueBadge) { - overdueBadge.style.display = 'none'; // Conditions for badge not met + overdueBadge.toggleClass('sf-hidden', true); // Conditions for badge not met } let sectionTime = 0; @@ -412,7 +428,7 @@ export class ListViewRenderer { await this._updateOrRenderNoteList(notesContainerEl, notesForSection, dateStr, container); } - + // Message for activeListBaseDate with no notes let noNotesForDateMsg = container.querySelector(".review-no-notes-for-date") as HTMLElement; const activeListBaseDate = this.getActiveListBaseDate(); @@ -421,9 +437,9 @@ export class ListViewRenderer { noNotesForDateMsg = container.createDiv("review-no-notes-for-date"); // Similar to all-caught-up } noNotesForDateMsg.setText(`No notes scheduled on or after ${activeListBaseDate.toLocaleDateString()}.`); - noNotesForDateMsg.style.display = ''; + noNotesForDateMsg.toggleClass('sf-hidden', false); } else if (noNotesForDateMsg) { - noNotesForDateMsg.style.display = 'none'; + noNotesForDateMsg.toggleClass('sf-hidden', true); } } @@ -434,28 +450,28 @@ export class ListViewRenderer { if (activeSession) { if (!sessionSection) { sessionSection = container.createDiv("review-session-section"); - new Setting(sessionSection).setHeading().setName("Active Review Session"); + new Setting(sessionSection).setHeading().setName("Active review session"); const sessionInfo = sessionSection.createDiv("review-session-info"); sessionInfo.createDiv({ cls: "review-session-name" }); sessionInfo.createDiv({ cls: "review-session-progress" }); const progressBarContainer = sessionInfo.createDiv("review-session-progress-bar-container"); progressBarContainer.createDiv("review-session-progress-bar"); - const continueBtn = sessionSection.createEl("button", { text: "Continue Session", cls: "review-session-continue" }); + const continueBtn = sessionSection.createEl("button", { text: "Continue session", cls: "review-session-continue" }); continueBtn.addEventListener("click", () => { /* ... continue logic ... */ }); // TODO: Implement continue logic if not already present - const endBtn = sessionSection.createEl("button", { text: "End Session", cls: "review-session-end" }); - endBtn.addEventListener("click", () => { - this.plugin.reviewSessionService.setActiveSession(null); - this.refreshSidebarView(); + const endBtn = sessionSection.createEl("button", { text: "End session", cls: "review-session-end" }); + endBtn.addEventListener("click", () => { + this.plugin.reviewSessionService.setActiveSession(null); + void this.refreshSidebarView(); }); } - sessionSection.style.display = ''; + 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}%`; } else if (sessionSection) { - sessionSection.style.display = 'none'; + sessionSection.toggleClass('sf-hidden', true); } } @@ -476,19 +492,20 @@ export class ListViewRenderer { if (upcomingKeys.length > 0) { if (!upcomingSection) { upcomingSection = container.createDiv("review-upcoming-section"); - new Setting(upcomingSection).setHeading().setName("Upcoming Reviews"); + new Setting(upcomingSection).setHeading().setName("Upcoming reviews"); upcomingSection.createDiv("review-upcoming-list"); // List container } - upcomingSection.style.display = ''; + upcomingSection.toggleClass('sf-hidden', false); const upcomingListEl = upcomingSection.querySelector(".review-upcoming-list") as HTMLElement; if (!upcomingListEl) return; // Should not happen const existingDayItemElements = Array.from(upcomingListEl.querySelectorAll(".review-upcoming-day")) as HTMLElement[]; - const dayKeysInDom = new Set(existingDayItemElements.map(el => el.dataset.dayKey).filter(Boolean)); + // Remove stale day items for (const dayItemEl of existingDayItemElements) { - if (!upcomingKeys.includes(dayItemEl.dataset.dayKey!)) { + const key = dayItemEl.dataset.dayKey; + if (key && !upcomingKeys.includes(key)) { dayItemEl.remove(); } } @@ -511,21 +528,21 @@ 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", async () => { // Attach listener once + dayItemEl.addEventListener("click", () => { // Attach listener once const currentDayKey = dayItemEl.dataset.dayKey; if (!currentDayKey) return; const expandedUpcomingDayKey = this.getExpandedUpcomingDayKey(); const isCurrentlyExpanded = expandedUpcomingDayKey === currentDayKey; this.setExpandedUpcomingDayKey(isCurrentlyExpanded ? null : currentDayKey); // Visual state and note list rendering will be handled by the main render pass - await this.refreshSidebarView(); // Trigger a re-render to reflect expansion + void this.refreshSidebarView(); // Trigger a re-render to reflect expansion }); } - + // Update summary const daySummaryNameEl = dayItemEl.querySelector(".review-upcoming-day-summary .review-upcoming-day-name") as HTMLElement; let upcomingDisplayHeader = dayKey; - if (notesForDay.length > 0) { // Should always be true here + if (notesForDay.length > 0) { // Should always be true here const sampleUpcomingDate = new Date(notesForDay[0].nextReviewDate); const formattedUpcomingDate = DateUtils.formatDate(sampleUpcomingDate.getTime(), 'medium'); if (["Today", "Tomorrow"].includes(dayKey) || dayKey.startsWith("In ")) { @@ -545,15 +562,15 @@ export class ListViewRenderer { if (!notesContainerEl) { notesContainerEl = dayItemEl.createDiv("review-upcoming-notes-container"); } - notesContainerEl.style.display = ''; + notesContainerEl.toggleClass('sf-hidden', false); await this._updateOrRenderNoteList(notesContainerEl, notesForDay, dayKey, container); } else if (notesContainerEl) { - notesContainerEl.style.display = 'none'; // Hide instead of removing + notesContainerEl.toggleClass('sf-hidden', true); // Hide instead of removing } } } else if (upcomingSection) { - upcomingSection.style.display = 'none'; + upcomingSection.toggleClass('sf-hidden', true); } } @@ -561,9 +578,9 @@ export class ListViewRenderer { * Renders or updates a list of notes within a given container. */ private async _updateOrRenderNoteList( - notesContainer: HTMLElement, - notes: ReviewSchedule[], - dateStr: string, + notesContainer: HTMLElement, + notes: ReviewSchedule[], + dateStr: string, parentContainerForBulkActions: HTMLElement ): Promise { const existingNoteElements = Array.from(notesContainer.querySelectorAll('.review-note-item[data-note-path]')) as HTMLElement[]; @@ -608,7 +625,7 @@ export class ListViewRenderer { for (const noteEl of notesInOrder) { notesContainer.appendChild(noteEl); } - + this.setLastSelectedNotePath(lastSelectedNotePathRef.current); } @@ -653,9 +670,9 @@ export class ListViewRenderer { private updateBulkActionButtonsVisibility(container: HTMLElement): void { const selectedNotesPaths = this.getSelectedNotes(); const bulkActionsContainer = container.querySelector('.review-bulk-actions') as HTMLElement; - + if (bulkActionsContainer) { - bulkActionsContainer.style.display = selectedNotesPaths.length > 1 ? 'flex' : 'none'; + 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; @@ -667,9 +684,9 @@ export class ListViewRenderer { return schedule && DateUtils.startOfDay(new Date(schedule.nextReviewDate)) > todayStart; }); advanceSelectedBtn.disabled = !hasEligibleFutureNote; - advanceSelectedBtn.style.display = ''; // Always show if bulk actions are visible, rely on disabled state + advanceSelectedBtn.toggleClass('sf-hidden', false); // Always show if bulk actions are visible, rely on disabled state } else { - advanceSelectedBtn.disabled = true; + advanceSelectedBtn.disabled = true; // advanceSelectedBtn.style.display = 'none'; // Or hide if no selection } } @@ -694,7 +711,7 @@ export class ListViewRenderer { const reviewButtonsContainer = container.querySelector('.review-buttons-container'); if (reviewButtonsContainer) { - (reviewButtonsContainer as HTMLElement).style.display = dueNotesForStats.length > 0 ? '' : 'none'; + (reviewButtonsContainer as HTMLElement).toggleClass('sf-hidden', dueNotesForStats.length === 0); } this.updateBulkActionButtonsVisibility(container); @@ -703,15 +720,15 @@ export class ListViewRenderer { const notesInCurrentContext = this.plugin.reviewController.getTodayNotes(); if (allCaughtUpEl) { - (allCaughtUpEl as HTMLElement).style.display = notesInCurrentContext.length === 0 ? '' : 'none'; + (allCaughtUpEl as HTMLElement).toggleClass('sf-hidden', notesInCurrentContext.length !== 0); if (notesInCurrentContext.length === 0) { - allCaughtUpEl.setText(this.getActiveListBaseDate() ? "No notes for selected date." : "All caught up! No notes due for review."); + allCaughtUpEl.setText(this.getActiveListBaseDate() ? "No notes for selected date." : "All caught up! No notes due for review."); } } else if (notesInCurrentContext.length === 0) { const buttonsContainer = container.querySelector('.review-buttons-container'); const newCaughtUpEl = container.createDiv("review-all-caught-up"); newCaughtUpEl.setText(this.getActiveListBaseDate() ? "No notes for selected date." : "All caught up! No notes due for review."); - + // Insert after stats or buttons if they exist // const statsEl = container.querySelector(".review-stats-list-view"); // Stats section is removed const anchor = buttonsContainer; // || statsEl; // Stats section removed @@ -720,7 +737,7 @@ export class ListViewRenderer { } else if (anchor) { container.appendChild(newCaughtUpEl); } else { - container.prepend(newCaughtUpEl); // Fallback + container.prepend(newCaughtUpEl); // Fallback } } } @@ -770,11 +787,11 @@ export class ListViewRenderer { let overdueBadge = headerTextEl.querySelector(".review-overdue-badge") as HTMLElement | null; if (dateStr === "Due notes") { - const daysDiff = await Promise.all(notesInSection.map(async noteEl => { + const daysDiff = notesInSection.map(noteEl => { const path = (noteEl as HTMLElement).dataset.notePath; const noteSchedule = path ? this.plugin.reviewScheduleService.schedules[path] : null; return noteSchedule ? Math.abs(Math.floor((noteSchedule.nextReviewDate - DateUtils.startOfDay()) / (24 * 60 * 60 * 1000))) : 0; - })); + }); const maxDays = Math.max(0, ...daysDiff.filter(d => !isNaN(d))); if (maxDays > 0) { const badgeText = ` (${maxDays} ${maxDays === 1 ? 'day' : 'days'} overdue)`; @@ -807,7 +824,7 @@ export class ListViewRenderer { /** * Group notes by their review date, considering activeListBaseDate. */ - async groupNotesByDate(notes: ReviewSchedule[], includeFuture: boolean = false): Promise> { + groupNotesByDate(notes: ReviewSchedule[], _includeFuture = false): Record { const grouped: Record = {}; const actualTodayStart = DateUtils.startOfDay(new Date()); const activeListBaseDate = this.getActiveListBaseDate(); @@ -831,7 +848,7 @@ export class ListViewRenderer { // Determine the group string. // If activeListBaseDate is set, format relative to that date. // If activeListBaseDate is null (default view), format relative to actual current time. - let dateStr = DateUtils.formatDate(note.nextReviewDate, 'relative', activeListBaseDate); + const dateStr = DateUtils.formatDate(note.nextReviewDate, 'relative', activeListBaseDate); // Overdue notes will now naturally be grouped under "Due notes" (or similar key from DateUtils) // by the dateStr determined above, rather than being merged into "Today". diff --git a/ui/sidebar/note-item-renderer.ts b/ui/sidebar/note-item-renderer.ts index b82a52b..f9d1663 100644 --- a/ui/sidebar/note-item-renderer.ts +++ b/ui/sidebar/note-item-renderer.ts @@ -3,6 +3,7 @@ import SpaceforgePlugin from "../../main"; import { ReviewSchedule } from "../../models/review-schedule"; import { DateUtils } from "../../utils/dates"; import { EstimationUtils } from "../../utils/estimation"; +import { ConfirmationModal } from "../confirmation-modal"; export class NoteItemRenderer { private plugin: SpaceforgePlugin; @@ -60,7 +61,7 @@ export class NoteItemRenderer { const currentStepDisplay = note.reviewCount < totalInitialSteps ? note.reviewCount + 1 : totalInitialSteps; phaseEl.createDiv({ title: "Initial", text: "Initial" }); phaseEl.createDiv({ title: `${currentStepDisplay}/${totalInitialSteps}`, text: `${currentStepDisplay}/${totalInitialSteps}` }); - const phaseTimeEl = phaseEl.createDiv({ cls: "phase-time", title: formattedTime, text: formattedTime }); + phaseEl.createDiv({ cls: "phase-time", title: formattedTime, text: formattedTime }); phaseEl.addClass("review-phase-initial"); } else { phaseEl.setText(note.scheduleCategory === 'graduated' ? "Graduated" : "Spaced"); @@ -71,16 +72,16 @@ export class NoteItemRenderer { EstimationUtils.formatTimeWithColor(estimatedTime, timeElNew); } } - + // Drag handle visibility (buttons are static, drag handle might change) const buttonsEl = noteEl.querySelector(".review-note-buttons") as HTMLElement; - let dragHandleEl = buttonsEl?.querySelector(".review-note-drag-handle") as HTMLElement; + const dragHandleEl = buttonsEl?.querySelector(".review-note-drag-handle") as HTMLElement; 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); // Ensure draggable attribute is managed if it was set directly if (isDraggable && !dragHandleEl.hasAttribute("draggable")) { - // No need to set draggable here, mousedown does it. + // No need to set draggable here, mousedown does it. } else if (!isDraggable) { noteEl.removeAttribute("draggable"); // Ensure main element is not draggable } @@ -146,7 +147,7 @@ export class NoteItemRenderer { const advanceBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-advance" }); setIcon(advanceBtn, "arrow-left-circle"); // Or 'chevrons-left', 'skip-back' advanceBtn.title = "Advance"; - + const postponeBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-postpone" }); setIcon(postponeBtn, "arrow-right-circle"); // Or 'chevrons-right', 'skip-forward' postponeBtn.title = "Postpone"; @@ -154,7 +155,7 @@ export class NoteItemRenderer { const removeBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-remove" }); setIcon(removeBtn, "trash-2"); removeBtn.title = "Remove"; - + const dragHandleEl = buttonsEl.createDiv("review-note-drag-handle"); // Create drag handle structure dragHandleEl.setAttribute('aria-label', 'Drag to reorder'); for (let i = 0; i < 3; i++) { @@ -164,72 +165,83 @@ export class NoteItemRenderer { // --- Attach Event Listeners (once) --- // Note: these listeners will use 'noteToRender' from the outer scope at the time of creation. // If note data within the item needs to be fresh for these handlers, they might need to re-fetch it via noteEl.dataset.notePath - + titleEl.addEventListener("click", (e) => { e.stopPropagation(); const path = noteEl.dataset.notePath; // Get current path from element - if (path) this.plugin.reviewController.openNoteWithoutReview(path); + if (path) void this.plugin.reviewController.openNoteWithoutReview(path); }); - reviewBtn.addEventListener("click", async (e) => { + reviewBtn.addEventListener("click", (e) => { e.stopPropagation(); const path = noteEl.dataset.notePath; if (path) { - await this.plugin.reviewController.reviewNote(path); - // onNoteAction is often called by the review process itself, - // but calling here ensures refresh if reviewNote doesn't trigger it. - await onNoteAction(); + void (async () => { + await this.plugin.reviewController.reviewNote(path); + // onNoteAction is often called by the review process itself, + // but calling here ensures refresh if reviewNote doesn't trigger it. + await onNoteAction(); + })(); } }); - advanceBtn.addEventListener("click", async (e) => { + advanceBtn.addEventListener("click", (e) => { e.stopPropagation(); if (advanceBtn.disabled) return; const path = noteEl.dataset.notePath; if (path) { - await this.plugin.reviewController.advanceNote(path); - // The controller's advanceNote method handles notices and sidebar refresh. - // onNoteAction ensures this renderer's parent (ListViewRenderer) refreshes. - await onNoteAction(); + void (async () => { + await this.plugin.reviewController.advanceNote(path); + // The controller's advanceNote method handles notices and sidebar refresh. + // onNoteAction ensures this renderer's parent (ListViewRenderer) refreshes. + await onNoteAction(); + })(); } }); - postponeBtn.addEventListener("click", async (e) => { + postponeBtn.addEventListener("click", (e) => { e.stopPropagation(); const path = noteEl.dataset.notePath; if (path) { - try { - await this.plugin.reviewController.postponeNote(path); - await this.plugin.savePluginData(); - new Notice(`Note postponed`); - await onNoteAction(); - } catch (error) { - new Notice("Failed to postpone note."); - await onNoteAction(); - } + void (async () => { + try { + await this.plugin.reviewController.postponeNote(path); + new Notice(`Note postponed`); + await onNoteAction(); + } catch (_error) { + new Notice("Failed to postpone note."); + await onNoteAction(); + } + })(); } }); - removeBtn.addEventListener("click", async (e) => { + removeBtn.addEventListener("click", (e) => { e.stopPropagation(); const path = noteEl.dataset.notePath; if (path) { const file = this.plugin.app.vault.getAbstractFileByPath(path); - const confirmed = confirm(`Remove "${file instanceof TFile ? file.basename : path}" from review schedule?`); - if (!confirmed) return; - - try { - await this.plugin.reviewScheduleService.removeFromReview(path); - await this.plugin.savePluginData(); - new Notice(`Note removed from review schedule`); - await onNoteAction(); - } catch (error) { - new Notice("Failed to remove note from schedule."); - await onNoteAction(); - } + new ConfirmationModal( + this.plugin.app, + 'Remove Note', + `Remove "${file instanceof TFile ? file.basename : path}" from review schedule?`, + () => { + void (async () => { + try { + this.plugin.reviewScheduleService.removeFromReview(path); + await this.plugin.savePluginData(); + new Notice(`Note removed from review schedule`); + await onNoteAction(); + } catch (_error) { + new Notice("Failed to remove note from schedule."); + await onNoteAction(); + } + })(); + } + ).open(); } }); - + dragHandleEl.addEventListener("mousedown", (e) => { e.stopPropagation(); // Only enable dragging if not disabled (checked by _populateNoteItemDetails via class) @@ -240,7 +252,7 @@ export class NoteItemRenderer { noteEl.addEventListener("dragstart", (e) => { const path = noteEl.dataset.notePath; if (path && noteEl.getAttribute("draggable") === "true") { // Check if draggable - e.dataTransfer?.setData("text/plain", path); + e.dataTransfer?.setData("text/plain", path); } else { e.preventDefault(); // Prevent drag if not supposed to be draggable } @@ -291,7 +303,7 @@ export class NoteItemRenderer { lastSelectedNotePathRef.current = currentPath; onSelectionChange(); }); - + noteEl.addEventListener('contextmenu', (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -306,9 +318,9 @@ export class NoteItemRenderer { menu.addItem((item) => item .setTitle("Review note") .setIcon("play-circle") - .onClick(async () => { - this.plugin.reviewController.reviewNote(path); - await onNoteAction(); + .onClick(() => { + void this.plugin.reviewController.reviewNote(path); + void onNoteAction(); })); menu.addItem((item) => item .setTitle("Postpone by 1 day") @@ -340,15 +352,21 @@ export class NoteItemRenderer { menu.addItem((item) => item .setTitle("Remove from review") .setIcon("trash") - .onClick(async () => { + .onClick(() => { const file = this.plugin.app.vault.getAbstractFileByPath(path); - const confirmed = confirm(`Remove "${file instanceof TFile ? file.basename : path}" from review schedule?`); - if (confirmed) { - await this.plugin.reviewScheduleService.removeFromReview(path); - await this.plugin.savePluginData(); - new Notice("Note removed from review schedule."); - await onNoteAction(); - } + new ConfirmationModal( + this.plugin.app, + 'Remove Note', + `Remove "${file instanceof TFile ? file.basename : path}" from review schedule?`, + () => { + void (async () => { + this.plugin.reviewScheduleService.removeFromReview(path); + await this.plugin.savePluginData(); + new Notice("Note removed from review schedule."); + await onNoteAction(); + })(); + } + ).open(); })); menu.showAtMouseEvent(e); }); diff --git a/ui/sidebar/pomodoro-ui-manager.ts b/ui/sidebar/pomodoro-ui-manager.ts index ad069f2..774d1c9 100644 --- a/ui/sidebar/pomodoro-ui-manager.ts +++ b/ui/sidebar/pomodoro-ui-manager.ts @@ -7,8 +7,7 @@ export class PomodoroUIManager { private attachedContainer: HTMLElement | null = null; // The container provided by ListViewRenderer // References to Pomodoro UI elements - private pomodoroVisibilityToggleBtnContainer: HTMLElement | null = null; - private pomodoroVisibilityToggleBtn: HTMLButtonElement | null = null; + // private pomodoroVisibilityToggleBtnContainer & Btn removed as unused public pomodoroRootEl: HTMLElement | null = null; // Container for the actual timer content private pomodoroTimerDisplayEl: HTMLElement | null = null; private pomodoroStartBtn: HTMLButtonElement | null = null; @@ -21,24 +20,24 @@ export class PomodoroUIManager { private pomodoroQuickLongInput: HTMLInputElement | null = null; private pomodoroQuickSessionsInput: HTMLInputElement | null = null; private pomodoroCalculationResultEl: HTMLElement | null = null; - + // New estimation and cycle tracking elements (moved to calculation panel) private pomodoroCycleProgressEl: HTMLElement | null = null; - + // User override input elements private pomodoroUserOverrideHoursInput: HTMLInputElement | null = null; private pomodoroUserOverrideMinutesInput: HTMLInputElement | null = null; private pomodoroUserAddToEstimationCheckbox: HTMLInputElement | null = null; // private isPomodoroSectionOpen: boolean = false; // No longer needed, section is always "open" - private areButtonsVisible: boolean = true; // For Play/Pause/Skip buttons - private isTimerTextVisible: boolean = true; // For the timer countdown text + private areButtonsVisible = true; // For Play/Pause/Skip buttons + private isTimerTextVisible = true; // For the timer countdown text private longPressTimer: number | null = null; private veryLongPressTimer: number | null = null; private readonly LONG_PRESS_DURATION = 500; // ms private readonly VERY_LONG_PRESS_DURATION = 1500; // ms - private didLongPress: boolean = false; - private didVeryLongPress: boolean = false; + private didLongPress = false; + private didVeryLongPress = false; /** @@ -56,12 +55,12 @@ 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."); + new Notice("Invalid Pomodoro durations. Settings not saved. Please enter positive numbers."); // eslint-disable-line obsidianmd/ui/sentence-case // 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); - if(this.pomodoroQuickLongInput) this.pomodoroQuickLongInput.value = String(this.plugin.settings.pomodoroLongBreakDuration); - if(this.pomodoroQuickSessionsInput) this.pomodoroQuickSessionsInput.value = String(this.plugin.settings.pomodoroSessionsUntilLongBreak); + if (this.pomodoroQuickWorkInput) this.pomodoroQuickWorkInput.value = String(this.plugin.settings.pomodoroWorkDuration); + if (this.pomodoroQuickShortInput) this.pomodoroQuickShortInput.value = String(this.plugin.settings.pomodoroShortBreakDuration); + if (this.pomodoroQuickLongInput) this.pomodoroQuickLongInput.value = String(this.plugin.settings.pomodoroLongBreakDuration); + if (this.pomodoroQuickSessionsInput) this.pomodoroQuickSessionsInput.value = String(this.plugin.settings.pomodoroSessionsUntilLongBreak); return false; } } @@ -78,7 +77,7 @@ export class PomodoroUIManager { */ public attachAndRender(container: HTMLElement): void { this.attachedContainer = container; - + // Ensure pomodoroRootEl exists and is parented correctly // The old pomodoroVisibilityToggleBtnContainer is now hidden by CSS and its logic removed. let rootElWasCreated = false; @@ -98,9 +97,9 @@ export class PomodoroUIManager { if (rootElWasCreated || (this.pomodoroRootEl && this.pomodoroRootEl.children.length === 0)) { this.renderPomodoroTimer(this.pomodoroRootEl); } - + // Always update UI details as the section is considered always open - this.updatePomodoroUI(); + this.updatePomodoroUI(); } // getIsPomodoroSectionOpen, setIsPomodoroSectionOpen, setupPomodoroVisibilityToggleButton, updatePomodoroVisibility are no longer needed @@ -154,15 +153,14 @@ export class PomodoroUIManager { // Timer Display if (!this.pomodoroTimerDisplayEl || this.pomodoroTimerDisplayEl.parentElement !== mainControlsRow) { this.pomodoroTimerDisplayEl?.remove(); - this.pomodoroTimerDisplayEl = mainControlsRow.createDiv("pomodoro-timer-display"); - this.pomodoroTimerDisplayEl.addClass("pomodoro-timer-fade"); + this.pomodoroTimerDisplayEl = mainControlsRow.createDiv({ cls: "pomodoro-timer-display pomodoro-timer-fade" }); } this.pomodoroTimerDisplayEl.setText(this.plugin.pomodoroService.getFormattedTimeLeft()); // Add event listeners for long-press and click on timer display if (this.pomodoroTimerDisplayEl) { this.pomodoroTimerDisplayEl.addEventListener("mousedown", (e) => { // Prevent text selection during long press - e.preventDefault(); + e.preventDefault(); this.didLongPress = false; this.didVeryLongPress = false; @@ -173,11 +171,11 @@ export class PomodoroUIManager { }, this.LONG_PRESS_DURATION); this.veryLongPressTimer = window.setTimeout(() => { - this.didVeryLongPress = true; + this.didVeryLongPress = true; this.isTimerTextVisible = !this.isTimerTextVisible; // Toggle timer text // If timer text is now hidden, also hide buttons. If shown, show buttons. - this.areButtonsVisible = this.isTimerTextVisible; - this.updateTimerTextDisplay(); + this.areButtonsVisible = this.isTimerTextVisible; + this.updateTimerTextDisplay(); this.updateButtonVisibility(); }, this.VERY_LONG_PRESS_DURATION); }); @@ -210,7 +208,7 @@ export class PomodoroUIManager { this.pomodoroTimerDisplayEl.addEventListener("mouseup", handlePressEnd); this.pomodoroTimerDisplayEl.addEventListener("touchend", handlePressEnd); - + const cancelPress = () => { if (this.veryLongPressTimer) window.clearTimeout(this.veryLongPressTimer); if (this.longPressTimer) window.clearTimeout(this.longPressTimer); @@ -222,10 +220,10 @@ export class PomodoroUIManager { this.pomodoroTimerDisplayEl.addEventListener("mouseleave", cancelPress); this.pomodoroTimerDisplayEl.addEventListener("touchmove", cancelPress); - + // Touchstart needs to mirror mousedown logic for setting up timers - this.pomodoroTimerDisplayEl.addEventListener("touchstart", (e) => { - e.preventDefault(); + this.pomodoroTimerDisplayEl.addEventListener("touchstart", (e) => { + e.preventDefault(); this.didLongPress = false; this.didVeryLongPress = false; @@ -257,7 +255,7 @@ export class PomodoroUIManager { this.pomodoroCycleProgressEl?.remove(); this.pomodoroCycleProgressEl = container.createDiv("pomodoro-cycle-progress"); } - + // Quick Settings Toggle Button - REMOVED // if (!this.pomodoroQuickSettingsToggleBtn || this.pomodoroQuickSettingsToggleBtn.parentElement !== mainControlsRow) { // this.pomodoroQuickSettingsToggleBtn?.remove(); @@ -267,8 +265,14 @@ export class PomodoroUIManager { // this.pomodoroQuickSettingsToggleBtn.addEventListener("click", () => { // const panel = this.pomodoroQuickSettingsPanelEl; // if (!panel) return; - // const isCurrentlyHidden = panel.style.display === 'none' || !panel.style.display; - // panel.style.display = isCurrentlyHidden ? 'flex' : 'none'; + // const isCurrentlyHidden = panel.classList.contains('sf-hidden'); + // if (isCurrentlyHidden) { + // panel.classList.remove('sf-hidden'); + // panel.classList.add('sf-visible'); + // } else { + // panel.classList.remove('sf-visible'); + // panel.classList.add('sf-hidden'); + // } // if (isCurrentlyHidden) { // Populate inputs when opening // if(this.pomodoroQuickWorkInput) this.pomodoroQuickWorkInput.value = String(this.plugin.settings.pomodoroWorkDuration); @@ -278,22 +282,23 @@ export class PomodoroUIManager { // } // }); // } - + // Quick Settings Panel // The panel is now created directly under the mainControlsRow for better layout control with dropdown let settingsPanelContainer = container.querySelector(".pomodoro-settings-panel-container") as HTMLElement; if (!settingsPanelContainer) { settingsPanelContainer = container.createDiv("pomodoro-settings-panel-container"); } - + if (!this.pomodoroQuickSettingsPanelEl || this.pomodoroQuickSettingsPanelEl.parentElement !== settingsPanelContainer) { this.pomodoroQuickSettingsPanelEl?.remove(); this.pomodoroQuickSettingsPanelEl = settingsPanelContainer.createDiv("pomodoro-quick-settings-panel"); this.pomodoroQuickSettingsPanelEl.classList.add('sf-hidden'); // Initial state // Recreate inputs and buttons if panel is new - const createQuickSetting = (labelText: string, inputType: string = 'number'): HTMLInputElement => { - const settingDiv = this.pomodoroQuickSettingsPanelEl!.createDiv("pomodoro-quick-setting"); + const panel = this.pomodoroQuickSettingsPanelEl; + const createQuickSetting = (labelText: string, inputType = 'number'): HTMLInputElement => { + const settingDiv = panel.createDiv("pomodoro-quick-setting"); settingDiv.createEl("label", { text: labelText }); const input = settingDiv.createEl("input", { type: inputType }); input.setAttr("min", "1"); @@ -324,46 +329,46 @@ export class PomodoroUIManager { // }); // User override time inputs - const overrideContainer = this.pomodoroQuickSettingsPanelEl.createDiv("pomodoro-override-container"); - const overrideLabel = overrideContainer.createEl("label", { text: "Override time (optional):", cls: "pomodoro-override-label" }); - + const overrideContainer = this.pomodoroQuickSettingsPanelEl.createDiv({ cls: "pomodoro-override-container" }); + overrideContainer.createEl("label", { text: "Override time (optional):", cls: "pomodoro-override-label" }); + const overrideInputsContainer = overrideContainer.createDiv("pomodoro-override-inputs"); this.pomodoroUserOverrideHoursInput = overrideInputsContainer.createEl("input", { type: "number", cls: "pomodoro-override-hours" }); this.pomodoroUserOverrideHoursInput.setAttr("min", "0"); this.pomodoroUserOverrideHoursInput.setAttr("placeholder", "H"); this.pomodoroUserOverrideHoursInput.value = String(this.plugin.pluginState.pomodoroUserOverrideHours); - + const hoursLabel = overrideInputsContainer.createSpan("pomodoro-override-label-small"); - hoursLabel.setText("h"); - + hoursLabel.setText("h"); // eslint-disable-line obsidianmd/ui/sentence-case + this.pomodoroUserOverrideMinutesInput = overrideInputsContainer.createEl("input", { type: "number", cls: "pomodoro-override-minutes" }); this.pomodoroUserOverrideMinutesInput.setAttr("min", "0"); this.pomodoroUserOverrideMinutesInput.setAttr("placeholder", "M"); this.pomodoroUserOverrideMinutesInput.value = String(this.plugin.pluginState.pomodoroUserOverrideMinutes); - + const minutesLabel = overrideInputsContainer.createSpan("pomodoro-override-label-small"); - minutesLabel.setText("m"); - + minutesLabel.setText("m"); // eslint-disable-line obsidianmd/ui/sentence-case + // Add to estimation toggle const toggleContainer = overrideContainer.createDiv("pomodoro-override-toggle-container"); this.pomodoroUserAddToEstimationCheckbox = toggleContainer.createEl("input", { type: "checkbox", cls: "pomodoro-add-to-estimation" }); this.pomodoroUserAddToEstimationCheckbox.checked = this.plugin.pluginState.pomodoroUserAddToEstimation; - + const toggleLabel = toggleContainer.createEl("label", { text: "Add to estimated time", cls: "pomodoro-toggle-label" }); toggleLabel.setAttribute("for", "pomodoro-add-to-estimation"); - const calculateBtn = buttonsContainer.createEl("button", { text: "Calculate Reading Time", cls: "pomodoro-quick-calculate-btn" }); - calculateBtn.addEventListener("click", async () => { + const calculateBtn = buttonsContainer.createEl("button", { text: "Calculate reading time", cls: "pomodoro-quick-calculate-btn" }); + calculateBtn.addEventListener("click", () => { const settingsSaved = this._savePomodoroSettings(); if (settingsSaved) { - await this.calculateAndDisplayPomodoroEstimate(); + void this.calculateAndDisplayPomodoroEstimate(); } }); this.pomodoroCalculationResultEl = this.pomodoroQuickSettingsPanelEl.createDiv({ cls: "pomodoro-calculation-result" }); this.pomodoroCalculationResultEl.classList.add('sf-hidden'); } - + this.updatePomodoroUI(); // Ensure UI reflects current state after potential recreation } @@ -377,16 +382,16 @@ export class PomodoroUIManager { this.saveUserOverrideSettings(); // Use notes from the review controller, which are context-aware (selected date or actual today) - const notesForEstimate = this.plugin.reviewController.getTodayNotes(); - + const notesForEstimate = this.plugin.reviewController.getTodayNotes(); + // Check if we have either notes or user override const userOverrideHours = this.plugin.pluginState.pomodoroUserOverrideHours || 0; const userOverrideMinutes = this.plugin.pluginState.pomodoroUserOverrideMinutes || 0; const userOverrideTimeInMinutes = (userOverrideHours * 60) + userOverrideMinutes; - + if (notesForEstimate.length === 0 && userOverrideTimeInMinutes === 0) { const activeDate = this.plugin.reviewController.getCurrentReviewDateOverride(); - const message = activeDate + const message = activeDate ? `No notes scheduled for ${new Date(activeDate).toLocaleDateString()} to calculate.` : "No notes currently due to calculate."; this.pomodoroCalculationResultEl.setText(message); @@ -397,23 +402,22 @@ export class PomodoroUIManager { // Use the PomodoroService to calculate estimation (this also resets and updates cycle tracking) const result = await this.plugin.pomodoroService.calculateEstimationFromNotes(notesForEstimate); - + if (!result) { this.pomodoroCalculationResultEl.setText("Unable to calculate estimation."); - this.pomodoroCalculationResultEl.style.display = 'block'; + this.pomodoroCalculationResultEl.removeClass('sf-hidden'); return; } - const { totalReadingTimeInSeconds, totalReadingTimeInMinutes, pomodorosNeeded, totalTimeWithBreaksMinutes } = result; + const { totalReadingTimeInSeconds, pomodorosNeeded, totalTimeWithBreaksMinutes } = result; // Get user override values for display (already declared above) const addToEstimation = this.plugin.pluginState.pomodoroUserAddToEstimation || false; - const formattedTotalReadingTime = EstimationUtils.formatTime(totalReadingTimeInSeconds); const formattedTotalTimeWithBreaks = EstimationUtils.formatTime(Math.ceil(totalTimeWithBreaksMinutes * 60)); this.pomodoroCalculationResultEl.empty(); - + // Show base reading time if we have notes and it wasn't completely overridden if (notesForEstimate.length > 0 && totalReadingTimeInSeconds > 0 && (!userOverrideTimeInMinutes || addToEstimation)) { // Calculate base reading time without overrides for display @@ -427,19 +431,19 @@ export class PomodoroUIManager { // Show message when using only override time this.pomodoroCalculationResultEl.createEl("p", { text: `Using override time only (no notes).` }); } - + // Show user override information if applicable if (userOverrideTimeInMinutes > 0) { - const overrideText = addToEstimation + const overrideText = addToEstimation ? `Added ${userOverrideHours}h ${userOverrideMinutes}m override time.` : `Using ${userOverrideHours}h ${userOverrideMinutes}m override time (replacing estimate).`; this.pomodoroCalculationResultEl.createEl("p", { text: overrideText, cls: "pomodoro-override-info" }); } - + this.pomodoroCalculationResultEl.createEl("p", { text: `Requires ~${pomodorosNeeded} Pomodoro work session(s).` }); this.pomodoroCalculationResultEl.createEl("p", { text: `Total time with breaks: ~${formattedTotalTimeWithBreaks}.` }); - this.pomodoroCalculationResultEl.style.display = 'block'; - + this.pomodoroCalculationResultEl.removeClass('sf-hidden'); + // Update the cycle progress display to show the new estimation this.updateCycleProgressDisplay(); } @@ -455,8 +459,8 @@ export class PomodoroUIManager { this.plugin.pluginState.pomodoroUserOverrideHours = hours; this.plugin.pluginState.pomodoroUserOverrideMinutes = minutes; this.plugin.pluginState.pomodoroUserAddToEstimation = addToEstimation; - - this.plugin.savePluginData(); + + void this.plugin.savePluginData(); } /** @@ -475,10 +479,10 @@ export class PomodoroUIManager { } if (isCurrentlyHidden) { // Populate inputs when opening - if(this.pomodoroQuickWorkInput) this.pomodoroQuickWorkInput.value = String(this.plugin.settings.pomodoroWorkDuration); - if(this.pomodoroQuickShortInput) this.pomodoroQuickShortInput.value = String(this.plugin.settings.pomodoroShortBreakDuration); - if(this.pomodoroQuickLongInput) this.pomodoroQuickLongInput.value = String(this.plugin.settings.pomodoroLongBreakDuration); - if(this.pomodoroQuickSessionsInput) this.pomodoroQuickSessionsInput.value = String(this.plugin.settings.pomodoroSessionsUntilLongBreak); + if (this.pomodoroQuickWorkInput) this.pomodoroQuickWorkInput.value = String(this.plugin.settings.pomodoroWorkDuration); + if (this.pomodoroQuickShortInput) this.pomodoroQuickShortInput.value = String(this.plugin.settings.pomodoroShortBreakDuration); + if (this.pomodoroQuickLongInput) this.pomodoroQuickLongInput.value = String(this.plugin.settings.pomodoroLongBreakDuration); + if (this.pomodoroQuickSessionsInput) this.pomodoroQuickSessionsInput.value = String(this.plugin.settings.pomodoroSessionsUntilLongBreak); } else { // Panel is being closed, so save the settings this._savePomodoroSettings(); @@ -494,14 +498,14 @@ export class PomodoroUIManager { // Using opacity is generally smoother and preserves layout/border. } } - + private updateButtonVisibility(): void { - const buttonsVisibility = this.areButtonsVisible ? '' : 'none'; + const isRunning = this.plugin.pluginState.pomodoroIsRunning; - if (this.pomodoroStartBtn) this.pomodoroStartBtn.style.display = isRunning ? 'none' : buttonsVisibility; - if (this.pomodoroStopBtn) this.pomodoroStopBtn.style.display = isRunning ? buttonsVisibility : 'none'; - if (this.pomodoroSkipBtn) this.pomodoroSkipBtn.style.display = buttonsVisibility; + if (this.pomodoroStartBtn) this.pomodoroStartBtn.toggleClass('sf-hidden', isRunning); + if (this.pomodoroStopBtn) this.pomodoroStopBtn.toggleClass('sf-hidden', !isRunning); + if (this.pomodoroSkipBtn) this.pomodoroSkipBtn.toggleClass('sf-hidden', !this.areButtonsVisible); } @@ -509,13 +513,13 @@ export class PomodoroUIManager { * Updates the Pomodoro UI based on the current state from PomodoroService. */ public updatePomodoroUI(): void { - // Don't update if the UI hasn't been attached yet or pomodoroRootEl is not created + // Don't update if the UI hasn't been attached yet or pomodoroRootEl is not created if (!this.attachedContainer || !this.pomodoroRootEl) { - return; + return; } - + // Ensure the root container is visible (it should always be, managed by attachAndRender) - this.pomodoroRootEl.style.display = ''; + this.pomodoroRootEl.removeClass('sf-hidden'); const state = this.plugin.pluginState; const service = this.plugin.pomodoroService; @@ -526,45 +530,45 @@ export class PomodoroUIManager { if (state.pomodoroCurrentMode !== 'idle') { this.pomodoroTimerDisplayEl.addClass(`mode-${state.pomodoroCurrentMode}`); } else { - this.pomodoroTimerDisplayEl.addClass('mode-idle'); + this.pomodoroTimerDisplayEl.addClass('mode-idle'); } if (state.pomodoroIsRunning) { this.pomodoroTimerDisplayEl.addClass('timer-visible'); } else { - this.pomodoroTimerDisplayEl.removeClass('timer-visible'); + this.pomodoroTimerDisplayEl.removeClass('timer-visible'); } } // This function now primarily updates timer text, mode classes, and calls helper visibility functions. - + if (this.pomodoroTimerDisplayEl) { this.pomodoroTimerDisplayEl.setText(service.getFormattedTimeLeft()); // Always set text for screen readers / state this.updateTimerTextDisplay(); // Then apply visual visibility for the text - + this.pomodoroTimerDisplayEl.className = 'pomodoro-timer-display pomodoro-timer-fade'; // Reset classes if (state.pomodoroCurrentMode !== 'idle') { this.pomodoroTimerDisplayEl.addClass(`mode-${state.pomodoroCurrentMode}`); } else { - this.pomodoroTimerDisplayEl.addClass('mode-idle'); + this.pomodoroTimerDisplayEl.addClass('mode-idle'); } // 'timer-visible' class might be redundant if opacity is used, but keep for now if it affects other styles. if (state.pomodoroIsRunning) { this.pomodoroTimerDisplayEl.addClass('timer-visible'); } else { - this.pomodoroTimerDisplayEl.removeClass('timer-visible'); + this.pomodoroTimerDisplayEl.removeClass('timer-visible'); } } - + this.updateButtonVisibility(); // Update button visibility based on their state // Update classes on the root element itself for styling paused/idle states this.pomodoroRootEl.toggleClass('is-running', state.pomodoroIsRunning); this.pomodoroRootEl.toggleClass('is-paused', !state.pomodoroIsRunning && state.pomodoroCurrentMode !== 'idle'); this.pomodoroRootEl.toggleClass('is-idle', state.pomodoroCurrentMode === 'idle'); - + // Hide calculation result if quick settings panel is closed if (this.pomodoroCalculationResultEl && this.pomodoroQuickSettingsPanelEl?.style.display === 'none') { - this.pomodoroCalculationResultEl.style.display = 'none'; + this.pomodoroCalculationResultEl.addClass('sf-hidden'); } // Update cycle progress display @@ -580,23 +584,23 @@ export class PomodoroUIManager { if (!this.pomodoroCycleProgressEl) return; const cycleProgress = this.plugin.pomodoroService.getCycleProgress(); - + if (cycleProgress) { const { current, total, workSessionsRemaining, totalWorkSessions, totalTimeMinutes } = cycleProgress; - + // Calculate completed sessions const completedSessions = totalWorkSessions - workSessionsRemaining; - + // Format total time const totalHours = Math.floor(totalTimeMinutes / 60); const totalMinutes = Math.round(totalTimeMinutes % 60); const timeString = totalHours > 0 ? `${totalHours}H/${totalMinutes}M` : `${totalMinutes}M`; - + this.pomodoroCycleProgressEl.setText(`Cycles ${current}/${total} - Sessions ${completedSessions}/${totalWorkSessions} - ${timeString}`); - this.pomodoroCycleProgressEl.style.display = ''; + this.pomodoroCycleProgressEl.removeClass('sf-hidden'); this.pomodoroCycleProgressEl.addClass('cycle-active'); } else { - this.pomodoroCycleProgressEl.style.display = 'none'; + this.pomodoroCycleProgressEl.addClass('sf-hidden'); } } @@ -606,7 +610,7 @@ export class PomodoroUIManager { private formatTime(totalSeconds: number): string { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); - + if (hours > 0) { return `${hours}h ${minutes}m`; } else { diff --git a/ui/upcoming-events.ts b/ui/upcoming-events.ts index 9e11a5b..7a4f10b 100644 --- a/ui/upcoming-events.ts +++ b/ui/upcoming-events.ts @@ -1,7 +1,7 @@ import { Notice, setIcon } from "obsidian"; import SpaceforgePlugin from "../main"; import { CalendarEvent, UpcomingEvent } from "../models/calendar-event"; -import { DateUtils } from "../utils/dates"; + /** * Upcoming events component for calendar view @@ -11,12 +11,12 @@ export class UpcomingEvents { * Reference to the main plugin */ plugin: SpaceforgePlugin; - + /** * Container element for upcoming events */ containerEl: HTMLElement; - + /** * Upcoming events data */ @@ -36,7 +36,7 @@ export class UpcomingEvents { /** * Render upcoming events list */ - async render(): Promise { + render(): void { if (!this.plugin.settings.enableCalendarEvents || !this.plugin.settings.showUpcomingEvents) { this.containerEl.empty(); return; @@ -48,7 +48,7 @@ export class UpcomingEvents { } // Load upcoming events data - await this.loadUpcomingEvents(); + this.loadUpcomingEvents(); // Clear container this.containerEl.empty(); @@ -64,7 +64,7 @@ export class UpcomingEvents { /** * Load upcoming events data */ - async loadUpcomingEvents(): Promise { + loadUpcomingEvents(): void { this.upcomingEvents = this.plugin.calendarEventService.getUpcomingEvents( this.plugin.settings.upcomingEventsDays || 7 ); @@ -75,13 +75,13 @@ export class UpcomingEvents { */ private renderEmptyState(): void { const emptyState = this.containerEl.createDiv("upcoming-events-empty"); - + const emptyIcon = emptyState.createDiv("upcoming-events-empty-icon"); setIcon(emptyIcon, "calendar"); - + const emptyText = emptyState.createDiv("upcoming-events-empty-text"); emptyText.setText("No upcoming events"); - + const emptySubtext = emptyState.createDiv("upcoming-events-empty-subtext"); emptySubtext.setText("Events will appear here once you create them"); } @@ -92,10 +92,10 @@ export class UpcomingEvents { private renderEventsList(): void { // Create header const header = this.containerEl.createDiv("upcoming-events-header"); - + const headerTitle = header.createDiv("upcoming-events-title"); - headerTitle.setText("Upcoming Events"); - + headerTitle.setText("Upcoming events"); + const headerSubtitle = header.createDiv("upcoming-events-subtitle"); const daysText = this.plugin.settings.upcomingEventsDays || 7; headerSubtitle.setText(`Next ${daysText} days`); @@ -120,17 +120,17 @@ export class UpcomingEvents { this.upcomingEvents.forEach(upcomingEvent => { let dayKey: string; - + if (upcomingEvent.isToday) { dayKey = "Today"; } else if (upcomingEvent.isTomorrow) { dayKey = "Tomorrow"; } else { const eventDate = new Date(upcomingEvent.event.date); - dayKey = eventDate.toLocaleDateString(undefined, { - weekday: 'short', - month: 'short', - day: 'numeric' + dayKey = eventDate.toLocaleDateString(undefined, { + weekday: 'short', + month: 'short', + day: 'numeric' }); } @@ -138,7 +138,10 @@ export class UpcomingEvents { eventsByDay.set(dayKey, []); } - eventsByDay.get(dayKey)!.push(upcomingEvent); + const dayList = eventsByDay.get(dayKey); + if (dayList) { + dayList.push(upcomingEvent); + } }); return eventsByDay; @@ -149,13 +152,13 @@ export class UpcomingEvents { */ private renderDaySection(container: HTMLElement, dayKey: string, dayEvents: UpcomingEvent[]): void { const daySection = container.createDiv("upcoming-events-day-section"); - + // Day header const dayHeader = daySection.createDiv("upcoming-events-day-header"); - + const dayTitle = dayHeader.createDiv("upcoming-events-day-title"); dayTitle.setText(dayKey); - + if (dayKey === "Today") { daySection.addClass("today"); } else if (dayKey === "Tomorrow") { @@ -164,7 +167,7 @@ export class UpcomingEvents { // Events for this day const dayEventsContainer = daySection.createDiv("upcoming-events-day-events"); - + dayEvents.forEach(upcomingEvent => { this.renderEventItem(dayEventsContainer, upcomingEvent); }); @@ -176,21 +179,21 @@ export class UpcomingEvents { private renderEventItem(container: HTMLElement, upcomingEvent: UpcomingEvent): void { const event = upcomingEvent.event; const eventItem = container.createDiv("upcoming-events-event-item"); - + // Event color indicator const colorIndicator = eventItem.createDiv("upcoming-events-event-color"); const eventColor = this.plugin.calendarEventService?.getEventColor(event) || '#95A5A6'; colorIndicator.style.setProperty('--event-color', eventColor); - + // Event content const eventContent = eventItem.createDiv("upcoming-events-event-content"); - + // Event title and time const eventHeader = eventContent.createDiv("upcoming-events-event-header"); - + const eventTitle = eventHeader.createDiv("upcoming-events-event-title"); eventTitle.setText(event.title); - + if (event.time) { const eventTime = eventHeader.createDiv("upcoming-events-event-time"); eventTime.setText(event.time); @@ -198,37 +201,37 @@ export class UpcomingEvents { const eventTime = eventHeader.createDiv("upcoming-events-event-time"); eventTime.setText("All day"); } - + // Event details if (event.description || event.location) { const eventDetails = eventContent.createDiv("upcoming-events-event-details"); - + if (event.location) { const eventLocation = eventDetails.createDiv("upcoming-events-event-location"); setIcon(eventLocation, "map-pin"); eventLocation.appendText(" " + event.location); } - + if (event.description) { const eventDescription = eventDetails.createDiv("upcoming-events-event-description"); eventDescription.setText(event.description.substring(0, 100) + (event.description.length > 100 ? "..." : "")); } } - + // Event category const eventCategory = eventContent.createDiv("upcoming-events-event-category"); eventCategory.setText(event.category); - + // Click handler to show event details eventItem.addEventListener("click", () => { this.showEventDetails(event); }); - + // Add hover effect eventItem.addEventListener("mouseenter", () => { eventItem.addClass("hover"); }); - + eventItem.addEventListener("mouseleave", () => { eventItem.removeClass("hover"); }); @@ -241,29 +244,29 @@ export class UpcomingEvents { */ private showEventDetails(event: CalendarEvent): void { const eventDate = new Date(event.date); - const dateStr = eventDate.toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' + const dateStr = eventDate.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' }); - + let details = `📅 ${event.title}\n📆 ${dateStr}`; - + if (event.time) { details += `\n🕐 ${event.time}`; } - + if (event.description) { details += `\n📝 ${event.description}`; } - + if (event.location) { details += `\n📍 ${event.location}`; } - + details += `\n🏷️ ${event.category}`; - + new Notice(details, 8000); } diff --git a/utils/dates.ts b/utils/dates.ts index 68b6910..8914367 100644 --- a/utils/dates.ts +++ b/utils/dates.ts @@ -13,7 +13,7 @@ export class DateUtils { newDate.setHours(0, 0, 0, 0); return newDate.getTime(); } - + /** * Add days to a timestamp * @@ -24,7 +24,7 @@ export class DateUtils { static addDays(timestamp: number, days: number): number { return timestamp + (days * 24 * 60 * 60 * 1000); } - + /** * Format a timestamp as a readable date string * @@ -38,11 +38,9 @@ export class DateUtils { if (format === 'relative') { // Determine the reference date for relative calculations - const referenceDateForCalc = baseDateParam ? new Date(baseDateParam) : new Date(); // Normalize dates to their start of day for accurate day-based comparison const normalizedNoteEventDate = this.startOfDay(noteEventDate); - const normalizedReferenceDate = this.startOfDay(referenceDateForCalc); const normalizedActualCurrentDate = this.startOfDay(new Date()); // Actual current day, for "Due notes" // "Due notes" are always relative to the *actual* current day, regardless of baseDateParam @@ -83,22 +81,22 @@ export class DateUtils { } else if (format === 'short') { return noteEventDate.toLocaleDateString(); } else if (format === 'long') { - return noteEventDate.toLocaleDateString(undefined, { - weekday: 'long', - year: 'numeric', - month: 'long', - day: 'numeric' + return noteEventDate.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' }); } else { // Medium format (default) - return noteEventDate.toLocaleDateString(undefined, { - weekday: 'short', - month: 'short', + return noteEventDate.toLocaleDateString(undefined, { + weekday: 'short', + month: 'short', day: 'numeric' }); } } - + /** * Get the day difference between two timestamps * @@ -109,15 +107,15 @@ export class DateUtils { static dayDifference(timestamp1: number, timestamp2: number): number { const date1 = new Date(timestamp1); const date2 = new Date(timestamp2); - + // Reset to midnight date1.setHours(0, 0, 0, 0); date2.setHours(0, 0, 0, 0); - + // Calculate difference in days const diffTime = Math.abs(date2.getTime() - date1.getTime()); const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - + return diffDays; } @@ -155,11 +153,11 @@ export class DateUtils { static dayDifferenceUTC(timestamp1: number, timestamp2: number): number { const date1UTCMidnight = this.startOfUTCDay(new Date(timestamp1)); const date2UTCMidnight = this.startOfUTCDay(new Date(timestamp2)); - + // Calculate difference in days const diffTime = Math.abs(date2UTCMidnight - date1UTCMidnight); const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24)); - + return diffDays; } @@ -171,7 +169,7 @@ export class DateUtils { */ static isSameDay(date1: Date, date2: Date): boolean { return date1.getFullYear() === date2.getFullYear() && - date1.getMonth() === date2.getMonth() && - date1.getDate() === date2.getDate(); + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate(); } } diff --git a/utils/estimation.ts b/utils/estimation.ts index 1822579..baadf00 100644 --- a/utils/estimation.ts +++ b/utils/estimation.ts @@ -15,22 +15,22 @@ export class EstimationUtils { fiction: 250, // Fiction/prose simple: 300 // Simple content }; - + /** * Average English word length in characters (including spaces) */ private static readonly AVG_WORD_LENGTH = 5.5; - + /** * Minimum review time in seconds */ private static readonly MIN_REVIEW_TIME = 30; - + /** * Reference to the plugin (for access to settings) */ private static plugin: SpaceforgePlugin; - + /** * Set the plugin reference * @@ -39,7 +39,7 @@ export class EstimationUtils { static setPlugin(plugin: SpaceforgePlugin): void { this.plugin = plugin; } - + /** * Get the user's reading speed from settings * @@ -49,17 +49,17 @@ export class EstimationUtils { static getReadingSpeed(contentType?: keyof typeof EstimationUtils.READING_SPEEDS): number { // Get base reading speed from settings const baseSpeed = this.plugin?.settings.readingSpeed || 200; - + // Apply content-specific adjustment if specified if (contentType) { const baseContentSpeed = this.READING_SPEEDS.notes; const contentSpeed = this.READING_SPEEDS[contentType]; return baseSpeed * (contentSpeed / baseContentSpeed); } - + return baseSpeed; } - + /** * Estimate review time for a file based on its content * @@ -68,35 +68,35 @@ export class EstimationUtils { * @param contentType Type of content for reading speed adjustment * @returns Estimated review time in seconds */ - static async estimateReviewTime( - file: TFile, - fileContent?: string, + static estimateReviewTime( + file: TFile, + fileContent?: string, contentType: keyof typeof EstimationUtils.READING_SPEEDS = 'notes' - ): Promise { + ): number { if (!file) { return this.MIN_REVIEW_TIME; } - + // Use file size as a rough proxy if content is not available if (!fileContent) { const sizeEstimate = Math.ceil(file.stat.size / (this.AVG_WORD_LENGTH * 7)) * 60; return Math.max(this.MIN_REVIEW_TIME, sizeEstimate); } - + // Count words in content const wordCount = this.countWords(fileContent); - + // Calculate reading time based on words and reading speed const readingSpeed = this.getReadingSpeed(contentType); const readingTimeMinutes = wordCount / readingSpeed; - + // Add some buffer time for actual review (thinking, interacting) const reviewTimeSeconds = Math.ceil(readingTimeMinutes * 60); - + // Return at least minimum review time return Math.max(this.MIN_REVIEW_TIME, reviewTimeSeconds); } - + /** * Calculate aggregate review time for multiple notes * @@ -107,15 +107,15 @@ export class EstimationUtils { if (!this.plugin) { return paths.length * this.MIN_REVIEW_TIME; } - + let totalTime = 0; for (const path of paths) { totalTime += await this.plugin.dataStorage.estimateReviewTime(path); } - + return totalTime; } - + /** * Count words in text * @@ -131,12 +131,12 @@ export class EstimationUtils { .replace(/\*\*.*?\*\*/g, '$1') // Bold to plain text .replace(/\*.*?\*/g, '$1') // Italic to plain text .replace(/~~.*?~~/g, '$1'); // Strikethrough to plain text - + // Count words (sequences of non-whitespace characters) const words = cleanText.match(/\S+/g) || []; return words.length; } - + /** * Format seconds as a readable time string * @@ -145,13 +145,13 @@ export class EstimationUtils { */ static formatTime(seconds: number): string { const minutes = Math.floor(seconds / 60); - + if (minutes < 60) { return `${minutes} min`; } else { const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; - + if (remainingMinutes === 0) { return `${hours} hr`; } else { @@ -159,7 +159,7 @@ export class EstimationUtils { } } } - + /** * Format a time estimate with color coding based on duration * @@ -170,7 +170,7 @@ export class EstimationUtils { static formatTimeWithColor(seconds: number, element: HTMLElement): void { const formattedTime = this.formatTime(seconds); element.setText(formattedTime); - + // Color code based on duration if (seconds < 5 * 60) { // Less than 5 minutes element.addClass("review-time-short"); diff --git a/utils/event-emitter.ts b/utils/event-emitter.ts index 2775454..c324fe5 100644 --- a/utils/event-emitter.ts +++ b/utils/event-emitter.ts @@ -5,22 +5,22 @@ export class EventEmitter { /** * Event listeners by event name */ - private listeners: Record = {}; - + private listeners: Record void)[]> = {}; + /** * Register a listener for an event * * @param event Event name * @param callback Function to call when event is emitted */ - on(event: string, callback: Function): void { + on(event: string, callback: (...args: any[]) => void): void { if (!this.listeners[event]) { this.listeners[event] = []; } - + this.listeners[event].push(callback); } - + /** * Emit an event * @@ -31,26 +31,26 @@ export class EventEmitter { if (!this.listeners[event]) { return; } - + for (const callback of this.listeners[event]) { callback(...args); } } - + /** * Remove a listener for an event * * @param event Event name * @param callback Function to remove */ - off(event: string, callback: Function): void { + off(event: string, callback: (...args: any[]) => void): void { if (!this.listeners[event]) { return; } - + this.listeners[event] = this.listeners[event].filter(cb => cb !== callback); } - + /** * Remove all listeners for an event * diff --git a/utils/link-analyzer.ts b/utils/link-analyzer.ts index 7284362..465800f 100644 --- a/utils/link-analyzer.ts +++ b/utils/link-analyzer.ts @@ -8,7 +8,7 @@ export interface NoteLink { * The text content of the link */ text: string; - + /** * Whether this is an embed link (![[...]]) or a regular link ([[...]]) */ @@ -23,32 +23,32 @@ export interface FileNode { * Path to the file */ path: string; - + /** * File content */ content?: string; - + /** * Outgoing links (files this file links to) */ outgoingLinks: string[]; - + /** * Regular (non-embed) outgoing links */ regularLinks: string[]; - + /** * Embed outgoing links */ embedLinks: string[]; - + /** * Incoming links (files that link to this file) */ incomingLinks: string[]; - + /** * Count of incoming links (used for ranking) */ @@ -63,12 +63,12 @@ export interface ReviewHierarchy { * Root nodes in the hierarchy (starting points) */ rootNodes: string[]; - + /** * All nodes in the hierarchy */ nodes: Record; - + /** * Traversal order for review */ @@ -85,7 +85,7 @@ export class LinkAnalyzer { * Second capture group matches the content inside the brackets */ private static readonly LINK_REGEX = /(!?)(?:\[\[(.*?)\]\])/g; - + /** * Analyze links in a folder and build a review hierarchy * @@ -95,8 +95,8 @@ export class LinkAnalyzer { * @returns Review hierarchy for the folder */ static async analyzeFolder( - vault: Vault, - folder: TFolder, + vault: Vault, + folder: TFolder, includeSubfolders: boolean ): Promise { // Get all markdown files in the folder @@ -108,7 +108,7 @@ export class LinkAnalyzer { return parentPath === folder.path; } }); - + // Initialize nodes const nodes: Record = {}; for (const file of files) { @@ -121,17 +121,17 @@ export class LinkAnalyzer { incomingLinkCount: 0 }; } - + // Read file contents and extract links, preserving order for (const file of files) { try { const content = await vault.read(file); const node = nodes[file.path]; node.content = content; - + // Extract links in the order they appear const noteLinks = this.extractLinks(content); - + // Process each link in order for (const noteLink of noteLinks) { // Try to resolve the link to a full path @@ -140,7 +140,7 @@ export class LinkAnalyzer { // Add to appropriate outgoing links arrays if (!node.outgoingLinks.includes(resolvedPath)) { node.outgoingLinks.push(resolvedPath); - + // Also add to the appropriate type-specific array if (noteLink.isEmbed) { if (!node.embedLinks.includes(resolvedPath)) { @@ -152,7 +152,7 @@ export class LinkAnalyzer { } } } - + // Update incoming links for the target const targetNode = nodes[resolvedPath]; if (!targetNode.incomingLinks.includes(file.path)) { @@ -161,22 +161,22 @@ export class LinkAnalyzer { } } } - } catch (error) { /* handle error */ } + } catch (_error) { /* handle error */ } } - + // Find the node with the most outgoing links as the starting point const startingNode = this.findStartingNode(nodes); - + // Create traversal order that respects the order of links const traversalOrder = this.createTraversalOrder(nodes, startingNode); - + return { rootNodes: startingNode, nodes, traversalOrder }; } - + /** * Extract links from markdown content in the order they appear * @@ -186,10 +186,10 @@ export class LinkAnalyzer { static extractLinks(content: string): NoteLink[] { const links: NoteLink[] = []; let match; - + // Reset regex lastIndex to ensure we start from the beginning this.LINK_REGEX.lastIndex = 0; - + // Find all [[...]] or ![[...]] links in order while ((match = this.LINK_REGEX.exec(content)) !== null) { links.push({ @@ -197,10 +197,10 @@ export class LinkAnalyzer { isEmbed: match[1] === '!' // True if it has an exclamation mark }); } - + return links; } - + /** * Resolve a link to a full file path * @@ -218,29 +218,29 @@ export class LinkAnalyzer { return exactFile.path; } } - + // Try to find by basename (without extension) const basename = link.split('/').pop(); if (!basename) return null; - + // Look for files with matching basename const matchingFiles = allFiles.filter(f => f.basename === basename); - + if (matchingFiles.length === 0) { return null; } - + if (matchingFiles.length === 1) { return matchingFiles[0].path; } - + // If multiple matches, try to find the one in the same folder const sourceDir = sourcePath.substring(0, sourcePath.lastIndexOf('/')); const sameDir = matchingFiles.find(f => f.path.startsWith(sourceDir + '/')); - + return sameDir ? sameDir.path : matchingFiles[0].path; } - + /** * Find the best starting node based on file naming and link structure * @@ -252,10 +252,10 @@ export class LinkAnalyzer { if (Object.keys(nodes).length === 0) { return []; } - + // Organize nodes into a folder structure for better matching const folderNodes: Record = {}; - + for (const path in nodes) { const folderPath = path.substring(0, path.lastIndexOf('/')); if (!folderNodes[folderPath]) { @@ -263,12 +263,12 @@ export class LinkAnalyzer { } folderNodes[folderPath].push(path); } - + // For each folder, look for the best starting node for (const folderPath in folderNodes) { const folderName = folderPath.split('/').pop()?.toLowerCase() || ''; const filesInFolder = folderNodes[folderPath]; - + // HIGHEST PRIORITY: Exact match with folder name for (const path of filesInFolder) { const fileName = path.split('/').pop()?.toLowerCase().replace(/\.md$/, '') || ''; @@ -293,7 +293,7 @@ export class LinkAnalyzer { } } } - + // FOURTH PRIORITY: Look for files with the most outgoing links // Prefer regular links over embeds const sortedNodes = Object.values(nodes) @@ -303,21 +303,21 @@ export class LinkAnalyzer { if (regularLinkDiff !== 0) { return regularLinkDiff; } - + // If they have the same number of regular links, check total outgoing links return b.outgoingLinks.length - a.outgoingLinks.length; }); - - if (sortedNodes.length > 0 && + + if (sortedNodes.length > 0 && (sortedNodes[0].regularLinks.length > 0 || sortedNodes[0].outgoingLinks.length > 0)) { // Return the node with the most links return [sortedNodes[0].path]; } - + // FALLBACK: Use all files as root nodes if none of the above criteria match return Object.keys(nodes).length > 0 ? [Object.keys(nodes)[0]] : []; } - + /** * Find root nodes (files with the most incoming links) * For backward compatibility, retained but replaced by findStartingNode @@ -328,7 +328,7 @@ export class LinkAnalyzer { private static findRootNodes(nodes: Record): string[] { return this.findStartingNode(nodes); } - + /** * Create a traversal order for reviewing files that respects the exact order of links * @@ -342,7 +342,7 @@ export class LinkAnalyzer { ): string[] { const visited = new Set(); const traversalOrder: string[] = []; - + // Track which folder each file belongs to const fileFolders = new Map(); for (const path in nodes) { @@ -350,11 +350,11 @@ export class LinkAnalyzer { const folderPath = path.substring(0, path.lastIndexOf('/')); fileFolders.set(path, folderPath); } - + const mainFolder = fileFolders.get(startNodePath) || ""; // Helper function for depth-first traversal, respecting link order and folder constraints - const traverse = (currentNodePath: string, currentDepth: number = 0, currentMainFolder: string) => { + const traverse = (currentNodePath: string, currentDepth = 0, currentMainFolder: string) => { if (visited.has(currentNodePath)) { return; } @@ -363,15 +363,13 @@ export class LinkAnalyzer { if (!node) { return; } - + visited.add(currentNodePath); traversalOrder.push(currentNodePath); - - const currentNodeFolder = fileFolders.get(currentNodePath) || ""; - + // Prefer regular links over embeds, then all outgoing links if no regular ones const linksToFollow = node.regularLinks.length > 0 ? node.regularLinks : node.outgoingLinks; - + for (const linkedPath of linksToFollow) { if (!nodes[linkedPath]) { // Ensure the linked path is a known node in the current analysis context continue; @@ -384,28 +382,30 @@ export class LinkAnalyzer { if (!visited.has(linkedPath)) { // Avoid re-traversing already processed branches traverse(linkedPath, currentDepth + 1, currentMainFolder); } else { + // no-op } } else { // This link goes outside the main folder being analyzed or is not a known node. } } }; - + // Start traversal from the designated startNodePath, if it exists if (nodes[startNodePath]) { traverse(startNodePath, 0, mainFolder); } else { + // no-op } - + // The traversal initiated by traverse(startNodePath, 0) should now correctly capture // only the linked hierarchy within the same folder. We no longer add all other // unlinked files from the mainFolder. - + // Log the traversal order summary - + return traversalOrder; } - + /** * Create a traversal order for reviewing files * @@ -421,32 +421,32 @@ export class LinkAnalyzer { if (rootNodes.length === 1) { return this.createOrderedTraversal(nodes, rootNodes[0]); } - + // Otherwise use the original implementation for multiple root nodes const visited = new Set(); const traversalOrder: string[] = []; - + // Helper function for depth-first traversal const traverse = (nodePath: string) => { if (visited.has(nodePath)) return; - + visited.add(nodePath); traversalOrder.push(nodePath); - + const node = nodes[nodePath]; if (!node) return; - + // Traverse outgoing links depth-first for (const linkedPath of node.outgoingLinks) { traverse(linkedPath); } }; - + // Start traversal from each root node for (const rootPath of rootNodes) { traverse(rootPath); } - + // Add any remaining unvisited nodes for (const nodePath of Object.keys(nodes)) { if (!visited.has(nodePath)) { @@ -454,10 +454,10 @@ export class LinkAnalyzer { visited.add(nodePath); } } - + return traversalOrder; } - + /** * Analyze links in a single note * @@ -467,29 +467,29 @@ export class LinkAnalyzer { * @returns Array of resolved link paths in the order they appear */ static async analyzeNoteLinks( - vault: Vault, - filePath: string, - regularOnly: boolean = false + vault: Vault, + filePath: string, + regularOnly = false ): Promise { const file = vault.getAbstractFileByPath(filePath); if (!(file instanceof TFile)) { return []; } - + try { const content = await vault.read(file); const noteLinks = this.extractLinks(content); const resolvedLinks: string[] = []; - + // Maintain a record of links that appear multiple times // We'll only include the first occurrence in our results const seenLinks = new Set(); - + // Filter links if regularOnly is true - const filteredLinks = regularOnly - ? noteLinks.filter(link => !link.isEmbed) + const filteredLinks = regularOnly + ? noteLinks.filter(link => !link.isEmbed) : noteLinks; - + // Process links in the exact order they appear in the document for (const link of filteredLinks) { const resolvedPath = this.resolveLink( @@ -497,15 +497,15 @@ export class LinkAnalyzer { filePath, vault.getMarkdownFiles() ); - + if (resolvedPath && !seenLinks.has(resolvedPath)) { resolvedLinks.push(resolvedPath); seenLinks.add(resolvedPath); } } - - + + return resolvedLinks; - } catch (error) { /* handle error */ return []; } + } catch (_error) { /* handle error */ return []; } } }