mirror of
https://github.com/dralkh/spaceforge.git
synced 2026-07-22 06:45:03 +00:00
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.
This commit is contained in:
parent
6c615f9914
commit
7ac79ae05f
8 changed files with 533 additions and 69 deletions
5
main.ts
5
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);
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
195
services/recurring-notes-service.ts
Normal file
195
services/recurring-notes-service.ts
Normal file
|
|
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
const schedule = this.plugin.reviewScheduleService.schedules[path];
|
||||
if (!schedule?.isRecurring) {
|
||||
return;
|
||||
}
|
||||
|
||||
schedule.recurrenceEndDate = newEndDate;
|
||||
await this.plugin.savePluginData();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
// 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<void> {
|
||||
// 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<HTMLElement>('.review-bulk-actions');
|
||||
private updateBulkActionButtonsVisibility(container: HTMLElement): void {
|
||||
const selectedNotesPaths = this.getSelectedNotes();
|
||||
const bulkActionsContainer = container.querySelector<HTMLElement>('.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<HTMLButtonElement>('.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<HTMLButtonElement>('.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.
|
||||
|
|
|
|||
|
|
@ -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<HTMLElement>(".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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue