From 7ac79ae05fdfaa21179f460612bd271aede57857 Mon Sep 17 00:00:00 2001 From: dralkh Date: Sun, 15 Feb 2026 10:05:34 +0300 Subject: [PATCH] feat: add recurring notes and drag-and-drop reordering - Implement recurring notes feature with interval selection - Add drag-and-drop reordering with position persistence - Update data model to support recurring notes - Add UI controls for recurrence settings - Implement position tracking for drag-and-drop - Align recurring note colors with other button backgrounds Recurring notes can be toggled via button or context menu, show visual indicators, and automatically schedule next occurrences. Drag-and-drop reordering persists the new order across sessions using existing customNoteOrder system. --- main.ts | 5 + models/review-schedule.ts | 6 + models/settings.ts | 16 +++ services/recurring-notes-service.ts | 195 ++++++++++++++++++++++++++++ styles/_variables.css | 5 + styles/sidebar.css | 56 ++++++++ ui/sidebar/list-view-renderer.ts | 161 ++++++++++++++++++----- ui/sidebar/note-item-renderer.ts | 158 ++++++++++++++++------ 8 files changed, 533 insertions(+), 69 deletions(-) create mode 100644 services/recurring-notes-service.ts diff --git a/main.ts b/main.ts index a30118e..3319782 100644 --- a/main.ts +++ b/main.ts @@ -25,6 +25,7 @@ import { ReviewSessionService } from './services/review-session-service'; import { MCQService } from './services/mcq-service'; import { PomodoroService } from './services/pomodoro-service'; import { CalendarEventService } from './services/calendar-event-service'; +import { RecurringNotesService } from './services/recurring-notes-service'; /** * Spaceforge: Spaced Repetition Plugin for Obsidian @@ -44,6 +45,7 @@ export default class SpaceforgePlugin extends Plugin { mcqService: MCQService; pomodoroService: PomodoroService; calendarEventService: CalendarEventService; + recurringNotesService: RecurringNotesService; private readonly stylesheetPath: string = "styles.css"; @@ -99,6 +101,9 @@ export default class SpaceforgePlugin extends Plugin { const eventsArray = Object.values(this.pluginState.calendarEvents); this.calendarEventService.initialize(eventsArray); + // Initialize RecurringNotesService after settings are loaded + this.recurringNotesService = new RecurringNotesService(this); + diff --git a/models/review-schedule.ts b/models/review-schedule.ts index d0e8224..62d1876 100644 --- a/models/review-schedule.ts +++ b/models/review-schedule.ts @@ -65,6 +65,12 @@ export interface ReviewSchedule { }; schedulingAlgorithm: 'sm2' | 'fsrs'; // Determines which algo rules apply + + // Recurring notes settings + isRecurring?: boolean; + recurrenceInterval?: number; // Interval in days for recurring notes + recurrenceEndDate?: number | null; // Optional end date for recurrence (timestamp) + originalSchedule?: ReviewSchedule | null; // Store original schedule for reference } // Removed INITIAL_INTERVALS, isInitialPhase, and getInitialInterval. diff --git a/models/settings.ts b/models/settings.ts index 918c33b..5da81c0 100644 --- a/models/settings.ts +++ b/models/settings.ts @@ -335,6 +335,18 @@ export interface SpaceforgeSettings { * Default: 'personal' */ defaultEventCategory: string; + + /** + * Enable recurring notes feature + * Default: false + */ + enableRecurringNotes: boolean; + + /** + * Default recurrence interval for new recurring notes (in days) + * Default: 7 + */ + defaultRecurrenceInterval: number; } /** @@ -434,4 +446,8 @@ export const DEFAULT_SETTINGS: SpaceforgeSettings = { showUpcomingEvents: true, upcomingEventsDays: 7, defaultEventCategory: 'personal', + + // Recurring Notes Defaults + enableRecurringNotes: false, + defaultRecurrenceInterval: 7, }; diff --git a/services/recurring-notes-service.ts b/services/recurring-notes-service.ts new file mode 100644 index 0000000..d9b4a39 --- /dev/null +++ b/services/recurring-notes-service.ts @@ -0,0 +1,195 @@ +import SpaceforgePlugin from '../main'; +import { ReviewSchedule } from '../models/review-schedule'; +import { DateUtils } from '../utils/dates'; + +/** + * Service for managing recurring notes functionality + */ +export class RecurringNotesService { + plugin: SpaceforgePlugin; + + constructor(plugin: SpaceforgePlugin) { + this.plugin = plugin; + } + + /** + * Convert a regular note to a recurring note + * @param path Path to the note file + * @param interval Recurrence interval in days + * @param endDate Optional end date for recurrence (timestamp) + */ + async convertToRecurringNote(path: string, interval: number, endDate: number | null = null): Promise { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (!schedule) { + console.warn(`No schedule found for note: ${path}`); + return; + } + + // Store the original schedule before making it recurring + const originalSchedule: ReviewSchedule = { + ...schedule, + isRecurring: false, + recurrenceInterval: undefined, + recurrenceEndDate: undefined, + originalSchedule: null + }; + + // Update the schedule to be recurring + schedule.isRecurring = true; + schedule.recurrenceInterval = interval; + schedule.recurrenceEndDate = endDate; + schedule.originalSchedule = originalSchedule; + + // Schedule the next occurrence + await this.scheduleNextRecurrence(path); + + await this.plugin.savePluginData(); + } + + /** + * Schedule the next occurrence of a recurring note + * @param path Path to the note file + */ + async scheduleNextRecurrence(path: string): Promise { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (!schedule || !schedule.isRecurring || !schedule.recurrenceInterval) { + return; + } + + const now = Date.now(); + const nextReviewDate = schedule.nextReviewDate || now; + + // Calculate the next occurrence based on the recurrence interval + const nextOccurrenceDate = nextReviewDate + (schedule.recurrenceInterval * 24 * 60 * 60 * 1000); + + // If there's an end date and we've passed it, don't schedule further occurrences + if (schedule.recurrenceEndDate && nextOccurrenceDate > schedule.recurrenceEndDate) { + schedule.isRecurring = false; // Stop recurrence + return; + } + + // Update the schedule with the next occurrence date + schedule.nextReviewDate = nextOccurrenceDate; + schedule.lastReviewDate = nextReviewDate; // Mark as reviewed for this occurrence + + await this.plugin.savePluginData(); + } + + /** + * Convert a recurring note back to a regular note + * @param path Path to the note file + */ + async convertToRegularNote(path: string): Promise { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (!schedule || !schedule.isRecurring) { + return; + } + + // Restore original schedule if available + if (schedule.originalSchedule) { + const original = schedule.originalSchedule; + schedule.path = original.path; + schedule.lastReviewDate = original.lastReviewDate; + schedule.nextReviewDate = original.nextReviewDate; + schedule.ease = original.ease; + schedule.interval = original.interval; + schedule.consecutive = original.consecutive; + schedule.reviewCount = original.reviewCount; + schedule.repetitionCount = original.repetitionCount; + schedule.scheduleCategory = original.scheduleCategory; + schedule.fsrsData = original.fsrsData; + schedule.schedulingAlgorithm = original.schedulingAlgorithm; + } + + // Remove recurring properties + schedule.isRecurring = false; + schedule.recurrenceInterval = undefined; + schedule.recurrenceEndDate = undefined; + schedule.originalSchedule = null; + + await this.plugin.savePluginData(); + } + + /** + * Check if a note is recurring + * @param path Path to the note file + * @returns True if the note is recurring + */ + isNoteRecurring(path: string): boolean { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + return !!schedule?.isRecurring; + } + + /** + * Get recurrence information for a note + * @param path Path to the note file + * @returns Recurrence information or null if not recurring + */ + getRecurrenceInfo(path: string): { + interval: number; + endDate: number | null; + nextOccurrence: number; + } | null { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (!schedule?.isRecurring || !schedule.recurrenceInterval) { + return null; + } + + return { + interval: schedule.recurrenceInterval, + endDate: schedule.recurrenceEndDate || null, + nextOccurrence: schedule.nextReviewDate + }; + } + + /** + * Process all recurring notes and schedule their next occurrences + * This should be called periodically (e.g., daily) + */ + async processAllRecurringNotes(): Promise { + const schedules = this.plugin.reviewScheduleService.schedules; + const now = Date.now(); + + for (const path in schedules) { + const schedule = schedules[path]; + if (schedule?.isRecurring && schedule.recurrenceInterval) { + // Check if the note is due for its next occurrence + if (schedule.nextReviewDate <= now) { + await this.scheduleNextRecurrence(path); + } + } + } + + await this.plugin.savePluginData(); + } + + /** + * Update the recurrence interval for a note + * @param path Path to the note file + * @param newInterval New recurrence interval in days + */ + async updateRecurrenceInterval(path: string, newInterval: number): Promise { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (!schedule?.isRecurring) { + return; + } + + schedule.recurrenceInterval = newInterval; + await this.plugin.savePluginData(); + } + + /** + * Update the recurrence end date for a note + * @param path Path to the note file + * @param newEndDate New end date timestamp or null for no end date + */ + async updateRecurrenceEndDate(path: string, newEndDate: number | null): Promise { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + if (!schedule?.isRecurring) { + return; + } + + schedule.recurrenceEndDate = newEndDate; + await this.plugin.savePluginData(); + } +} \ No newline at end of file diff --git a/styles/_variables.css b/styles/_variables.css index 907895e..c9b698d 100644 --- a/styles/_variables.css +++ b/styles/_variables.css @@ -21,6 +21,11 @@ --sf-overdue-accent: #ff8c42; /* Warm orange/amber */ --sf-overdue-bg: rgba(255, 140, 66, 0.08); /* Very light orange tint */ + /* Recurring colors - aligned with other button backgrounds */ + --sf-recurring-accent: var(--interactive-accent, #5e81ac); /* Use same accent color as other buttons */ + --sf-recurring-accent-hover: color-mix(in srgb, var(--interactive-accent, #5e81ac), white 85%); /* Lighter version for hover */ + --sf-recurring-bg: rgba(94, 129, 172, 0.05); /* Very light blue tint to match accent */ + /* Text colors */ --sf-text: var(--text-normal, #333); --sf-text-muted: var(--text-muted, #888); diff --git a/styles/sidebar.css b/styles/sidebar.css index 4863871..b8ec15f 100644 --- a/styles/sidebar.css +++ b/styles/sidebar.css @@ -367,6 +367,7 @@ flex-wrap: wrap; transition: var(--sf-transition); border: 1px solid transparent; + position: relative; } .review-note-item:last-child { @@ -537,6 +538,26 @@ border: 1px dashed var(--sf-primary); } +.review-note-item.drag-over-above::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background-color: var(--sf-primary); +} + +.review-note-item.drag-over-below::after { + content: ""; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background-color: var(--sf-primary); +} + .review-all-caught-up { padding: var(--sf-space-md); text-align: center; @@ -910,3 +931,38 @@ .pomodoro-container.is-idle .pomodoro-timer-display { opacity: 0.5; /* Dim timer more when idle */ } + +/* Recurring Notes Styles */ +.review-note-item.recurring-note { + border-left: 3px solid var(--sf-recurring-accent); + background-color: var(--sf-recurring-bg); +} + +.recurrence-badge { + font-size: 11px; + padding: 2px 6px; + border-radius: 100px; + background-color: var(--sf-recurring-accent); + color: white; + font-weight: 500; + margin-left: 8px; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.recurrence-badge::before { + content: "↻"; + font-size: 10px; +} + +.review-note-button.review-note-recurrence { + background-color: var(--sf-recurring-accent); + color: white; + border-color: var(--sf-recurring-accent); +} + +.review-note-button.review-note-recurrence:hover { + background-color: var(--sf-recurring-accent-hover); + border-color: var(--sf-recurring-accent-hover); +} diff --git a/ui/sidebar/list-view-renderer.ts b/ui/sidebar/list-view-renderer.ts index 2a86c6b..b0595db 100644 --- a/ui/sidebar/list-view-renderer.ts +++ b/ui/sidebar/list-view-renderer.ts @@ -54,16 +54,19 @@ export class ListViewRenderer { this.refreshSidebarView = stateAccessors.refreshSidebarView; } - /** - * Render the list view content into the provided container. - * @param container Container element for list view content - */ - async render(container: HTMLElement): Promise { - // container.empty(); // Clear only the list view content area -- REMOVED + /** + * Render the list view content into the provided container. + * @param container Container element for list view content + */ + async render(container: HTMLElement): Promise { + // container.empty(); // Clear only the list view content area -- REMOVED - const activeListBaseDate = this.getActiveListBaseDate(); - const selectedNotes = this.getSelectedNotes(); - const dueNotesForStats = this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true); + const activeListBaseDate = this.getActiveListBaseDate(); + const selectedNotes = this.getSelectedNotes(); + const dueNotesForStats = this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true); + + // Setup drag-and-drop event handlers for the container + this.setupDragAndDrop(container); // --- Stats Section (REMOVED as per user request) --- // await this._ensureAndUpdateStatsSection(container, dueNotesForStats); @@ -727,31 +730,125 @@ export class ListViewRenderer { * Updates the visibility of the bulk action buttons based on selection count. * (Called by handleSelectionChange and render) */ - private updateBulkActionButtonsVisibility(container: HTMLElement): void { - const selectedNotesPaths = this.getSelectedNotes(); - const bulkActionsContainer = container.querySelector('.review-bulk-actions'); + private updateBulkActionButtonsVisibility(container: HTMLElement): void { + const selectedNotesPaths = this.getSelectedNotes(); + const bulkActionsContainer = container.querySelector('.review-bulk-actions'); - if (bulkActionsContainer) { - bulkActionsContainer.toggleClass('sf-hidden', selectedNotesPaths.length <= 1); + if (bulkActionsContainer) { + bulkActionsContainer.toggleClass('sf-hidden', selectedNotesPaths.length <= 1); - // Handle visibility/disabled state of "Advance Selected" button - const advanceSelectedBtn = bulkActionsContainer.querySelector('.review-bulk-advance'); - if (advanceSelectedBtn) { - if (selectedNotesPaths.length > 1) { - const todayStart = DateUtils.startOfUTCDay(new Date()); // Returns timestamp - const hasEligibleFutureNote = selectedNotesPaths.some(path => { - const schedule = this.plugin.reviewScheduleService.schedules[path]; - return schedule && DateUtils.startOfUTCDay(new Date(schedule.nextReviewDate)) > todayStart; - }); - advanceSelectedBtn.disabled = !hasEligibleFutureNote; - advanceSelectedBtn.toggleClass('sf-hidden', false); // Always show if bulk actions are visible, rely on disabled state - } else { - advanceSelectedBtn.disabled = true; - // advanceSelectedBtn.style.display = 'none'; // Or hide if no selection - } - } - } - } + // Handle visibility/disabled state of "Advance Selected" button + const advanceSelectedBtn = bulkActionsContainer.querySelector('.review-bulk-advance'); + if (advanceSelectedBtn) { + if (selectedNotesPaths.length > 1) { + const todayStart = DateUtils.startOfUTCDay(new Date()); // Returns timestamp + const hasEligibleFutureNote = selectedNotesPaths.some(path => { + const schedule = this.plugin.reviewScheduleService.schedules[path]; + return schedule && DateUtils.startOfUTCDay(new Date(schedule.nextReviewDate)) > todayStart; + }); + advanceSelectedBtn.disabled = !hasEligibleFutureNote; + advanceSelectedBtn.toggleClass('sf-hidden', false); // Always show if bulk actions are visible, rely on disabled state + } else { + advanceSelectedBtn.disabled = true; + // advanceSelectedBtn.style.display = 'none'; // Or hide if no selection + } + } + } + } + + /** + * Setup drag-and-drop event handlers for the container + */ + private setupDragAndDrop(container: HTMLElement): void { + // Add drag-over event handler + container.addEventListener('dragover', (e) => { + e.preventDefault(); + const target = e.target as HTMLElement; + const noteItem = target.closest('.review-note-item') as HTMLElement | null; + + if (noteItem) { + // Find the drop target container + const notesContainer = noteItem.closest('.review-notes-container'); + if (notesContainer) { + // Find all note items in this container + const noteItems = Array.from(notesContainer.querySelectorAll('.review-note-item')) as HTMLElement[]; + + // Find the position where we should insert + const targetRect = noteItem.getBoundingClientRect(); + const midPoint = targetRect.top + targetRect.height / 2; + + // Remove any existing drag-over indicators + noteItems.forEach(item => item.classList.remove('drag-over', 'drag-over-above', 'drag-over-below')); + + if (e.clientY < midPoint) { + noteItem.classList.add('drag-over', 'drag-over-above'); + } else { + noteItem.classList.add('drag-over', 'drag-over-below'); + } + } + } + }); + + // Add drop event handler + container.addEventListener('drop', async (e) => { + e.preventDefault(); + const target = e.target as HTMLElement; + const noteItem = target.closest('.review-note-item') as HTMLElement | null; + const draggedPath = e.dataTransfer?.getData('text/plain'); + + if (noteItem && draggedPath) { + const notesContainer = noteItem.closest('.review-notes-container'); + if (notesContainer) { + // Remove drag-over indicators + const noteItems = Array.from(notesContainer.querySelectorAll('.review-note-item')) as HTMLElement[]; + noteItems.forEach(item => item.classList.remove('drag-over', 'drag-over-above', 'drag-over-below')); + + // Find the target note path + const targetPath = noteItem.dataset.notePath; + + if (targetPath && draggedPath !== targetPath) { + // Get the current custom order + const currentOrder = [...this.plugin.reviewScheduleService.customNoteOrder]; + + // Find the positions of the dragged and target notes + const draggedIndex = currentOrder.indexOf(draggedPath); + const targetIndex = currentOrder.indexOf(targetPath); + + if (draggedIndex !== -1 && targetIndex !== -1) { + // Remove the dragged note from its current position + currentOrder.splice(draggedIndex, 1); + + // Check if we're dropping above or below the target + const targetRect = noteItem.getBoundingClientRect(); + const midPoint = targetRect.top + targetRect.height / 2; + const isAbove = e.clientY < midPoint; + + // Insert the dragged note at the new position + const insertIndex = isAbove ? targetIndex : targetIndex + 1; + currentOrder.splice(insertIndex, 0, draggedPath); + + // Update the custom order + await this.plugin.reviewScheduleService.updateCustomNoteOrder(currentOrder); + await this.plugin.savePluginData(); + + // Refresh the view to show the new order + await this.refreshSidebarView(); + } + } + } + } + }); + + // Add drag-leave event handler to clean up indicators + container.addEventListener('dragleave', (e) => { + const target = e.target as HTMLElement; + const noteItem = target.closest('.review-note-item') as HTMLElement | null; + + if (noteItem) { + noteItem.classList.remove('drag-over', 'drag-over-above', 'drag-over-below'); + } + }); + } /** * Updates the main header statistics display. diff --git a/ui/sidebar/note-item-renderer.ts b/ui/sidebar/note-item-renderer.ts index a086764..8726d02 100644 --- a/ui/sidebar/note-item-renderer.ts +++ b/ui/sidebar/note-item-renderer.ts @@ -37,6 +37,22 @@ export class NoteItemRenderer { noteEl.removeClass("selected"); } + // Recurrence status + const isRecurring = this.plugin.recurringNotesService.isNoteRecurring(note.path); + if (isRecurring) { + noteEl.addClass("recurring-note"); + const recurrenceInfo = this.plugin.recurringNotesService.getRecurrenceInfo(note.path); + if (recurrenceInfo) { + const recurrenceBadge = noteEl.querySelector('.recurrence-badge') || noteEl.createSpan('recurrence-badge'); + recurrenceBadge.setText(`↻ ${recurrenceInfo.interval}d`); + (recurrenceBadge as HTMLElement).title = `This note recurs every ${recurrenceInfo.interval} days`; + } + } else { + noteEl.removeClass("recurring-note"); + const existingBadge = noteEl.querySelector('.recurrence-badge'); + if (existingBadge) existingBadge.remove(); + } + // Title const titleEl = noteEl.querySelector(".review-note-title"); if (titleEl) { @@ -156,6 +172,11 @@ export class NoteItemRenderer { setIcon(removeBtn, "trash-2"); removeBtn.title = "Remove"; + // Add recurrence button + const recurrenceBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-recurrence" }); + setIcon(recurrenceBtn, "repeat"); + recurrenceBtn.title = "Toggle recurrence"; + 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++) { @@ -216,29 +237,53 @@ export class NoteItemRenderer { } }); - removeBtn.addEventListener("click", (e) => { + removeBtn.addEventListener("click", (e) => { + e.stopPropagation(); + const path = noteEl.dataset.notePath; + if (path) { + const file = this.plugin.app.vault.getAbstractFileByPath(path); + 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(); + } + }); + + // Add event handler for recurrence button + recurrenceBtn.addEventListener("click", (e) => { e.stopPropagation(); const path = noteEl.dataset.notePath; if (path) { - const file = this.plugin.app.vault.getAbstractFileByPath(path); - 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(); - } - })(); + void (async () => { + const isCurrentlyRecurring = this.plugin.recurringNotesService.isNoteRecurring(path); + + if (isCurrentlyRecurring) { + // Convert back to regular note + await this.plugin.recurringNotesService.convertToRegularNote(path); + new Notice(`Note is no longer recurring`); + } else { + // Convert to recurring note with default interval + const defaultInterval = this.plugin.settings.defaultRecurrenceInterval; + await this.plugin.recurringNotesService.convertToRecurringNote(path, defaultInterval); + new Notice(`Note is now recurring every ${defaultInterval} days`); } - ).open(); + + await onNoteAction(); + })(); } }); @@ -349,25 +394,64 @@ export class NoteItemRenderer { } } - menu.addItem((item) => item - .setTitle("Remove from review") - .setIcon("trash") - .onClick(() => { - const file = this.plugin.app.vault.getAbstractFileByPath(path); - 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(); - })(); + // Add recurrence context menu items + const isCurrentlyRecurring = this.plugin.recurringNotesService.isNoteRecurring(path); + if (isCurrentlyRecurring) { + menu.addItem((item) => item + .setTitle("Stop recurrence") + .setIcon("x-circle") + .onClick(async () => { + await this.plugin.recurringNotesService.convertToRegularNote(path); + new Notice("Note is no longer recurring"); + await onNoteAction(); + })); + + // Add option to change recurrence interval + menu.addItem((item) => item + .setTitle("Change recurrence interval") + .setIcon("calendar-range") + .onClick(async () => { + const recurrenceInfo = this.plugin.recurringNotesService.getRecurrenceInfo(path); + if (recurrenceInfo) { + // For now, just cycle through common intervals + const currentInterval = recurrenceInfo.interval; + const newInterval = currentInterval === 7 ? 14 : currentInterval === 14 ? 30 : 7; + await this.plugin.recurringNotesService.updateRecurrenceInterval(path, newInterval); + new Notice(`Recurrence interval changed to ${newInterval} days`); + await onNoteAction(); } - ).open(); - })); + })); + } else { + menu.addItem((item) => item + .setTitle("Make recurring") + .setIcon("repeat") + .onClick(async () => { + const defaultInterval = this.plugin.settings.defaultRecurrenceInterval; + await this.plugin.recurringNotesService.convertToRecurringNote(path, defaultInterval); + new Notice(`Note is now recurring every ${defaultInterval} days`); + await onNoteAction(); + })); + } + + menu.addItem((item) => item + .setTitle("Remove from review") + .setIcon("trash") + .onClick(() => { + const file = this.plugin.app.vault.getAbstractFileByPath(path); + 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); });