diff --git a/README.md b/README.md
index 7be3065..3765c0d 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,15 @@ https://github.com/user-attachments/assets/10bb251e-360c-41ae-a128-e90f3f3e2576
* **Comprehensive Review Management:**
* A dedicated sidebar view displays upcoming reviews, estimated completion times, and allows for easy navigation with configurable hotkey support.
* Calendar view for visualizing your review schedule with standardized UTC date calculations.
+* **š
Calendar Events Organization:**
+ * **Full Event Management**: Create, edit, and delete calendar events with comprehensive CRUD operations
+ * **Event Categories**: Color-coded categories (Work, Personal, Study, Meeting, Health, Social, Other) for visual organization
+ * **Recurring Events**: Support for daily, weekly, monthly, and yearly recurrence patterns with end dates
+ * **Visual Event Display**: Events shown as colored tabs in calendar cells with hover interactions
+ * **Upcoming Events List**: Organized list of upcoming events below calendar grid with day-based grouping
+ * **Quick Event Creation**: Hover plus button on calendar days for fast event creation with pre-filled dates
+ * **Event Details Modal**: Comprehensive event creation/editing interface with all event properties
+ * **Real-time Updates**: Calendar refreshes instantly after event operations with proper state management
* **Flexible Note Addition:**
* Add individual notes or entire folders to your review schedule via the right-click context menu.
* Use Obsidian commands to add the current note or its folder.
@@ -82,6 +91,15 @@ npm install && npm run build && node install.js --d /home/user/Documents/vault/.
2. Start, pause, reset, or skip Pomodoro sessions (Work, Short Break, Long Break).
3. The timer and current session type are displayed in the sidebar.
+#### Managing Calendar Events
+1. **Create Events**: Click the plus button in the calendar header or hover over calendar days to reveal the quick-add button
+2. **View Events**: Events appear as colored tabs in calendar cells and in the upcoming events list below the calendar
+3. **Edit Events**: Double-click event tabs in calendar cells to open the edit modal
+4. **Delete Events**: Use the delete button in the edit modal (no confirmation required for quick workflow)
+5. **Event Categories**: Choose from predefined categories with automatic color coding or set custom colors
+6. **Recurring Events**: Set recurrence patterns (daily, weekly, monthly, yearly) with optional end dates
+7. **Event Details**: Add title, description, date, time, location, and category for comprehensive event management
+
## Core Concepts
### Scheduling Algorithms: Optimize Your Learning
@@ -98,6 +116,7 @@ The Spaceforge settings tab allows you to customize:
* **Scheduling Algorithm:** Select and configure FSRS or SM-2 parameters with standardized UTC date calculations.
* **MCQ Generation:** Enable/disable MCQs, select API provider, enter API keys and models, configure scoring behavior.
* **Pomodoro Timer:** Enable/disable, set durations for work/break sessions, sound notifications.
+* **Calendar Events:** Enable/disable calendar events, set default event category, configure event display options.
* **Data Management:** Set a custom path for plugin data, import/export data, manage data integrity.
## Commands & Shortcuts
@@ -109,6 +128,7 @@ Spaceforge adds several commands to the Obsidian command palette:
* `Spaceforge: Add Current Note to Review Schedule`
* `Spaceforge: Add Current Note's Folder to Review Schedule`
* `Spaceforge: Open Review Sidebar`
+* `Spaceforge: Create Calendar Event`
You can assign custom keyboard shortcuts to these commands via Obsidian's hotkey settings. Spaceforge now includes enhanced navigation command execution with configurable hotkey support for improved workflow efficiency.
@@ -141,6 +161,10 @@ This plugin is licensed under the MIT License.
* **Customizable Sidebar Sections:** Allow users to show/hide or reorder sections within the Spaceforge sidebar for a personalized study environment.
* **More Informative Note Items:** Display more details on note items (e.g., icons for MCQ availability, leech status, FSRS/SM-2 type; hover for more stats) for quick insights.
* **Calendar View Enhancements:** More interactive calendar with direct review options, visual density indicators, and hover details for better schedule management.
+* **Event Reminders:** Notifications and alerts for upcoming calendar events with customizable timing.
+* **Calendar Synchronization:** Integration with external calendar services (Google Calendar, Outlook, etc.).
+* **Advanced Event Filtering:** Filter events by category, date range, or search terms for better organization.
+* **Event Templates**: Pre-defined event templates for common activities (meetings, study sessions, etc.).
* **Pomodoro UI Refinements:** Visual timer, session history, and more sound customization options for an improved focus timer experience.
**IV. Integrations & Data Management:**
diff --git a/main.js b/main.js
index 9429f66..0d0bdb1 100644
--- a/main.js
+++ b/main.js
@@ -32,7 +32,7 @@ __export(main_exports, {
default: () => SpaceforgePlugin
});
module.exports = __toCommonJS(main_exports);
-var import_obsidian26 = require("obsidian");
+var import_obsidian28 = require("obsidian");
// utils/event-emitter.ts
var EventEmitter = class {
@@ -4332,7 +4332,7 @@ var ContextMenuHandler = class {
};
// ui/sidebar-view.ts
-var import_obsidian16 = require("obsidian");
+var import_obsidian18 = require("obsidian");
// ui/sidebar/pomodoro-ui-manager.ts
var import_obsidian12 = require("obsidian");
@@ -5882,7 +5882,603 @@ var ListViewRenderer = class {
};
// ui/calendar-view.ts
+var import_obsidian17 = require("obsidian");
+
+// ui/upcoming-events.ts
var import_obsidian15 = require("obsidian");
+var UpcomingEvents = class {
+ /**
+ * Initialize upcoming events component
+ *
+ * @param containerEl Container element
+ * @param plugin Reference to the main plugin
+ */
+ constructor(containerEl, plugin) {
+ /**
+ * Upcoming events data
+ */
+ this.upcomingEvents = [];
+ this.containerEl = containerEl;
+ this.plugin = plugin;
+ }
+ /**
+ * Render upcoming events list
+ */
+ async render() {
+ if (!this.plugin.settings.enableCalendarEvents || !this.plugin.settings.showUpcomingEvents) {
+ this.containerEl.empty();
+ return;
+ }
+ if (!this.plugin.calendarEventService) {
+ this.containerEl.empty();
+ return;
+ }
+ await this.loadUpcomingEvents();
+ this.containerEl.empty();
+ if (this.upcomingEvents.length === 0) {
+ this.renderEmptyState();
+ return;
+ }
+ this.renderEventsList();
+ }
+ /**
+ * Load upcoming events data
+ */
+ async loadUpcomingEvents() {
+ this.upcomingEvents = this.plugin.calendarEventService.getUpcomingEvents(
+ this.plugin.settings.upcomingEventsDays || 7
+ );
+ }
+ /**
+ * Render empty state when no upcoming events
+ */
+ renderEmptyState() {
+ const emptyState = this.containerEl.createDiv("upcoming-events-empty");
+ const emptyIcon = emptyState.createDiv("upcoming-events-empty-icon");
+ (0, import_obsidian15.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");
+ }
+ /**
+ * Render the events list
+ */
+ renderEventsList() {
+ const header = this.containerEl.createDiv("upcoming-events-header");
+ const headerTitle = header.createDiv("upcoming-events-title");
+ headerTitle.setText("Upcoming Events");
+ const headerSubtitle = header.createDiv("upcoming-events-subtitle");
+ const daysText = this.plugin.settings.upcomingEventsDays || 7;
+ headerSubtitle.setText(`Next ${daysText} days`);
+ const eventsContainer = this.containerEl.createDiv("upcoming-events-container");
+ const eventsByDay = this.groupEventsByDay();
+ eventsByDay.forEach((dayEvents, dayKey) => {
+ this.renderDaySection(eventsContainer, dayKey, dayEvents);
+ });
+ }
+ /**
+ * Group upcoming events by day
+ */
+ groupEventsByDay() {
+ const eventsByDay = /* @__PURE__ */ new Map();
+ this.upcomingEvents.forEach((upcomingEvent) => {
+ let dayKey;
+ if (upcomingEvent.isToday) {
+ dayKey = "Today";
+ } else if (upcomingEvent.isTomorrow) {
+ dayKey = "Tomorrow";
+ } else {
+ const eventDate = new Date(upcomingEvent.event.date);
+ dayKey = eventDate.toLocaleDateString(void 0, {
+ weekday: "short",
+ month: "short",
+ day: "numeric"
+ });
+ }
+ if (!eventsByDay.has(dayKey)) {
+ eventsByDay.set(dayKey, []);
+ }
+ eventsByDay.get(dayKey).push(upcomingEvent);
+ });
+ return eventsByDay;
+ }
+ /**
+ * Render a day section with its events
+ */
+ renderDaySection(container, dayKey, dayEvents) {
+ const daySection = container.createDiv("upcoming-events-day-section");
+ 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") {
+ daySection.addClass("tomorrow");
+ }
+ const dayEventsContainer = daySection.createDiv("upcoming-events-day-events");
+ dayEvents.forEach((upcomingEvent) => {
+ this.renderEventItem(dayEventsContainer, upcomingEvent);
+ });
+ }
+ /**
+ * Render a single event item
+ */
+ renderEventItem(container, upcomingEvent) {
+ var _a;
+ const event = upcomingEvent.event;
+ const eventItem = container.createDiv("upcoming-events-event-item");
+ const colorIndicator = eventItem.createDiv("upcoming-events-event-color");
+ const eventColor = ((_a = this.plugin.calendarEventService) == null ? void 0 : _a.getEventColor(event)) || "#95A5A6";
+ colorIndicator.style.backgroundColor = eventColor;
+ const eventContent = eventItem.createDiv("upcoming-events-event-content");
+ 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);
+ } else if (event.isAllDay) {
+ const eventTime = eventHeader.createDiv("upcoming-events-event-time");
+ eventTime.setText("All day");
+ }
+ if (event.description || event.location) {
+ const eventDetails = eventContent.createDiv("upcoming-events-event-details");
+ if (event.location) {
+ const eventLocation = eventDetails.createDiv("upcoming-events-event-location");
+ (0, import_obsidian15.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 ? "..." : ""));
+ }
+ }
+ const eventCategory = eventContent.createDiv("upcoming-events-event-category");
+ eventCategory.setText(event.category);
+ eventItem.addEventListener("click", () => {
+ this.showEventDetails(event);
+ });
+ eventItem.addEventListener("mouseenter", () => {
+ eventItem.addClass("hover");
+ });
+ eventItem.addEventListener("mouseleave", () => {
+ eventItem.removeClass("hover");
+ });
+ }
+ /**
+ * Show event details modal
+ *
+ * @param event Event to show details for
+ */
+ showEventDetails(event) {
+ const eventDate = new Date(event.date);
+ const dateStr = eventDate.toLocaleDateString(void 0, {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric"
+ });
+ let details = `\u{1F4C5} ${event.title}
+\u{1F4C6} ${dateStr}`;
+ if (event.time) {
+ details += `
+\u{1F550} ${event.time}`;
+ }
+ if (event.description) {
+ details += `
+\u{1F4DD} ${event.description}`;
+ }
+ if (event.location) {
+ details += `
+\u{1F4CD} ${event.location}`;
+ }
+ details += `
+\u{1F3F7}\uFE0F ${event.category}`;
+ new import_obsidian15.Notice(details, 8e3);
+ }
+ /**
+ * Refresh the upcoming events display
+ */
+ async refresh() {
+ await this.render();
+ }
+ /**
+ * Clean up the component
+ */
+ destroy() {
+ this.containerEl.empty();
+ }
+};
+
+// ui/event-modal.ts
+var import_obsidian16 = require("obsidian");
+
+// models/calendar-event.ts
+var EventCategory = /* @__PURE__ */ ((EventCategory2) => {
+ EventCategory2["Work"] = "work";
+ EventCategory2["Personal"] = "personal";
+ EventCategory2["Study"] = "study";
+ EventCategory2["Meeting"] = "meeting";
+ EventCategory2["Health"] = "health";
+ EventCategory2["Social"] = "social";
+ EventCategory2["Other"] = "other";
+ return EventCategory2;
+})(EventCategory || {});
+var EventRecurrence = /* @__PURE__ */ ((EventRecurrence2) => {
+ EventRecurrence2["None"] = "none";
+ EventRecurrence2["Daily"] = "daily";
+ EventRecurrence2["Weekly"] = "weekly";
+ EventRecurrence2["Monthly"] = "monthly";
+ EventRecurrence2["Yearly"] = "yearly";
+ return EventRecurrence2;
+})(EventRecurrence || {});
+var EVENT_CATEGORY_COLORS = {
+ ["work" /* Work */]: "#4A90E2",
+ ["personal" /* Personal */]: "#7B68EE",
+ ["study" /* Study */]: "#50C878",
+ ["meeting" /* Meeting */]: "#FF6B6B",
+ ["health" /* Health */]: "#FF9F40",
+ ["social" /* Social */]: "#FF69B4",
+ ["other" /* Other */]: "#95A5A6"
+};
+
+// ui/event-modal.ts
+var EventModal = class extends import_obsidian16.Modal {
+ constructor(app, plugin, event = null, onSave) {
+ super(app);
+ this.plugin = plugin;
+ this.event = event;
+ this.onSave = onSave;
+ }
+ onOpen() {
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k;
+ const { contentEl } = this;
+ contentEl.empty();
+ contentEl.addClass("event-modal");
+ const header = contentEl.createDiv("event-modal-header");
+ const headerIcon = header.createDiv("event-modal-header-icon");
+ (0, import_obsidian16.setIcon)(headerIcon, this.event ? "edit" : "calendar-plus");
+ const headerText = header.createDiv("event-modal-header-text");
+ headerText.createEl("h2", {
+ text: this.event ? "Edit Event" : "New Event",
+ cls: "event-modal-title"
+ });
+ const formContainer = contentEl.createDiv("event-modal-form");
+ 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", {
+ type: "text",
+ cls: "event-modal-input",
+ placeholder: "Event title..."
+ });
+ if ((_a = this.event) == null ? void 0 : _a.title) {
+ this.titleInput.value = this.event.title;
+ }
+ this.titleInput.addEventListener("input", () => this.validateForm());
+ 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", {
+ type: "date",
+ cls: "event-modal-input"
+ });
+ if ((_b = this.event) == null ? void 0 : _b.date) {
+ const date = new Date(this.event.date);
+ this.dateInput.value = date.toISOString().split("T")[0];
+ } else {
+ this.dateInput.value = (/* @__PURE__ */ 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", {
+ type: "time",
+ cls: "event-modal-input"
+ });
+ if ((_c = this.event) == null ? void 0 : _c.time) {
+ this.timeInput.value = this.event.time;
+ }
+ this.timeInput.addEventListener("change", () => this.updateAllDayToggle());
+ const allDayRow = formContainer.createDiv("event-modal-row");
+ const allDayGroup = allDayRow.createDiv("event-modal-field-group");
+ this.isAllDayToggle = allDayGroup.createEl("input", {
+ type: "checkbox",
+ cls: "event-modal-checkbox"
+ });
+ this.isAllDayToggle.checked = (_e = (_d = this.event) == null ? void 0 : _d.isAllDay) != null ? _e : false;
+ this.isAllDayToggle.addEventListener("change", () => this.toggleTimeInput());
+ const allDayLabel = allDayGroup.createEl("label", {
+ text: "All-day event",
+ cls: "event-modal-checkbox-label"
+ });
+ 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", {
+ type: "text",
+ cls: "event-modal-input",
+ placeholder: "Location (optional)..."
+ });
+ if ((_f = this.event) == null ? void 0 : _f.location) {
+ this.locationInput.value = this.event.location;
+ }
+ 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", {
+ cls: "event-modal-select"
+ });
+ Object.values(EventCategory).forEach((category) => {
+ const option = this.categorySelect.createEl("option", {
+ value: category,
+ text: category.charAt(0).toUpperCase() + category.slice(1)
+ });
+ this.categorySelect.appendChild(option);
+ });
+ if ((_g = this.event) == null ? void 0 : _g.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", {
+ type: "color",
+ cls: "event-modal-color-input"
+ });
+ if ((_h = this.event) == null ? void 0 : _h.color) {
+ this.colorInput.value = this.event.color;
+ } else {
+ this.updateColorFromCategory();
+ }
+ 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", {
+ cls: "event-modal-textarea",
+ placeholder: "Event details (optional)..."
+ });
+ this.descriptionInput.rows = 2;
+ if ((_i = this.event) == null ? void 0 : _i.description) {
+ this.descriptionInput.value = this.event.description;
+ }
+ 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", {
+ cls: "event-modal-select"
+ });
+ Object.values(EventRecurrence).forEach((recurrence) => {
+ const option = this.recurrenceSelect.createEl("option", {
+ value: recurrence,
+ text: this.getRecurrenceDisplayText(recurrence)
+ });
+ this.recurrenceSelect.appendChild(option);
+ });
+ if ((_j = this.event) == null ? void 0 : _j.recurrence) {
+ this.recurrenceSelect.value = this.event.recurrence;
+ } else {
+ this.recurrenceSelect.value = "none" /* None */;
+ }
+ this.recurrenceSelect.addEventListener("change", () => this.toggleRecurrenceEndDate());
+ 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", {
+ type: "date",
+ cls: "event-modal-input"
+ });
+ if ((_k = this.event) == null ? void 0 : _k.recurrenceEndDate) {
+ const date = new Date(this.event.recurrenceEndDate);
+ this.recurrenceEndDateInput.value = date.toISOString().split("T")[0];
+ }
+ const buttonContainer = contentEl.createDiv("event-modal-actions");
+ const cancelButton = buttonContainer.createEl("button", {
+ text: "Cancel",
+ cls: "event-modal-btn event-modal-btn-secondary"
+ });
+ cancelButton.addEventListener("click", () => this.close());
+ if (this.event) {
+ const deleteButton = buttonContainer.createEl("button", {
+ text: "Delete",
+ cls: "event-modal-btn event-modal-btn-danger"
+ });
+ deleteButton.addEventListener("click", () => this.deleteEvent());
+ }
+ 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());
+ this.validateForm();
+ this.updateAllDayToggle();
+ this.toggleTimeInput();
+ this.toggleRecurrenceEndDate();
+ this.updateColorPreview();
+ }
+ onClose() {
+ const { contentEl } = this;
+ contentEl.empty();
+ }
+ /**
+ * Validate form and enable/disable save button
+ */
+ validateForm() {
+ const saveButton = this.contentEl.querySelector(".event-modal-btn-primary");
+ const isValid = this.titleInput.value.trim() !== "" && this.dateInput.value !== "";
+ if (saveButton) {
+ saveButton.disabled = !isValid;
+ }
+ }
+ /**
+ * Update color based on selected category
+ */
+ updateColorFromCategory() {
+ const category = this.categorySelect.value;
+ const categoryColor = EVENT_CATEGORY_COLORS[category];
+ this.colorInput.value = categoryColor;
+ this.updateColorPreview();
+ }
+ /**
+ * Update color preview
+ */
+ updateColorPreview() {
+ const colorPreview = this.contentEl.querySelector(".event-modal-color-preview");
+ if (colorPreview) {
+ colorPreview.style.backgroundColor = this.colorInput.value;
+ }
+ }
+ /**
+ * Toggle time input based on all-day setting
+ */
+ toggleTimeInput() {
+ const isAllDay = this.isAllDayToggle.checked;
+ const timeContainer = this.timeInput.closest(".event-modal-input-group");
+ if (timeContainer) {
+ timeContainer.style.display = isAllDay ? "none" : "block";
+ }
+ if (isAllDay) {
+ this.timeInput.value = "";
+ }
+ }
+ /**
+ * Update all-day toggle based on time input
+ */
+ updateAllDayToggle() {
+ const hasTime = this.timeInput.value !== "";
+ if (hasTime && this.isAllDayToggle.checked) {
+ this.isAllDayToggle.checked = false;
+ this.toggleTimeInput();
+ }
+ }
+ /**
+ * Toggle recurrence end date based on recurrence setting
+ */
+ toggleRecurrenceEndDate() {
+ const recurrence = this.recurrenceSelect.value;
+ const endDateContainer = this.recurrenceEndDateInput.closest(".event-modal-recurrence-end-container");
+ if (endDateContainer) {
+ endDateContainer.style.display = recurrence === "none" /* None */ ? "none" : "block";
+ }
+ if (recurrence === "none" /* None */) {
+ this.recurrenceEndDateInput.value = "";
+ }
+ }
+ /**
+ * Get display text for recurrence option
+ */
+ getRecurrenceDisplayText(recurrence) {
+ switch (recurrence) {
+ case "none" /* None */:
+ return "None";
+ case "daily" /* Daily */:
+ return "Daily";
+ case "weekly" /* Weekly */:
+ return "Weekly";
+ case "monthly" /* Monthly */:
+ return "Monthly";
+ case "yearly" /* Yearly */:
+ return "Yearly";
+ default:
+ return recurrence;
+ }
+ }
+ /**
+ * Delete the event
+ */
+ async deleteEvent() {
+ var _a;
+ if (!this.event)
+ return;
+ try {
+ const deleted = (_a = this.plugin.calendarEventService) == null ? void 0 : _a.deleteEvent(this.event.id);
+ if (deleted) {
+ new import_obsidian16.Notice("Event deleted successfully");
+ await this.plugin.savePluginData();
+ this.onSave(this.event);
+ this.close();
+ } else {
+ new import_obsidian16.Notice("Failed to delete event");
+ }
+ } catch (error) {
+ console.error("Error deleting event:", error);
+ new import_obsidian16.Notice("Error deleting event");
+ }
+ }
+ /**
+ * Save the event
+ */
+ async saveEvent() {
+ var _a, _b;
+ try {
+ const title = this.titleInput.value.trim();
+ const description = this.descriptionInput.value.trim();
+ const date = new Date(this.dateInput.value);
+ const time = this.timeInput.value;
+ const location = this.locationInput.value.trim();
+ const category = this.categorySelect.value;
+ const color = this.colorInput.value;
+ const isAllDay = this.isAllDayToggle.checked;
+ const recurrence = this.recurrenceSelect.value;
+ const recurrenceEndDateText = this.recurrenceEndDateInput.value;
+ const recurrenceEndDate = recurrenceEndDateText ? new Date(recurrenceEndDateText).getTime() : void 0;
+ if (!title || !this.dateInput.value) {
+ new import_obsidian16.Notice("Please fill in all required fields");
+ return;
+ }
+ let eventData;
+ if (this.event) {
+ eventData = {
+ ...this.event,
+ title,
+ description: description || void 0,
+ date: date.getTime(),
+ time: time || void 0,
+ location: location || void 0,
+ category,
+ color: color !== EVENT_CATEGORY_COLORS[category] ? color : void 0,
+ isAllDay,
+ recurrence,
+ recurrenceEndDate
+ };
+ const updatedEvent = (_a = this.plugin.calendarEventService) == null ? void 0 : _a.updateEvent(this.event.id, eventData);
+ if (updatedEvent) {
+ this.onSave(updatedEvent);
+ new import_obsidian16.Notice("Event updated successfully");
+ }
+ } else {
+ eventData = {
+ title,
+ description: description || void 0,
+ date: date.getTime(),
+ time: time || void 0,
+ location: location || void 0,
+ category,
+ color: color !== EVENT_CATEGORY_COLORS[category] ? color : void 0,
+ isAllDay,
+ recurrence,
+ recurrenceEndDate
+ };
+ const newEvent = (_b = this.plugin.calendarEventService) == null ? void 0 : _b.createEvent(eventData);
+ if (newEvent) {
+ this.onSave(newEvent);
+ new import_obsidian16.Notice("Event created successfully");
+ }
+ }
+ await this.plugin.savePluginData();
+ this.close();
+ } catch (error) {
+ console.error("Error saving event:", error);
+ new import_obsidian16.Notice("Error saving event");
+ }
+ }
+};
+
+// ui/calendar-view.ts
var CalendarView = class {
/**
* Initialize calendar view
@@ -5895,10 +6491,19 @@ var CalendarView = class {
* Reviews grouped by date
*/
this.reviewsByDate = /* @__PURE__ */ new Map();
+ /**
+ * Events grouped by date
+ */
+ this.eventsByDate = /* @__PURE__ */ new Map();
// Persistent UI elements
this.calendarHeaderEl = null;
this.monthTitleEl = null;
this.calendarGridEl = null;
+ this.upcomingEventsEl = null;
+ this.upcomingEventsComponent = null;
+ // Tooltip elements
+ this.tooltipEl = null;
+ this.tooltipTimeout = null;
this.containerEl = containerEl;
this.plugin = plugin;
this.currentDate = /* @__PURE__ */ new Date();
@@ -5910,12 +6515,16 @@ var CalendarView = class {
this.ensureCalendarBaseStructure();
this.updateCalendarHeader();
await this.loadReviewsData();
+ await this.loadEventsData();
if (this.calendarGridEl) {
this.renderCalendarGridContent(this.calendarGridEl);
}
+ if (this.upcomingEventsComponent) {
+ await this.upcomingEventsComponent.render();
+ }
}
ensureCalendarBaseStructure() {
- var _a, _b;
+ var _a, _b, _c;
if (!this.containerEl)
return;
let calendarContainer = this.containerEl.querySelector(".calendar-container");
@@ -5926,14 +6535,14 @@ var CalendarView = class {
(_a = this.calendarHeaderEl) == null ? void 0 : _a.remove();
this.calendarHeaderEl = calendarContainer.createDiv("calendar-header");
const prevMonthBtn = this.calendarHeaderEl.createDiv("calendar-nav-btn");
- (0, import_obsidian15.setIcon)(prevMonthBtn, "chevron-left");
+ (0, import_obsidian17.setIcon)(prevMonthBtn, "chevron-left");
prevMonthBtn.addEventListener("click", () => {
this.currentDate.setMonth(this.currentDate.getMonth() - 1);
this.render();
});
this.monthTitleEl = this.calendarHeaderEl.createDiv("calendar-month-title");
const nextMonthBtn = this.calendarHeaderEl.createDiv("calendar-nav-btn");
- (0, import_obsidian15.setIcon)(nextMonthBtn, "chevron-right");
+ (0, import_obsidian17.setIcon)(nextMonthBtn, "chevron-right");
nextMonthBtn.addEventListener("click", () => {
this.currentDate.setMonth(this.currentDate.getMonth() + 1);
this.render();
@@ -5944,11 +6553,21 @@ var CalendarView = class {
this.currentDate = /* @__PURE__ */ new Date();
this.render();
});
+ const addEventBtn = this.calendarHeaderEl.createDiv("calendar-add-event-btn");
+ (0, import_obsidian17.setIcon)(addEventBtn, "plus");
+ addEventBtn.addEventListener("click", () => {
+ this.openCreateEventModal();
+ });
}
if (!this.calendarGridEl || !calendarContainer.contains(this.calendarGridEl)) {
(_b = this.calendarGridEl) == null ? void 0 : _b.remove();
this.calendarGridEl = calendarContainer.createDiv("calendar-grid");
}
+ if (!this.upcomingEventsEl || !calendarContainer.contains(this.upcomingEventsEl)) {
+ (_c = this.upcomingEventsEl) == null ? void 0 : _c.remove();
+ this.upcomingEventsEl = calendarContainer.createDiv("upcoming-events-wrapper");
+ this.upcomingEventsComponent = new UpcomingEvents(this.upcomingEventsEl, this.plugin);
+ }
}
/**
* Update calendar header (month title)
@@ -5987,6 +6606,36 @@ var CalendarView = class {
}
}
}
+ /**
+ * Load and organize event data by date
+ */
+ async loadEventsData() {
+ this.eventsByDate = /* @__PURE__ */ new Map();
+ if (!this.plugin.settings.enableCalendarEvents || !this.plugin.calendarEventService) {
+ return;
+ }
+ const { year, month } = this.getCalendarData();
+ const startDate = new Date(year, month, 1);
+ const endDate = new Date(year, month + 1, 0);
+ const eventsInRange = this.plugin.calendarEventService.getEventsInRange(
+ startDate.getTime(),
+ endDate.getTime()
+ );
+ for (const event of eventsInRange) {
+ const eventDayStart = DateUtils.startOfUTCDay(new Date(event.date));
+ const dateKey = eventDayStart.toString();
+ if (!this.eventsByDate.has(dateKey)) {
+ this.eventsByDate.set(dateKey, {
+ timestamp: eventDayStart,
+ events: []
+ });
+ }
+ const dateEvents = this.eventsByDate.get(dateKey);
+ if (dateEvents) {
+ dateEvents.events.push(event);
+ }
+ }
+ }
/**
* Render or update the calendar grid content
*
@@ -6031,6 +6680,7 @@ var CalendarView = class {
dayCell.addClass("today");
}
const dateReviews = this.reviewsByDate.get(dateKey);
+ const dateEvents = this.eventsByDate.get(dateKey);
if (dateReviews && dateReviews.notes.length > 0) {
dayCell.addClass("has-reviews");
const reviewCount = dayCell.createDiv("calendar-review-count");
@@ -6048,7 +6698,7 @@ var CalendarView = class {
await sidebarView.refresh();
} else {
this.plugin.app.workspace.requestSaveLayout();
- new import_obsidian15.Notice("Switched to list view. Sidebar will update.");
+ new import_obsidian17.Notice("Switched to list view. Sidebar will update.");
}
});
if (dateReviews.notes.length > 10)
@@ -6058,6 +6708,42 @@ var CalendarView = class {
else
dayCell.addClass("light-load");
}
+ if (dateEvents && dateEvents.events.length > 0) {
+ dayCell.addClass("has-events");
+ const eventsContainer = dayCell.createDiv("calendar-events-container");
+ const eventsToShow = dateEvents.events.slice(0, 3);
+ eventsToShow.forEach((event) => {
+ var _a;
+ const eventTab = eventsContainer.createDiv("calendar-event-tab");
+ eventTab.setText(event.title.substring(0, 8) + (event.title.length > 8 ? "..." : ""));
+ const eventColor = ((_a = this.plugin.calendarEventService) == null ? void 0 : _a.getEventColor(event)) || "#95A5A6";
+ eventTab.style.backgroundColor = eventColor;
+ eventTab.addEventListener("mouseenter", (e) => {
+ this.showEventTooltip(eventTab, event);
+ });
+ eventTab.addEventListener("mouseleave", (e) => {
+ this.hideEventTooltip();
+ });
+ eventTab.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.showEventDetails(event);
+ });
+ eventTab.addEventListener("dblclick", (e) => {
+ e.stopPropagation();
+ this.openEditEventModal(event);
+ });
+ });
+ if (dateEvents.events.length > 3) {
+ const moreIndicator = eventsContainer.createDiv("calendar-events-more");
+ moreIndicator.setText(`+${dateEvents.events.length - 3}`);
+ }
+ }
+ const addEventBtn = dayCell.createDiv("calendar-day-add-event");
+ (0, import_obsidian17.setIcon)(addEventBtn, "plus");
+ addEventBtn.addEventListener("click", (e) => {
+ e.stopPropagation();
+ this.openCreateEventModalForDate(currentDateObj);
+ });
dayOfMonth++;
} else {
dayCell.addClass("empty");
@@ -6088,10 +6774,191 @@ var CalendarView = class {
const today = /* @__PURE__ */ new Date();
return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day;
}
+ /**
+ * Show event details modal
+ *
+ * @param event Event to show details for
+ */
+ showEventDetails(event) {
+ const eventDate = new Date(event.date);
+ const dateStr = eventDate.toLocaleDateString(void 0, {
+ weekday: "long",
+ year: "numeric",
+ month: "long",
+ day: "numeric"
+ });
+ let details = `\u{1F4C5} ${event.title}
+\u{1F4C6} ${dateStr}`;
+ if (event.time) {
+ details += `
+\u{1F550} ${event.time}`;
+ }
+ if (event.description) {
+ details += `
+\u{1F4DD} ${event.description}`;
+ }
+ if (event.location) {
+ details += `
+\u{1F4CD} ${event.location}`;
+ }
+ details += `
+\u{1F3F7}\uFE0F ${event.category}`;
+ new import_obsidian17.Notice(details, 8e3);
+ }
+ /**
+ * Open create event modal
+ */
+ openCreateEventModal() {
+ const modal = new EventModal(
+ this.plugin.app,
+ this.plugin,
+ null,
+ async (savedEvent) => {
+ await this.render();
+ }
+ );
+ modal.open();
+ }
+ /**
+ * Open create event modal for a specific date
+ *
+ * @param date Date to pre-fill in the modal
+ */
+ openCreateEventModalForDate(date) {
+ const defaultEvent = {
+ title: "",
+ date: date.getTime(),
+ isAllDay: true,
+ category: this.plugin.settings.defaultEventCategory || "personal",
+ recurrence: "none"
+ };
+ const modal = new EventModal(
+ this.plugin.app,
+ this.plugin,
+ null,
+ async (savedEvent) => {
+ await this.render();
+ }
+ );
+ modal.open();
+ setTimeout(() => {
+ const dateInput = modal.contentEl.querySelector('input[type="date"]');
+ if (dateInput) {
+ dateInput.value = date.toISOString().split("T")[0];
+ }
+ }, 100);
+ }
+ /**
+ * Open edit event modal
+ *
+ * @param event Event to edit
+ */
+ openEditEventModal(event) {
+ const modal = new EventModal(
+ this.plugin.app,
+ this.plugin,
+ event,
+ async (savedEvent) => {
+ await this.render();
+ }
+ );
+ modal.open();
+ }
+ /**
+ * Clean up resources when the calendar view is destroyed
+ */
+ destroy() {
+ this.hideEventTooltip();
+ this.calendarHeaderEl = null;
+ this.monthTitleEl = null;
+ this.calendarGridEl = null;
+ this.upcomingEventsEl = null;
+ this.upcomingEventsComponent = null;
+ }
+ /**
+ * Show event tooltip
+ *
+ * @param targetEl Element to show tooltip for
+ * @param event Event to show details for
+ */
+ showEventTooltip(targetEl, event) {
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ }
+ this.tooltipTimeout = setTimeout(() => {
+ this.createEventTooltip(targetEl, event);
+ }, 300);
+ }
+ /**
+ * Hide event tooltip
+ */
+ hideEventTooltip() {
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ this.tooltipTimeout = null;
+ }
+ if (this.tooltipEl) {
+ this.tooltipEl.remove();
+ this.tooltipEl = null;
+ }
+ }
+ /**
+ * Create and show the tooltip element
+ *
+ * @param targetEl Element to position tooltip relative to
+ * @param event Event to show details for
+ */
+ createEventTooltip(targetEl, event) {
+ this.hideEventTooltip();
+ this.tooltipEl = document.createElement("div");
+ this.tooltipEl.className = "calendar-event-tooltip";
+ const eventDate = new Date(event.date);
+ const dateStr = eventDate.toLocaleDateString(void 0, {
+ weekday: "short",
+ month: "short",
+ day: "numeric"
+ });
+ let tooltipContent = `
${event.title}
`;
+ tooltipContent += `\u{1F4C5} ${dateStr}
`;
+ if (event.time) {
+ tooltipContent += `\u{1F550} ${event.time}
`;
+ }
+ if (event.description) {
+ tooltipContent += `\u{1F4DD} ${event.description}
`;
+ }
+ if (event.location) {
+ tooltipContent += `\u{1F4CD} ${event.location}
`;
+ }
+ tooltipContent += `\u{1F3F7}\uFE0F ${event.category}
`;
+ this.tooltipEl.innerHTML = tooltipContent;
+ const rect = targetEl.getBoundingClientRect();
+ const tooltipRect = this.tooltipEl.getBoundingClientRect();
+ document.body.appendChild(this.tooltipEl);
+ let left = rect.left + rect.width / 2 - this.tooltipEl.offsetWidth / 2;
+ let top = rect.bottom + 8;
+ if (left < 8)
+ left = 8;
+ if (left + this.tooltipEl.offsetWidth > window.innerWidth - 8) {
+ left = window.innerWidth - this.tooltipEl.offsetWidth - 8;
+ }
+ 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.opacity = "0";
+ this.tooltipEl.style.transform = "translateY(-4px)";
+ requestAnimationFrame(() => {
+ if (this.tooltipEl) {
+ this.tooltipEl.style.opacity = "1";
+ this.tooltipEl.style.transform = "translateY(0)";
+ }
+ });
+ }
};
// ui/sidebar-view.ts
-var ReviewSidebarView = class extends import_obsidian16.ItemView {
+var ReviewSidebarView = class extends import_obsidian18.ItemView {
constructor(leaf, plugin) {
super(leaf);
this.activeListBaseDate = null;
@@ -6339,7 +7206,7 @@ var ReviewSidebarView = class extends import_obsidian16.ItemView {
const path2 = dateNotes[index - 1].path;
await this.plugin.reviewController.swapNotes(path1, path2);
await this.refresh();
- new import_obsidian16.Notice(`Moved note up`);
+ new import_obsidian18.Notice(`Moved note up`);
}
/**
* Move a note down in the list
@@ -6363,7 +7230,7 @@ var ReviewSidebarView = class extends import_obsidian16.ItemView {
const path2 = dateNotes[index + 1].path;
await this.plugin.reviewController.swapNotes(path1, path2);
await this.refresh();
- new import_obsidian16.Notice(`Moved note down`);
+ new import_obsidian18.Notice(`Moved note down`);
}
/**
* Group notes by their folder
@@ -6412,7 +7279,7 @@ var ReviewSidebarView = class extends import_obsidian16.ItemView {
};
// ui/settings-tab.ts
-var import_obsidian17 = require("obsidian");
+var import_obsidian19 = require("obsidian");
// models/settings.ts
var DEFAULT_SETTINGS = {
@@ -6512,7 +7379,12 @@ var DEFAULT_SETTINGS = {
modifiers: [],
key: null
},
- navigationCommandDelay: 500
+ navigationCommandDelay: 500,
+ // Calendar Events Defaults
+ enableCalendarEvents: true,
+ showUpcomingEvents: true,
+ upcomingEventsDays: 7,
+ defaultEventCategory: "personal"
};
// models/plugin-data.ts
@@ -6541,7 +7413,9 @@ var DEFAULT_PLUGIN_STATE_DATA = {
// User Override Time Settings Defaults
pomodoroUserOverrideHours: 0,
pomodoroUserOverrideMinutes: 0,
- pomodoroUserAddToEstimation: false
+ pomodoroUserAddToEstimation: false,
+ // Calendar Events Defaults
+ calendarEvents: {}
};
var DEFAULT_APP_DATA = {
settings: DEFAULT_SETTINGS,
@@ -6549,7 +7423,7 @@ var DEFAULT_APP_DATA = {
};
// ui/settings-tab.ts
-var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
+var SpaceforgeSettingTab = class extends import_obsidian19.PluginSettingTab {
/**
* Initialize settings tab
*
@@ -6571,7 +7445,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
const header = sectionContainer.createEl("div", { cls: "sf-settings-section-header" });
if (iconName) {
const iconEl = header.createEl("span", { cls: "sf-settings-icon" });
- (0, import_obsidian17.setIcon)(iconEl, iconName);
+ (0, import_obsidian19.setIcon)(iconEl, iconName);
}
header.createEl("h3", { text: title });
const collapseIndicator = header.createEl("span", {
@@ -6595,7 +7469,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
const actionsContainer = containerEl.createEl("div", { cls: "sf-settings-actions" });
const exportBtn = actionsContainer.createEl("button", { text: "Export all data" });
exportBtn.addEventListener("click", () => {
- var _a, _b, _c, _d, _e, _f, _g, _h;
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i;
const pluginStateToExport = {
schedules: this.plugin.reviewScheduleService.schedules,
history: this.plugin.reviewHistoryService.history,
@@ -6617,7 +7491,8 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
pomodoroUserHasModifiedSettings: (_e = this.plugin.pluginState.pomodoroUserHasModifiedSettings) != null ? _e : false,
pomodoroUserOverrideHours: (_f = this.plugin.pluginState.pomodoroUserOverrideHours) != null ? _f : 0,
pomodoroUserOverrideMinutes: (_g = this.plugin.pluginState.pomodoroUserOverrideMinutes) != null ? _g : 0,
- pomodoroUserAddToEstimation: (_h = this.plugin.pluginState.pomodoroUserAddToEstimation) != null ? _h : false
+ pomodoroUserAddToEstimation: (_h = this.plugin.pluginState.pomodoroUserAddToEstimation) != null ? _h : false,
+ calendarEvents: (_i = this.plugin.pluginState.calendarEvents) != null ? _i : {}
};
const dataToExport = {
settings: this.plugin.settings,
@@ -6631,7 +7506,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
- new import_obsidian17.Notice("All plugin data exported successfully");
+ new import_obsidian19.Notice("All plugin data exported successfully");
});
const importBtn = actionsContainer.createEl("button", { text: "Import all data" });
importBtn.addEventListener("click", () => {
@@ -6662,9 +7537,9 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.initializeMCQComponents();
await this.plugin.savePluginData();
this.display();
- new import_obsidian17.Notice("All plugin data imported successfully. Plugin may require a reload for all changes to take effect.");
+ new import_obsidian19.Notice("All plugin data imported successfully. Plugin may require a reload for all changes to take effect.");
} catch (error) {
- new import_obsidian17.Notice(`Failed to import data: ${error.message}`);
+ new import_obsidian19.Notice(`Failed to import data: ${error.message}`);
}
}
};
@@ -6689,7 +7564,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.initializeMCQComponents();
await this.plugin.savePluginData();
this.display();
- new import_obsidian17.Notice("All plugin data reset to defaults.");
+ new import_obsidian19.Notice("All plugin data reset to defaults.");
}
});
const clearScheduleBtn = actionsContainer.createEl("button", {
@@ -6717,19 +7592,19 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.events.emit("sidebar-update");
}
await this.plugin.savePluginData();
- new import_obsidian17.Notice("All schedule data cleared successfully.");
+ new import_obsidian19.Notice("All schedule data cleared successfully.");
this.display();
(_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
} catch (error) {
- new import_obsidian17.Notice("Failed to clear schedule data. Check console for details.");
+ new import_obsidian19.Notice("Failed to clear schedule data. Check console for details.");
}
}
});
return actionsContainer;
};
const spacedRepSection = createCollapsible("Spaced repetition", "calendar-clock", true);
- new import_obsidian17.Setting(spacedRepSection).setName("Algorithm configuration").setHeading();
- const algoSelectionSetting = new import_obsidian17.Setting(spacedRepSection).setName("Default scheduling algorithm").setDesc("Choose the default algorithm for newly created notes.").addDropdown((dropdown) => dropdown.addOption("sm2", "SM-2").addOption("fsrs", "FSRS").setValue(this.plugin.settings.defaultSchedulingAlgorithm).onChange(async (value) => {
+ new import_obsidian19.Setting(spacedRepSection).setName("Algorithm configuration").setHeading();
+ const algoSelectionSetting = new import_obsidian19.Setting(spacedRepSection).setName("Default scheduling algorithm").setDesc("Choose the default algorithm for newly created notes.").addDropdown((dropdown) => dropdown.addOption("sm2", "SM-2").addOption("fsrs", "FSRS").setValue(this.plugin.settings.defaultSchedulingAlgorithm).onChange(async (value) => {
this.plugin.settings.defaultSchedulingAlgorithm = value;
await this.plugin.savePluginData();
this.display();
@@ -6740,35 +7615,35 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
const sm2Summary = sm2ParamsContainer.createEl("summary");
sm2Summary.setText("SM-2 parameters");
sm2ParamsContainer.open = false;
- new import_obsidian17.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).").addSlider((slider) => slider.setLimits(130, 500, 10).setValue(this.plugin.settings.baseEase).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.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).").addSlider((slider) => slider.setLimits(130, 500, 10).setValue(this.plugin.settings.baseEase).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.baseEase = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.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.").addToggle((toggle) => toggle.setValue(this.plugin.settings.useInitialSchedule).onChange(async (value) => {
+ new import_obsidian19.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.").addToggle((toggle) => toggle.setValue(this.plugin.settings.useInitialSchedule).onChange(async (value) => {
this.plugin.settings.useInitialSchedule = value;
await this.plugin.savePluginData();
this.display();
}));
if (this.plugin.settings.useInitialSchedule) {
- new import_obsidian17.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.").addText((text) => text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(", ")).onChange(async (value) => {
+ new import_obsidian19.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.").addText((text) => text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(", ")).onChange(async (value) => {
const intervals = value.split(",").map((s) => parseInt(s.trim())).filter((n) => !isNaN(n) && n >= 0);
if (intervals.length > 0 && intervals[0] === 0) {
this.plugin.settings.initialScheduleCustomIntervals = intervals;
await this.plugin.savePluginData();
} else {
- new import_obsidian17.Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5e3);
+ new import_obsidian19.Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5e3);
text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(", "));
}
}));
}
- new import_obsidian17.Setting(sm2ParamsContainer).setName("SM-2: Maximum interval (days)").setDesc("Longest possible interval between SM-2 reviews.").addText((text) => text.setValue(this.plugin.settings.maximumInterval.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(sm2ParamsContainer).setName("SM-2: Maximum interval (days)").setDesc("Longest possible interval between SM-2 reviews.").addText((text) => text.setValue(this.plugin.settings.maximumInterval.toString()).onChange(async (value) => {
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
this.plugin.settings.maximumInterval = numValue;
await this.plugin.savePluginData();
}
}));
- new import_obsidian17.Setting(sm2ParamsContainer).setName("SM-2: Load balancing").setDesc("Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.").addToggle((toggle) => toggle.setValue(this.plugin.settings.loadBalance).onChange(async (value) => {
+ new import_obsidian19.Setting(sm2ParamsContainer).setName("SM-2: Load balancing").setDesc("Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.").addToggle((toggle) => toggle.setValue(this.plugin.settings.loadBalance).onChange(async (value) => {
this.plugin.settings.loadBalance = value;
await this.plugin.savePluginData();
}));
@@ -6776,7 +7651,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
const fsrsSummary = fsrsParamsContainer.createEl("summary");
fsrsSummary.setText("FSRS parameters");
fsrsParamsContainer.open = false;
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Request retention").setDesc("Desired recall probability (0.7-0.99, default: 0.9). Higher values mean more frequent reviews.").addText((text) => {
+ new import_obsidian19.Setting(fsrsParamsContainer).setName("Request retention").setDesc("Desired recall probability (0.7-0.99, default: 0.9). Higher values mean more frequent reviews.").addText((text) => {
var _a, _b, _c;
return text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.request_retention) == null ? void 0 : _b.toString()) != null ? _c : DEFAULT_SETTINGS.fsrsParameters.request_retention.toString()).onChange(async (value) => {
const numValue = parseFloat(value);
@@ -6785,11 +7660,11 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
- new import_obsidian17.Notice("FSRS Request Retention must be between 0.7 and 0.99.");
+ new import_obsidian19.Notice("FSRS Request Retention must be between 0.7 and 0.99.");
}
});
});
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Maximum interval (days)").setDesc("Longest possible interval FSRS will schedule.").addText((text) => {
+ new import_obsidian19.Setting(fsrsParamsContainer).setName("Maximum interval (days)").setDesc("Longest possible interval FSRS will schedule.").addText((text) => {
var _a, _b, _c;
return text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.maximum_interval) == null ? void 0 : _b.toString()) != null ? _c : DEFAULT_SETTINGS.fsrsParameters.maximum_interval.toString()).onChange(async (value) => {
const numValue = parseInt(value);
@@ -6798,11 +7673,11 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
- new import_obsidian17.Notice("FSRS Maximum Interval must be a positive number.");
+ new import_obsidian19.Notice("FSRS Maximum Interval must be a positive number.");
}
});
});
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Learning steps (minutes)").setDesc("Comma-separated initial learning intervals in minutes (e.g., 1,10 for 1m, 10m).").addText((text) => {
+ new import_obsidian19.Setting(fsrsParamsContainer).setName("Learning steps (minutes)").setDesc("Comma-separated initial learning intervals in minutes (e.g., 1,10 for 1m, 10m).").addText((text) => {
var _a, _b, _c;
return text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.learning_steps) == null ? void 0 : _b.join(",")) != null ? _c : DEFAULT_SETTINGS.fsrsParameters.learning_steps.join(",")).onChange(async (value) => {
const steps = value.split(",").map((s) => parseInt(s.trim())).filter((n) => !isNaN(n) && n > 0);
@@ -6815,11 +7690,11 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
- new import_obsidian17.Notice("FSRS Learning Steps must be valid comma-separated numbers > 0, or empty.");
+ new import_obsidian19.Notice("FSRS Learning Steps must be valid comma-separated numbers > 0, or empty.");
}
});
});
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Enable fuzz").setDesc("Add slight randomness to FSRS intervals (recommended).").addToggle((toggle) => {
+ new import_obsidian19.Setting(fsrsParamsContainer).setName("Enable fuzz").setDesc("Add slight randomness to FSRS intervals (recommended).").addToggle((toggle) => {
var _a, _b;
return toggle.setValue((_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.enable_fuzz) != null ? _b : DEFAULT_SETTINGS.fsrsParameters.enable_fuzz).onChange(async (value) => {
this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, enable_fuzz: value };
@@ -6827,7 +7702,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
});
});
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Enable short term scheduling").setDesc("Use FSRS short-term memory model (affects initial learning steps).").addToggle((toggle) => {
+ new import_obsidian19.Setting(fsrsParamsContainer).setName("Enable short term scheduling").setDesc("Use FSRS short-term memory model (affects initial learning steps).").addToggle((toggle) => {
var _a, _b;
return toggle.setValue((_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.enable_short_term) != null ? _b : DEFAULT_SETTINGS.fsrsParameters.enable_short_term).onChange(async (value) => {
this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, enable_short_term: value };
@@ -6835,7 +7710,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
});
});
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Weights (w)").setDesc("FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.").addTextArea((text) => {
+ new import_obsidian19.Setting(fsrsParamsContainer).setName("Weights (w)").setDesc("FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.").addTextArea((text) => {
var _a, _b, _c;
text.inputEl.rows = 3;
text.inputEl.addClass("sf-full-width-textarea");
@@ -6846,7 +7721,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
await this.plugin.savePluginData();
this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
} else {
- new import_obsidian17.Notice("FSRS Weights must be a comma-separated list of 17 valid numbers.");
+ new import_obsidian19.Notice("FSRS Weights must be a comma-separated list of 17 valid numbers.");
}
});
});
@@ -6854,51 +7729,51 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
const conversionSummary = conversionContainer.createEl("summary");
conversionSummary.setText("Card conversion utilities");
conversionContainer.open = false;
- new import_obsidian17.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.").addButton((button) => button.setButtonText("Convert SM-2 to FSRS").setCta().onClick(async () => {
+ new import_obsidian19.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.").addButton((button) => button.setButtonText("Convert SM-2 to FSRS").setCta().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 import_obsidian17.Notice("Converting SM-2 cards to FSRS... This may take a moment.");
+ new import_obsidian19.Notice("Converting SM-2 cards to FSRS... This may take a moment.");
await this.plugin.reviewScheduleService.convertAllSm2ToFsrs();
await this.plugin.savePluginData();
- new import_obsidian17.Notice("All SM-2 cards have been converted to FSRS.");
+ new import_obsidian19.Notice("All SM-2 cards have been converted to FSRS.");
this.display();
}
}));
- new import_obsidian17.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.").addButton((button) => button.setButtonText("Convert FSRS to SM-2").setCta().onClick(async () => {
+ new import_obsidian19.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.").addButton((button) => button.setButtonText("Convert FSRS to SM-2").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 import_obsidian17.Notice("Converting FSRS cards to SM-2... This may take a moment.");
+ new import_obsidian19.Notice("Converting FSRS cards to SM-2... This may take a moment.");
await this.plugin.reviewScheduleService.convertAllFsrsToSm2();
await this.plugin.savePluginData();
- new import_obsidian17.Notice("All FSRS cards have been converted to SM-2.");
+ new import_obsidian19.Notice("All FSRS cards have been converted to SM-2.");
this.display();
}
}));
const interfaceSection = createCollapsible("Interface & behavior", "settings", false);
- new import_obsidian17.Setting(interfaceSection).setName("Display").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(interfaceSection).setName("Default view type").setDesc("Choose between list or calendar for the review sidebar").addDropdown((dropdown) => dropdown.addOption("list", "List view").addOption("calendar", "Calendar view").setValue(this.plugin.settings.sidebarViewType).onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Display").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(interfaceSection).setName("Default view type").setDesc("Choose between list or calendar for the review sidebar").addDropdown((dropdown) => dropdown.addOption("list", "List view").addOption("calendar", "Calendar view").setValue(this.plugin.settings.sidebarViewType).onChange(async (value) => {
var _a;
this.plugin.settings.sidebarViewType = value;
await this.plugin.savePluginData();
(_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
}));
- new import_obsidian17.Setting(interfaceSection).setName("Show navigation notifications").setDesc("Display notifications when moving between notes").addToggle((toggle) => toggle.setValue(this.plugin.settings.showNavigationNotifications).onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Show navigation notifications").setDesc("Display notifications when moving between notes").addToggle((toggle) => toggle.setValue(this.plugin.settings.showNavigationNotifications).onChange(async (value) => {
this.plugin.settings.showNavigationNotifications = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(interfaceSection).setName("Review behavior").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(interfaceSection).setName("Include subfolders").setDesc("When adding a folder to review, include all notes in subfolders").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeSubfolders).onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Review behavior").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(interfaceSection).setName("Include subfolders").setDesc("When adding a folder to review, include all notes in subfolders").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeSubfolders).onChange(async (value) => {
this.plugin.settings.includeSubfolders = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(interfaceSection).setName("Notification time").setDesc("Minutes before due time to notify about upcoming reviews (0 to disable)").addText((text) => text.setValue(this.plugin.settings.notifyBeforeDue.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Notification time").setDesc("Minutes before due time to notify about upcoming reviews (0 to disable)").addText((text) => text.setValue(this.plugin.settings.notifyBeforeDue.toString()).onChange(async (value) => {
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue >= 0) {
this.plugin.settings.notifyBeforeDue = numValue;
await this.plugin.savePluginData();
}
}));
- new import_obsidian17.Setting(interfaceSection).setName("Reading speed (WPM)").setDesc("Words per minute for estimating review time").addSlider((slider) => slider.setLimits(100, 500, 10).setValue(this.plugin.settings.readingSpeed).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Reading speed (WPM)").setDesc("Words per minute for estimating review time").addSlider((slider) => slider.setLimits(100, 500, 10).setValue(this.plugin.settings.readingSpeed).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.readingSpeed = value;
await this.plugin.savePluginData();
}));
@@ -6906,14 +7781,14 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
cls: "sf-setting-explain",
text: "Average adults read 200-250 WPM for regular content, 100-150 WPM for technical content"
});
- new import_obsidian17.Setting(interfaceSection).setName("Navigation command").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(interfaceSection).setName("Enable navigation command").setDesc("Execute a command with a slight delay after navigating to the next or previous note.").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableNavigationCommands).onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Navigation command").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(interfaceSection).setName("Enable navigation command").setDesc("Execute a command with a slight delay after navigating to the next or previous note.").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableNavigationCommands).onChange(async (value) => {
this.plugin.settings.enableNavigationCommands = value;
await this.plugin.savePluginData();
this.display();
}));
if (this.plugin.settings.enableNavigationCommands) {
- new import_obsidian17.Setting(interfaceSection).setName("Command to execute").setDesc("Click the button and press the desired hotkey.").addButton((button) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Command to execute").setDesc("Click the button and press the desired hotkey.").addButton((button) => {
const command = this.plugin.settings.navigationCommand;
const hotkeyText = command.key ? [...command.modifiers, command.key].join(" + ") : "Click to set";
button.setButtonText(hotkeyText).onClick(() => {
@@ -6946,13 +7821,13 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
document.addEventListener("keydown", keydownHandler, { capture: true });
});
});
- new import_obsidian17.Setting(interfaceSection).setName("Command execution delay (ms)").setDesc("How long to wait before executing the command after navigation.").addSlider((slider) => slider.setLimits(0, 2e3, 100).setValue(this.plugin.settings.navigationCommandDelay).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.Setting(interfaceSection).setName("Command execution delay (ms)").setDesc("How long to wait before executing the command after navigation.").addSlider((slider) => slider.setLimits(0, 2e3, 100).setValue(this.plugin.settings.navigationCommandDelay).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.navigationCommandDelay = value;
await this.plugin.savePluginData();
}));
}
const mcqSection = createCollapsible("Multiple choice questions", "newspaper", false);
- new import_obsidian17.Setting(mcqSection).setName("Enable MCQ feature").setDesc("Use AI-generated multiple-choice questions to test your knowledge").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableMCQ).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Enable MCQ feature").setDesc("Use AI-generated multiple-choice questions to test your knowledge").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableMCQ).onChange(async (value) => {
this.plugin.settings.enableMCQ = value;
await this.plugin.savePluginData();
this.plugin.initializeMCQComponents();
@@ -6968,8 +7843,8 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.display();
}));
if (this.plugin.settings.enableMCQ) {
- new import_obsidian17.Setting(mcqSection).setName("API configuration").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(mcqSection).setName("API provider").setDesc("Select the API provider for generating MCQs.").addDropdown((dropdown) => {
+ new import_obsidian19.Setting(mcqSection).setName("API configuration").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(mcqSection).setName("API provider").setDesc("Select the API provider for generating MCQs.").addDropdown((dropdown) => {
dropdown.addOption("openrouter" /* OpenRouter */, "OpenRouter").addOption("openai" /* OpenAI */, "OpenAI").addOption("ollama" /* Ollama */, "Ollama").addOption("gemini" /* Gemini */, "Gemini").addOption("claude" /* Claude */, "Claude").addOption("together" /* Together */, "Together AI").setValue(this.plugin.settings.mcqApiProvider).onChange(async (value) => {
this.plugin.settings.mcqApiProvider = value;
await this.plugin.savePluginData();
@@ -6979,116 +7854,116 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
});
const provider = this.plugin.settings.mcqApiProvider;
if (provider === "openrouter" /* OpenRouter */) {
- new import_obsidian17.Setting(mcqSection).setName("OpenRouter configuration").setHeading().setClass("sf-settings-subsection-provider-header");
+ new import_obsidian19.Setting(mcqSection).setName("OpenRouter configuration").setHeading().setClass("sf-settings-subsection-provider-header");
const apiKeyContainer = mcqSection.createEl("div", { cls: "sf-setting-highlight" });
- new import_obsidian17.Setting(apiKeyContainer).setName("OpenRouter API key").setDesc("Required for generating MCQs via OpenRouter.").addText((text) => text.setPlaceholder("Enter your OpenRouter API key").setValue(this.plugin.settings.openRouterApiKey).onChange(async (value) => {
+ new import_obsidian19.Setting(apiKeyContainer).setName("OpenRouter API key").setDesc("Required for generating MCQs via OpenRouter.").addText((text) => text.setPlaceholder("Enter your OpenRouter API key").setValue(this.plugin.settings.openRouterApiKey).onChange(async (value) => {
this.plugin.settings.openRouterApiKey = value;
await this.plugin.savePluginData();
}));
apiKeyContainer.createEl("div").setText("Get your API key at https://openrouter.ai/keys");
- new import_obsidian17.Setting(mcqSection).setName("OpenRouter model").setDesc("Model identifier from OpenRouter (e.g., openai/gpt-4.1-mini)").addText((text) => text.setPlaceholder("Enter OpenRouter model identifier").setValue(this.plugin.settings.openRouterModel).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("OpenRouter model").setDesc("Model identifier from OpenRouter (e.g., openai/gpt-4.1-mini)").addText((text) => text.setPlaceholder("Enter OpenRouter model identifier").setValue(this.plugin.settings.openRouterModel).onChange(async (value) => {
this.plugin.settings.openRouterModel = value;
await this.plugin.savePluginData();
}));
} else if (provider === "openai" /* OpenAI */) {
- new import_obsidian17.Setting(mcqSection).setName("OpenAI configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("OpenAI API key").setDesc("Your OpenAI API key.").addText((text) => text.setPlaceholder("Enter your OpenAI API key (sk-...)").setValue(this.plugin.settings.openaiApiKey).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("OpenAI configuration").setHeading().setClass("sf-settings-subsection-provider-header");
+ new import_obsidian19.Setting(mcqSection).setName("OpenAI API key").setDesc("Your OpenAI API key.").addText((text) => text.setPlaceholder("Enter your OpenAI API key (sk-...)").setValue(this.plugin.settings.openaiApiKey).onChange(async (value) => {
this.plugin.settings.openaiApiKey = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("OpenAI model").setDesc("Model name (e.g., gpt-3.5-turbo, gpt-4)").addText((text) => text.setPlaceholder("Enter OpenAI model name").setValue(this.plugin.settings.openaiModel).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("OpenAI model").setDesc("Model name (e.g., gpt-3.5-turbo, gpt-4)").addText((text) => text.setPlaceholder("Enter OpenAI model name").setValue(this.plugin.settings.openaiModel).onChange(async (value) => {
this.plugin.settings.openaiModel = value;
await this.plugin.savePluginData();
}));
} else if (provider === "ollama" /* Ollama */) {
- new import_obsidian17.Setting(mcqSection).setName("Ollama configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Ollama API URL").setDesc("URL of your running Ollama instance (e.g., http://localhost:11434)").addText((text) => text.setPlaceholder("http://localhost:11434").setValue(this.plugin.settings.ollamaApiUrl).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Ollama configuration").setHeading().setClass("sf-settings-subsection-provider-header");
+ new import_obsidian19.Setting(mcqSection).setName("Ollama API URL").setDesc("URL of your running Ollama instance (e.g., http://localhost:11434)").addText((text) => text.setPlaceholder("http://localhost:11434").setValue(this.plugin.settings.ollamaApiUrl).onChange(async (value) => {
this.plugin.settings.ollamaApiUrl = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("Ollama model").setDesc("Name of the Ollama model to use (e.g., llama3, mistral)").addText((text) => text.setPlaceholder("Enter Ollama model name").setValue(this.plugin.settings.ollamaModel).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Ollama model").setDesc("Name of the Ollama model to use (e.g., llama3, mistral)").addText((text) => text.setPlaceholder("Enter Ollama model name").setValue(this.plugin.settings.ollamaModel).onChange(async (value) => {
this.plugin.settings.ollamaModel = value;
await this.plugin.savePluginData();
}));
} else if (provider === "gemini" /* Gemini */) {
- new import_obsidian17.Setting(mcqSection).setName("Gemini configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Gemini API key").setDesc("Your Google AI Gemini API key.").addText((text) => text.setPlaceholder("Enter your Gemini API key").setValue(this.plugin.settings.geminiApiKey).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Gemini configuration").setHeading().setClass("sf-settings-subsection-provider-header");
+ new import_obsidian19.Setting(mcqSection).setName("Gemini API key").setDesc("Your Google AI Gemini API key.").addText((text) => text.setPlaceholder("Enter your Gemini API key").setValue(this.plugin.settings.geminiApiKey).onChange(async (value) => {
this.plugin.settings.geminiApiKey = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("Gemini model").setDesc("Model name (e.g., gemini-pro)").addText((text) => text.setPlaceholder("Enter Gemini model name").setValue(this.plugin.settings.geminiModel).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Gemini model").setDesc("Model name (e.g., gemini-pro)").addText((text) => text.setPlaceholder("Enter Gemini model name").setValue(this.plugin.settings.geminiModel).onChange(async (value) => {
this.plugin.settings.geminiModel = value;
await this.plugin.savePluginData();
}));
} else if (provider === "claude" /* Claude */) {
- new import_obsidian17.Setting(mcqSection).setName("Claude configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Claude API key").setDesc("Your Anthropic Claude API key.").addText((text) => text.setPlaceholder("Enter your Claude API key").setValue(this.plugin.settings.claudeApiKey).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Claude configuration").setHeading().setClass("sf-settings-subsection-provider-header");
+ new import_obsidian19.Setting(mcqSection).setName("Claude API key").setDesc("Your Anthropic Claude API key.").addText((text) => text.setPlaceholder("Enter your Claude API key").setValue(this.plugin.settings.claudeApiKey).onChange(async (value) => {
this.plugin.settings.claudeApiKey = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("Claude model").setDesc("Model name (e.g., claude-3-opus-20240229, claude-3-sonnet-20240229)").addText((text) => text.setPlaceholder("Enter Claude model name").setValue(this.plugin.settings.claudeModel).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Claude model").setDesc("Model name (e.g., claude-3-opus-20240229, claude-3-sonnet-20240229)").addText((text) => text.setPlaceholder("Enter Claude model name").setValue(this.plugin.settings.claudeModel).onChange(async (value) => {
this.plugin.settings.claudeModel = value;
await this.plugin.savePluginData();
}));
} else if (provider === "together" /* Together */) {
- new import_obsidian17.Setting(mcqSection).setName("Together AI configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Together AI API key").setDesc("Your Together AI API key.").addText((text) => text.setPlaceholder("Enter your Together AI API key").setValue(this.plugin.settings.togetherApiKey).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Together AI configuration").setHeading().setClass("sf-settings-subsection-provider-header");
+ new import_obsidian19.Setting(mcqSection).setName("Together AI API key").setDesc("Your Together AI API key.").addText((text) => text.setPlaceholder("Enter your Together AI API key").setValue(this.plugin.settings.togetherApiKey).onChange(async (value) => {
this.plugin.settings.togetherApiKey = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).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").setValue(this.plugin.settings.togetherModel).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).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").setValue(this.plugin.settings.togetherModel).onChange(async (value) => {
this.plugin.settings.togetherModel = value;
await this.plugin.savePluginData();
}));
}
- new import_obsidian17.Setting(mcqSection).setName("Question generation (common)").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(mcqSection).setName("Question amount mode").setDesc("How to determine the number of questions per note.").addDropdown((dropdown) => dropdown.addOption("fixed" /* Fixed */, "Fixed Number").addOption("wordsPerQuestion" /* WordsPerQuestion */, "Per X Words").setValue(this.plugin.settings.mcqQuestionAmountMode).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Question generation (common)").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(mcqSection).setName("Question amount mode").setDesc("How to determine the number of questions per note.").addDropdown((dropdown) => dropdown.addOption("fixed" /* Fixed */, "Fixed Number").addOption("wordsPerQuestion" /* WordsPerQuestion */, "Per X Words").setValue(this.plugin.settings.mcqQuestionAmountMode).onChange(async (value) => {
this.plugin.settings.mcqQuestionAmountMode = value;
await this.plugin.savePluginData();
this.display();
}));
if (this.plugin.settings.mcqQuestionAmountMode === "fixed" /* Fixed */) {
- new import_obsidian17.Setting(mcqSection).setName("Questions per note (Fixed)").setDesc("Number of questions to generate for each note.").addSlider((slider) => slider.setLimits(1, 10, 1).setValue(this.plugin.settings.mcqQuestionsPerNote).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Questions per note (Fixed)").setDesc("Number of questions to generate for each note.").addSlider((slider) => slider.setLimits(1, 10, 1).setValue(this.plugin.settings.mcqQuestionsPerNote).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.mcqQuestionsPerNote = value;
await this.plugin.savePluginData();
}));
} else if (this.plugin.settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- new import_obsidian17.Setting(mcqSection).setName("Words per question target").setDesc("Generate approximately 1 question for every X words in the note.").addText((text) => text.setPlaceholder("100").setValue(this.plugin.settings.mcqWordsPerQuestion.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Words per question target").setDesc("Generate approximately 1 question for every X words in the note.").addText((text) => text.setPlaceholder("100").setValue(this.plugin.settings.mcqWordsPerQuestion.toString()).onChange(async (value) => {
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
this.plugin.settings.mcqWordsPerQuestion = numValue;
await this.plugin.savePluginData();
} else {
- new import_obsidian17.Notice("Words per question must be a positive number.");
+ new import_obsidian19.Notice("Words per question must be a positive number.");
text.setValue(this.plugin.settings.mcqWordsPerQuestion.toString());
}
}));
}
- new import_obsidian17.Setting(mcqSection).setName("Choices per question").setDesc("Number of answer choices for each question").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.mcqChoicesPerQuestion).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Choices per question").setDesc("Number of answer choices for each question").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.mcqChoicesPerQuestion).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.mcqChoicesPerQuestion = value;
await this.plugin.savePluginData();
}));
const mcqFormattingGrid = mcqSection.createEl("div", { cls: "sf-setting-grid" });
const promptTypeContainer = mcqFormattingGrid.createEl("div");
- new import_obsidian17.Setting(promptTypeContainer).setName("Prompt type").setDesc("Format for MCQ generation").addDropdown((dropdown) => dropdown.addOption("basic", "Basic").addOption("detailed", "Detailed").setValue(this.plugin.settings.mcqPromptType).onChange(async (value) => {
+ new import_obsidian19.Setting(promptTypeContainer).setName("Prompt type").setDesc("Format for MCQ generation").addDropdown((dropdown) => dropdown.addOption("basic", "Basic").addOption("detailed", "Detailed").setValue(this.plugin.settings.mcqPromptType).onChange(async (value) => {
this.plugin.settings.mcqPromptType = value;
await this.plugin.savePluginData();
}));
const difficultyContainer = mcqFormattingGrid.createEl("div");
- new import_obsidian17.Setting(difficultyContainer).setName("MCQ difficulty").setDesc("Complexity level").addDropdown((dropdown) => dropdown.addOption("basic" /* Basic */, "Basic recall").addOption("advanced" /* Advanced */, "Advanced understanding").setValue(this.plugin.settings.mcqDifficulty).onChange(async (value) => {
+ new import_obsidian19.Setting(difficultyContainer).setName("MCQ difficulty").setDesc("Complexity level").addDropdown((dropdown) => dropdown.addOption("basic" /* Basic */, "Basic recall").addOption("advanced" /* Advanced */, "Advanced understanding").setValue(this.plugin.settings.mcqDifficulty).onChange(async (value) => {
this.plugin.settings.mcqDifficulty = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("Scoring").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(mcqSection).setName("Time deduction amount").setDesc("Score penalty for slow answers (0-1)").addSlider((slider) => slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.mcqTimeDeductionAmount).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Scoring").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(mcqSection).setName("Time deduction amount").setDesc("Score penalty for slow answers (0-1)").addSlider((slider) => slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.mcqTimeDeductionAmount).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.mcqTimeDeductionAmount = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("Time deduction threshold").setDesc("Apply penalty after this many seconds").addSlider((slider) => slider.setLimits(10, 120, 5).setValue(this.plugin.settings.mcqTimeDeductionSeconds).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Time deduction threshold").setDesc("Apply penalty after this many seconds").addSlider((slider) => slider.setLimits(10, 120, 5).setValue(this.plugin.settings.mcqTimeDeductionSeconds).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.mcqTimeDeductionSeconds = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.Setting(mcqSection).setName("Deduct full mark on first failure").setDesc("If enabled, the score for a question will be 0 if the first attempt is incorrect.").addToggle((toggle) => toggle.setValue(this.plugin.settings.mcqDeductFullMarkOnFirstFailure).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Deduct full mark on first failure").setDesc("If enabled, the score for a question will be 0 if the first attempt is incorrect.").addToggle((toggle) => toggle.setValue(this.plugin.settings.mcqDeductFullMarkOnFirstFailure).onChange(async (value) => {
this.plugin.settings.mcqDeductFullMarkOnFirstFailure = value;
await this.plugin.savePluginData();
}));
@@ -7120,18 +7995,18 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.plugin.settings.mcqAdvancedSystemPrompt = advancedTextarea.value;
await this.plugin.savePluginData();
});
- new import_obsidian17.Setting(mcqSection).setName("Advanced question behavior").setHeading().setClass("sf-settings-subsection-header");
- const regenerationSetting = new import_obsidian17.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.setValue(this.plugin.settings.enableQuestionRegenerationOnRating).onChange(async (value) => {
+ new import_obsidian19.Setting(mcqSection).setName("Advanced question behavior").setHeading().setClass("sf-settings-subsection-header");
+ const regenerationSetting = new import_obsidian19.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.setValue(this.plugin.settings.enableQuestionRegenerationOnRating).onChange(async (value) => {
this.plugin.settings.enableQuestionRegenerationOnRating = value;
await this.plugin.savePluginData();
this.display();
}));
if (this.plugin.settings.enableQuestionRegenerationOnRating) {
- new import_obsidian17.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)").addSlider((slider) => slider.setLimits(0, 5, 1).setValue(this.plugin.settings.minSm2RatingForQuestionRegeneration).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.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)").addSlider((slider) => slider.setLimits(0, 5, 1).setValue(this.plugin.settings.minSm2RatingForQuestionRegeneration).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.minSm2RatingForQuestionRegeneration = value;
await this.plugin.savePluginData();
}));
- new import_obsidian17.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)").addSlider((slider) => slider.setLimits(1, 4, 1).setValue(this.plugin.settings.minFsrsRatingForQuestionRegeneration).setDynamicTooltip().onChange(async (value) => {
+ new import_obsidian19.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)").addSlider((slider) => slider.setLimits(1, 4, 1).setValue(this.plugin.settings.minFsrsRatingForQuestionRegeneration).setDynamicTooltip().onChange(async (value) => {
this.plugin.settings.minFsrsRatingForQuestionRegeneration = value;
await this.plugin.savePluginData();
}));
@@ -7143,7 +8018,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
});
}
const pomodoroSection = createCollapsible("Pomodoro timer", "timer", false);
- new import_obsidian17.Setting(pomodoroSection).setName("Enable pomodoro timer").setDesc("Show the Pomodoro timer in the sidebar.").addToggle((toggle) => toggle.setValue(this.plugin.settings.pomodoroEnabled).onChange(async (value) => {
+ new import_obsidian19.Setting(pomodoroSection).setName("Enable pomodoro timer").setDesc("Show the Pomodoro timer in the sidebar.").addToggle((toggle) => toggle.setValue(this.plugin.settings.pomodoroEnabled).onChange(async (value) => {
var _a, _b;
this.plugin.settings.pomodoroEnabled = value;
await this.plugin.savePluginData();
@@ -7152,8 +8027,8 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
this.display();
}));
if (this.plugin.settings.pomodoroEnabled) {
- new import_obsidian17.Setting(pomodoroSection).setName("Timer durations (minutes)").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(pomodoroSection).setName("Work duration").setDesc("Length of a work session.").addText((text) => text.setPlaceholder("25").setValue(this.plugin.settings.pomodoroWorkDuration.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(pomodoroSection).setName("Timer durations (minutes)").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(pomodoroSection).setName("Work duration").setDesc("Length of a work session.").addText((text) => text.setPlaceholder("25").setValue(this.plugin.settings.pomodoroWorkDuration.toString()).onChange(async (value) => {
var _a;
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
@@ -7162,7 +8037,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
(_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
}
}));
- new import_obsidian17.Setting(pomodoroSection).setName("Short break duration").setDesc("Length of a short break.").addText((text) => text.setPlaceholder("5").setValue(this.plugin.settings.pomodoroShortBreakDuration.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(pomodoroSection).setName("Short break duration").setDesc("Length of a short break.").addText((text) => text.setPlaceholder("5").setValue(this.plugin.settings.pomodoroShortBreakDuration.toString()).onChange(async (value) => {
var _a;
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
@@ -7171,7 +8046,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
(_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
}
}));
- new import_obsidian17.Setting(pomodoroSection).setName("Long break duration").setDesc("Length of a long break.").addText((text) => text.setPlaceholder("15").setValue(this.plugin.settings.pomodoroLongBreakDuration.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(pomodoroSection).setName("Long break duration").setDesc("Length of a long break.").addText((text) => text.setPlaceholder("15").setValue(this.plugin.settings.pomodoroLongBreakDuration.toString()).onChange(async (value) => {
var _a;
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
@@ -7180,7 +8055,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
(_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
}
}));
- new import_obsidian17.Setting(pomodoroSection).setName("Sessions until long break").setDesc("Number of work sessions before a long break starts.").addText((text) => text.setPlaceholder("4").setValue(this.plugin.settings.pomodoroSessionsUntilLongBreak.toString()).onChange(async (value) => {
+ new import_obsidian19.Setting(pomodoroSection).setName("Sessions until long break").setDesc("Number of work sessions before a long break starts.").addText((text) => text.setPlaceholder("4").setValue(this.plugin.settings.pomodoroSessionsUntilLongBreak.toString()).onChange(async (value) => {
var _a;
const numValue = parseInt(value);
if (!isNaN(numValue) && numValue > 0) {
@@ -7189,8 +8064,8 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
(_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
}
}));
- new import_obsidian17.Setting(pomodoroSection).setName("Notifications").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(pomodoroSection).setName("Enable sound notifications").setDesc("Play a sound at the end of each work/break session.").addToggle((toggle) => toggle.setValue(this.plugin.settings.pomodoroSoundEnabled).onChange(async (value) => {
+ new import_obsidian19.Setting(pomodoroSection).setName("Notifications").setHeading().setClass("sf-settings-subsection-header");
+ new import_obsidian19.Setting(pomodoroSection).setName("Enable sound notifications").setDesc("Play a sound at the end of each work/break session.").addToggle((toggle) => toggle.setValue(this.plugin.settings.pomodoroSoundEnabled).onChange(async (value) => {
this.plugin.settings.pomodoroSoundEnabled = value;
await this.plugin.savePluginData();
}));
@@ -7280,7 +8155,7 @@ var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
};
// api/openrouter-service.ts
-var import_obsidian18 = require("obsidian");
+var import_obsidian20 = require("obsidian");
var OpenRouterService = class {
/**
* Initialize OpenRouter service
@@ -7300,11 +8175,11 @@ var OpenRouterService = class {
*/
async generateMCQs(notePath, noteContent, settings) {
if (!settings.openRouterApiKey) {
- new import_obsidian18.Notice("OpenRouter API key is not set. Please add it in the settings.");
+ new import_obsidian20.Notice("OpenRouter API key is not set. Please add it in the settings.");
return null;
}
try {
- new import_obsidian18.Notice("Generating MCQs using OpenRouter...");
+ new import_obsidian20.Notice("Generating MCQs using OpenRouter...");
let numQuestionsToGenerate;
if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
@@ -7316,7 +8191,7 @@ var OpenRouterService = class {
const response = await this.makeApiRequest(prompt, settings);
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
- new import_obsidian18.Notice("Failed to generate valid MCQs from OpenRouter. Please try again.");
+ new import_obsidian20.Notice("Failed to generate valid MCQs from OpenRouter. Please try again.");
return null;
}
const mcqSet = {
@@ -7326,7 +8201,7 @@ var OpenRouterService = class {
};
return mcqSet;
} catch (error) {
- new import_obsidian18.Notice("Failed to generate MCQs with OpenRouter. Please check console for details.");
+ new import_obsidian20.Notice("Failed to generate MCQs with OpenRouter. Please check console for details.");
return null;
}
}
@@ -7384,7 +8259,7 @@ ${noteContent}`;
const difficulty = settings.mcqDifficulty;
try {
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
- const response = await (0, import_obsidian18.requestUrl)({
+ const response = await (0, import_obsidian20.requestUrl)({
url: "https://openrouter.ai/api/v1/chat/completions",
method: "POST",
headers: {
@@ -7418,7 +8293,7 @@ ${noteContent}`;
}
return data.choices[0].message.content;
} catch (error) {
- new import_obsidian18.Notice(`OpenRouter API error: ${error.message}`);
+ new import_obsidian20.Notice(`OpenRouter API error: ${error.message}`);
throw error;
}
}
@@ -7492,29 +8367,29 @@ ${noteContent}`;
}
return questions.slice(0, numQuestionsToGenerate);
} catch (error) {
- new import_obsidian18.Notice("Error parsing MCQ response from OpenRouter. Please try again.");
+ new import_obsidian20.Notice("Error parsing MCQ response from OpenRouter. Please try again.");
return [];
}
}
};
// api/openai-service.ts
-var import_obsidian19 = require("obsidian");
+var import_obsidian21 = require("obsidian");
var OpenAIService = class {
constructor(plugin) {
this.plugin = plugin;
}
async generateMCQs(notePath, noteContent, settings) {
if (!settings.openaiApiKey) {
- new import_obsidian19.Notice("OpenAI API key is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian21.Notice("OpenAI API key is not set. Please add it in the Spaceforge settings.");
return null;
}
if (!settings.openaiModel) {
- new import_obsidian19.Notice("OpenAI Model is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian21.Notice("OpenAI Model is not set. Please add it in the Spaceforge settings.");
return null;
}
try {
- new import_obsidian19.Notice("Generating MCQs using OpenAI...");
+ new import_obsidian21.Notice("Generating MCQs using OpenAI...");
let numQuestionsToGenerate;
if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
@@ -7526,7 +8401,7 @@ var OpenAIService = class {
const response = await this.makeApiRequest(prompt, settings);
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
- new import_obsidian19.Notice("Failed to generate valid MCQs from OpenAI. Please try again.");
+ new import_obsidian21.Notice("Failed to generate valid MCQs from OpenAI. Please try again.");
return null;
}
return {
@@ -7535,7 +8410,7 @@ var OpenAIService = class {
generatedAt: Date.now()
};
} catch (error) {
- new import_obsidian19.Notice("Failed to generate MCQs with OpenAI. Please check console for details.");
+ new import_obsidian21.Notice("Failed to generate MCQs with OpenAI. Please check console for details.");
return null;
}
}
@@ -7579,7 +8454,7 @@ ${noteContent}`;
const difficulty = settings.mcqDifficulty;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
try {
- const response = await (0, import_obsidian19.requestUrl)({
+ const response = await (0, import_obsidian21.requestUrl)({
url: "https://api.openai.com/v1/chat/completions",
method: "POST",
headers: {
@@ -7604,7 +8479,7 @@ ${noteContent}`;
}
return data.choices[0].message.content;
} catch (error) {
- new import_obsidian19.Notice(`OpenAI API error: ${error.message}`);
+ new import_obsidian21.Notice(`OpenAI API error: ${error.message}`);
throw error;
}
}
@@ -7659,29 +8534,29 @@ ${noteContent}`;
}
return questions.slice(0, numQuestionsToGenerate);
} catch (error) {
- new import_obsidian19.Notice("Error parsing MCQ response from OpenAI. Please try again.");
+ new import_obsidian21.Notice("Error parsing MCQ response from OpenAI. Please try again.");
return [];
}
}
};
// api/ollama-service.ts
-var import_obsidian20 = require("obsidian");
+var import_obsidian22 = require("obsidian");
var OllamaService = class {
constructor(plugin) {
this.plugin = plugin;
}
async generateMCQs(notePath, noteContent, settings) {
if (!settings.ollamaApiUrl) {
- new import_obsidian20.Notice("Ollama API URL is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian22.Notice("Ollama API URL is not set. Please add it in the Spaceforge settings.");
return null;
}
if (!settings.ollamaModel) {
- new import_obsidian20.Notice("Ollama Model is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian22.Notice("Ollama Model is not set. Please add it in the Spaceforge settings.");
return null;
}
try {
- new import_obsidian20.Notice("Generating MCQs using Ollama...");
+ new import_obsidian22.Notice("Generating MCQs using Ollama...");
let numQuestionsToGenerate;
if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
@@ -7693,7 +8568,7 @@ var OllamaService = class {
const response = await this.makeApiRequest(prompt, settings);
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
- new import_obsidian20.Notice("Failed to generate valid MCQs from Ollama. Please try again.");
+ new import_obsidian22.Notice("Failed to generate valid MCQs from Ollama. Please try again.");
return null;
}
return {
@@ -7702,7 +8577,7 @@ var OllamaService = class {
generatedAt: Date.now()
};
} catch (error) {
- new import_obsidian20.Notice("Failed to generate MCQs with Ollama. Please check console for details.");
+ new import_obsidian22.Notice("Failed to generate MCQs with Ollama. Please check console for details.");
return null;
}
}
@@ -7745,7 +8620,7 @@ ${noteContent}`;
const difficulty = settings.mcqDifficulty;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
try {
- const response = await (0, import_obsidian20.requestUrl)({
+ const response = await (0, import_obsidian22.requestUrl)({
url: `${apiUrl}/api/chat`,
method: "POST",
headers: {
@@ -7771,7 +8646,7 @@ ${noteContent}`;
}
return data.message.content;
} catch (error) {
- new import_obsidian20.Notice(`Ollama API error: ${error.message}`);
+ new import_obsidian22.Notice(`Ollama API error: ${error.message}`);
throw error;
}
}
@@ -7826,29 +8701,29 @@ ${noteContent}`;
}
return questions.slice(0, numQuestionsToGenerate);
} catch (error) {
- new import_obsidian20.Notice("Error parsing MCQ response from Ollama. Please try again.");
+ new import_obsidian22.Notice("Error parsing MCQ response from Ollama. Please try again.");
return [];
}
}
};
// api/gemini-service.ts
-var import_obsidian21 = require("obsidian");
+var import_obsidian23 = require("obsidian");
var GeminiService = class {
constructor(plugin) {
this.plugin = plugin;
}
async generateMCQs(notePath, noteContent, settings) {
if (!settings.geminiApiKey) {
- new import_obsidian21.Notice("Gemini API key is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian23.Notice("Gemini API key is not set. Please add it in the Spaceforge settings.");
return null;
}
if (!settings.geminiModel) {
- new import_obsidian21.Notice("Gemini Model is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian23.Notice("Gemini Model is not set. Please add it in the Spaceforge settings.");
return null;
}
try {
- new import_obsidian21.Notice("Generating MCQs using Gemini...");
+ new import_obsidian23.Notice("Generating MCQs using Gemini...");
let numQuestionsToGenerate;
if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
@@ -7860,7 +8735,7 @@ var GeminiService = class {
const response = await this.makeApiRequest(prompt, settings);
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
- new import_obsidian21.Notice("Failed to generate valid MCQs from Gemini. Please try again.");
+ new import_obsidian23.Notice("Failed to generate valid MCQs from Gemini. Please try again.");
return null;
}
return {
@@ -7869,7 +8744,7 @@ var GeminiService = class {
generatedAt: Date.now()
};
} catch (error) {
- new import_obsidian21.Notice("Failed to generate MCQs with Gemini. Please check console for details.");
+ new import_obsidian23.Notice("Failed to generate MCQs with Gemini. Please check console for details.");
return null;
}
}
@@ -7908,7 +8783,7 @@ ${noteContent}`;
const model = settings.geminiModel;
const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
try {
- const response = await (0, import_obsidian21.requestUrl)({
+ const response = await (0, import_obsidian23.requestUrl)({
url: apiUrl,
method: "POST",
headers: {
@@ -7935,7 +8810,7 @@ ${noteContent}`;
}
return data.candidates[0].content.parts[0].text;
} catch (error) {
- new import_obsidian21.Notice(`Gemini API error: ${error.message}`);
+ new import_obsidian23.Notice(`Gemini API error: ${error.message}`);
throw error;
}
}
@@ -7990,29 +8865,29 @@ ${noteContent}`;
}
return questions.slice(0, numQuestionsToGenerate);
} catch (error) {
- new import_obsidian21.Notice("Error parsing MCQ response from Gemini. Please try again.");
+ new import_obsidian23.Notice("Error parsing MCQ response from Gemini. Please try again.");
return [];
}
}
};
// api/claude-service.ts
-var import_obsidian22 = require("obsidian");
+var import_obsidian24 = require("obsidian");
var ClaudeService = class {
constructor(plugin) {
this.plugin = plugin;
}
async generateMCQs(notePath, noteContent, settings) {
if (!settings.claudeApiKey) {
- new import_obsidian22.Notice("Claude API key is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian24.Notice("Claude API key is not set. Please add it in the Spaceforge settings.");
return null;
}
if (!settings.claudeModel) {
- new import_obsidian22.Notice("Claude Model is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian24.Notice("Claude Model is not set. Please add it in the Spaceforge settings.");
return null;
}
try {
- new import_obsidian22.Notice("Generating MCQs using Claude...");
+ new import_obsidian24.Notice("Generating MCQs using Claude...");
let numQuestionsToGenerate;
if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
@@ -8024,7 +8899,7 @@ var ClaudeService = class {
const response = await this.makeApiRequest(prompt, settings);
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
- new import_obsidian22.Notice("Failed to generate valid MCQs from Claude. Please try again.");
+ new import_obsidian24.Notice("Failed to generate valid MCQs from Claude. Please try again.");
return null;
}
return {
@@ -8033,7 +8908,7 @@ var ClaudeService = class {
generatedAt: Date.now()
};
} catch (error) {
- new import_obsidian22.Notice("Failed to generate MCQs with Claude. Please check console for details.");
+ new import_obsidian24.Notice("Failed to generate MCQs with Claude. Please check console for details.");
return null;
}
}
@@ -8077,7 +8952,7 @@ ${noteContent}`;
const difficulty = settings.mcqDifficulty;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
try {
- const response = await (0, import_obsidian22.requestUrl)({
+ const response = await (0, import_obsidian24.requestUrl)({
url: "https://api.anthropic.com/v1/messages",
method: "POST",
headers: {
@@ -8105,7 +8980,7 @@ ${noteContent}`;
}
return data.content[0].text;
} catch (error) {
- new import_obsidian22.Notice(`Claude API error: ${error.message}`);
+ new import_obsidian24.Notice(`Claude API error: ${error.message}`);
throw error;
}
}
@@ -8186,29 +9061,29 @@ ${noteContent}`;
}
return questions.slice(0, numQuestionsToGenerate);
} catch (error) {
- new import_obsidian22.Notice("Error parsing MCQ response from Claude. Please try again.");
+ new import_obsidian24.Notice("Error parsing MCQ response from Claude. Please try again.");
return [];
}
}
};
// api/together-service.ts
-var import_obsidian23 = require("obsidian");
+var import_obsidian25 = require("obsidian");
var TogetherService = class {
constructor(plugin) {
this.plugin = plugin;
}
async generateMCQs(notePath, noteContent, settings) {
if (!settings.togetherApiKey) {
- new import_obsidian23.Notice("Together AI API key is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian25.Notice("Together AI API key is not set. Please add it in the Spaceforge settings.");
return null;
}
if (!settings.togetherModel) {
- new import_obsidian23.Notice("Together AI Model is not set. Please add it in the Spaceforge settings.");
+ new import_obsidian25.Notice("Together AI Model is not set. Please add it in the Spaceforge settings.");
return null;
}
try {
- new import_obsidian23.Notice("Generating MCQs using Together AI...");
+ new import_obsidian25.Notice("Generating MCQs using Together AI...");
let numQuestionsToGenerate;
if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
@@ -8220,7 +9095,7 @@ var TogetherService = class {
const response = await this.makeApiRequest(prompt, settings);
const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
if (questions.length === 0) {
- new import_obsidian23.Notice("Failed to generate valid MCQs from Together AI. Please try again.");
+ new import_obsidian25.Notice("Failed to generate valid MCQs from Together AI. Please try again.");
return null;
}
return {
@@ -8229,7 +9104,7 @@ var TogetherService = class {
generatedAt: Date.now()
};
} catch (error) {
- new import_obsidian23.Notice("Failed to generate MCQs with Together AI. Please check console for details.");
+ new import_obsidian25.Notice("Failed to generate MCQs with Together AI. Please check console for details.");
return null;
}
}
@@ -8273,7 +9148,7 @@ ${noteContent}`;
const difficulty = settings.mcqDifficulty;
const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
try {
- const response = await (0, import_obsidian23.requestUrl)({
+ const response = await (0, import_obsidian25.requestUrl)({
url: "https://api.together.xyz/v1/chat/completions",
method: "POST",
headers: {
@@ -8300,7 +9175,7 @@ ${noteContent}`;
}
return data.choices[0].message.content;
} catch (error) {
- new import_obsidian23.Notice(`Together AI API error: ${error.message}`);
+ new import_obsidian25.Notice(`Together AI API error: ${error.message}`);
throw error;
}
}
@@ -8380,14 +9255,14 @@ ${noteContent}`;
}
return questions.slice(0, numQuestionsToGenerate);
} catch (error) {
- new import_obsidian23.Notice("Error parsing MCQ response from Together AI. Please try again.");
+ new import_obsidian25.Notice("Error parsing MCQ response from Together AI. Please try again.");
return [];
}
}
};
// services/review-schedule-service.ts
-var import_obsidian24 = require("obsidian");
+var import_obsidian26 = require("obsidian");
// services/fsrs-schedule-service.ts
var FsrsScheduleService = class {
@@ -8521,8 +9396,8 @@ var ReviewScheduleService = class {
*/
async scheduleNoteForReview(path, daysFromNow = 0) {
const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!file || !(file instanceof import_obsidian24.TFile) || file.extension !== "md") {
- new import_obsidian24.Notice("Only markdown files can be added to the review schedule");
+ if (!file || !(file instanceof import_obsidian26.TFile) || file.extension !== "md") {
+ new import_obsidian26.Notice("Only markdown files can be added to the review schedule");
return;
}
const now = Date.now();
@@ -8581,7 +9456,7 @@ var ReviewScheduleService = class {
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
- new import_obsidian24.Notice(`Note added to review schedule`);
+ new import_obsidian26.Notice(`Note added to review schedule`);
}
/**
* Check if a note is due for review on or before the specified date
@@ -9005,7 +9880,7 @@ var ReviewScheduleService = class {
this.plugin.events.emit("sidebar-update");
}, 50);
}
- new import_obsidian24.Notice(`Review postponed for ${days} day${days !== 1 ? "s" : ""}`);
+ new import_obsidian26.Notice(`Review postponed for ${days} day${days !== 1 ? "s" : ""}`);
}
/**
* Advance a note's review by one day, if eligible.
@@ -9045,7 +9920,7 @@ var ReviewScheduleService = class {
if (this.schedules[path]) {
delete this.schedules[path];
this.customNoteOrder = this.customNoteOrder.filter((p2) => p2 !== path);
- new import_obsidian24.Notice("Note removed from review schedule");
+ new import_obsidian26.Notice("Note removed from review schedule");
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
@@ -9057,7 +9932,7 @@ var ReviewScheduleService = class {
async clearAllSchedules() {
this.schedules = {};
this.customNoteOrder = [];
- new import_obsidian24.Notice("All review schedules have been cleared");
+ new import_obsidian26.Notice("All review schedules have been cleared");
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
}
@@ -9073,7 +9948,7 @@ var ReviewScheduleService = class {
*/
async estimateReviewTime(path) {
const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!(file instanceof import_obsidian24.TFile))
+ if (!(file instanceof import_obsidian26.TFile))
return 60;
try {
const content = await this.plugin.app.vault.read(file);
@@ -9093,7 +9968,7 @@ var ReviewScheduleService = class {
let count = 0;
for (const path of paths) {
const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!file || !(file instanceof import_obsidian24.TFile) || file.extension !== "md" || this.schedules[path]) {
+ if (!file || !(file instanceof import_obsidian26.TFile) || file.extension !== "md" || this.schedules[path]) {
continue;
}
const now = Date.now();
@@ -9320,7 +10195,7 @@ var ReviewHistoryService = class {
};
// services/review-session-service.ts
-var import_obsidian25 = require("obsidian");
+var import_obsidian27 = require("obsidian");
// models/review-session.ts
function generateSessionId(prefix = "session") {
@@ -9367,8 +10242,8 @@ var ReviewSessionService = class {
*/
async createReviewSession(folderPath, name) {
const folder = this.plugin.app.vault.getAbstractFileByPath(folderPath);
- if (!folder || !(folder instanceof import_obsidian25.TFolder)) {
- new import_obsidian25.Notice("Invalid folder for review session");
+ if (!folder || !(folder instanceof import_obsidian27.TFolder)) {
+ new import_obsidian27.Notice("Invalid folder for review session");
return null;
}
try {
@@ -9395,7 +10270,7 @@ var ReviewSessionService = class {
}
return session;
} catch (error) {
- new import_obsidian25.Notice("Failed to create review session");
+ new import_obsidian27.Notice("Failed to create review session");
return null;
}
}
@@ -9464,7 +10339,7 @@ var ReviewSessionService = class {
if (isSessionComplete(updatedSession)) {
updatedSession.isActive = false;
this.reviewSessions.activeSessionId = null;
- new import_obsidian25.Notice(`Completed review session: ${updatedSession.name}`);
+ new import_obsidian27.Notice(`Completed review session: ${updatedSession.name}`);
}
if (this.plugin.events) {
this.plugin.events.emit("sidebar-update");
@@ -9974,8 +10849,315 @@ var PomodoroService = class {
}
};
+// services/calendar-event-service.ts
+var CalendarEventService = class {
+ constructor() {
+ /**
+ * Storage for calendar events
+ */
+ this.events = /* @__PURE__ */ new Map();
+ }
+ /**
+ * Initialize the service with existing events
+ *
+ * @param events Array of existing events
+ */
+ initialize(events = []) {
+ this.events.clear();
+ events.forEach((event) => {
+ this.events.set(event.id, event);
+ });
+ }
+ /**
+ * Get all events
+ *
+ * @returns Array of all events
+ */
+ getAllEvents() {
+ return Array.from(this.events.values());
+ }
+ /**
+ * Get event by ID
+ *
+ * @param id Event ID
+ * @returns Event or null if not found
+ */
+ getEventById(id) {
+ return this.events.get(id) || null;
+ }
+ /**
+ * Create a new event
+ *
+ * @param eventData Event data (without id, createdAt, updatedAt)
+ * @returns Created event
+ */
+ createEvent(eventData) {
+ const now = Date.now();
+ const event = {
+ ...eventData,
+ id: this.generateId(),
+ createdAt: now,
+ updatedAt: now
+ };
+ this.events.set(event.id, event);
+ return event;
+ }
+ /**
+ * Update an existing event
+ *
+ * @param id Event ID
+ * @param updates Partial event data to update
+ * @returns Updated event or null if not found
+ */
+ updateEvent(id, updates) {
+ const existingEvent = this.events.get(id);
+ if (!existingEvent) {
+ return null;
+ }
+ const updatedEvent = {
+ ...existingEvent,
+ ...updates,
+ id,
+ // Ensure ID doesn't change
+ createdAt: existingEvent.createdAt,
+ // Preserve creation time
+ updatedAt: Date.now()
+ };
+ this.events.set(id, updatedEvent);
+ return updatedEvent;
+ }
+ /**
+ * Delete an event
+ *
+ * @param id Event ID
+ * @returns True if deleted, false if not found
+ */
+ deleteEvent(id) {
+ return this.events.delete(id);
+ }
+ /**
+ * Get events for a specific date range
+ *
+ * @param startDate Start date timestamp
+ * @param endDate End date timestamp
+ * @returns Array of events in the date range
+ */
+ getEventsInRange(startDate, endDate) {
+ return this.getAllEvents().filter((event) => {
+ 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;
+ });
+ }
+ /**
+ * Get events for a specific date
+ *
+ * @param date Date timestamp
+ * @returns Array of events for the date
+ */
+ getEventsForDate(date) {
+ const targetDate = DateUtils.startOfDay(new Date(date));
+ return this.getAllEvents().filter((event) => {
+ const eventDate = DateUtils.startOfDay(new Date(event.date));
+ return eventDate === targetDate;
+ });
+ }
+ /**
+ * Get events grouped by date for a date range
+ *
+ * @param startDate Start date timestamp
+ * @param endDate End date timestamp
+ * @returns Map of date keys to DateEvents
+ */
+ getEventsGroupedByDate(startDate, endDate) {
+ const eventsByDate = /* @__PURE__ */ new Map();
+ const eventsInRange = this.getEventsInRange(startDate, endDate);
+ eventsInRange.forEach((event) => {
+ const eventDateStart = DateUtils.startOfDay(new Date(event.date));
+ const dateKey = eventDateStart.toString();
+ if (!eventsByDate.has(dateKey)) {
+ eventsByDate.set(dateKey, {
+ timestamp: eventDateStart,
+ events: []
+ });
+ }
+ const dateEvents = eventsByDate.get(dateKey);
+ if (dateEvents) {
+ dateEvents.events.push(event);
+ }
+ });
+ return eventsByDate;
+ }
+ /**
+ * Get upcoming events
+ *
+ * @param daysAhead Number of days ahead to look
+ * @param fromDate Optional start date (defaults to today)
+ * @returns Array of upcoming events
+ */
+ getUpcomingEvents(daysAhead = 7, fromDate = /* @__PURE__ */ new Date()) {
+ const today = DateUtils.startOfDay(fromDate);
+ const endDate = DateUtils.addDays(today, daysAhead);
+ const eventsInRange = this.getEventsInRange(today, endDate);
+ return eventsInRange.map((event) => {
+ const eventDate = DateUtils.startOfDay(new Date(event.date));
+ const daysUntil = Math.floor((eventDate - today) / (24 * 60 * 60 * 1e3));
+ return {
+ event,
+ daysUntil,
+ isToday: daysUntil === 0,
+ isTomorrow: daysUntil === 1
+ };
+ }).sort((a, b2) => {
+ if (a.daysUntil !== b2.daysUntil) {
+ return a.daysUntil - b2.daysUntil;
+ }
+ if (a.event.isAllDay && !b2.event.isAllDay)
+ return -1;
+ if (!a.event.isAllDay && b2.event.isAllDay)
+ return 1;
+ if (a.event.time && b2.event.time) {
+ return a.event.time.localeCompare(b2.event.time);
+ }
+ return 0;
+ });
+ }
+ /**
+ * Generate recurring event instances
+ *
+ * @param event Recurring event
+ * @param startDate Start date for generating instances
+ * @param endDate End date for generating instances
+ * @returns Array of event instances
+ */
+ generateRecurringInstances(event, startDate, endDate) {
+ if (event.recurrence === "none" /* None */) {
+ return [event];
+ }
+ const instances = [];
+ const start = DateUtils.startOfDay(new Date(startDate));
+ const end = DateUtils.startOfDay(new Date(endDate));
+ const eventDate = DateUtils.startOfDay(new Date(event.date));
+ const recurrenceEnd = event.recurrenceEndDate ? DateUtils.startOfDay(new Date(event.recurrenceEndDate)) : end;
+ let currentDate = new Date(eventDate);
+ while (currentDate.getTime() <= Math.min(end, recurrenceEnd)) {
+ if (currentDate.getTime() >= start) {
+ const instance = {
+ ...event,
+ date: currentDate.getTime(),
+ id: `${event.id}-${currentDate.getTime()}`
+ // Unique ID for instance
+ };
+ instances.push(instance);
+ }
+ switch (event.recurrence) {
+ case "daily" /* Daily */:
+ currentDate.setDate(currentDate.getDate() + 1);
+ break;
+ case "weekly" /* Weekly */:
+ currentDate.setDate(currentDate.getDate() + 7);
+ break;
+ case "monthly" /* Monthly */:
+ currentDate.setMonth(currentDate.getMonth() + 1);
+ break;
+ case "yearly" /* Yearly */:
+ currentDate.setFullYear(currentDate.getFullYear() + 1);
+ break;
+ }
+ }
+ return instances;
+ }
+ /**
+ * Get events by category
+ *
+ * @param category Event category
+ * @returns Array of events in the category
+ */
+ getEventsByCategory(category) {
+ return this.getAllEvents().filter((event) => event.category === category);
+ }
+ /**
+ * Search events by title or description
+ *
+ * @param query Search query
+ * @returns Array of matching events
+ */
+ searchEvents(query) {
+ const lowerQuery = query.toLowerCase();
+ return this.getAllEvents().filter(
+ (event) => event.title.toLowerCase().includes(lowerQuery) || event.description && event.description.toLowerCase().includes(lowerQuery)
+ );
+ }
+ /**
+ * Export events to JSON
+ *
+ * @returns JSON string of all events
+ */
+ exportEvents() {
+ return JSON.stringify(this.getAllEvents(), null, 2);
+ }
+ /**
+ * Import events from JSON
+ *
+ * @param json JSON string of events
+ * @returns Number of imported events
+ */
+ importEvents(json) {
+ try {
+ const events = 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);
+ return 0;
+ }
+ }
+ /**
+ * Generate a unique ID for new events
+ *
+ * @returns Unique ID string
+ */
+ generateId() {
+ return `event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ }
+ /**
+ * Get event color (custom color or category color)
+ *
+ * @param event Event
+ * @returns Color hex code
+ */
+ getEventColor(event) {
+ return event.color || this.getCategoryColor(event.category);
+ }
+ /**
+ * Get category color
+ *
+ * @param category Event category
+ * @returns Color hex code
+ */
+ getCategoryColor(category) {
+ const colors = {
+ ["work" /* Work */]: "#4A90E2",
+ ["personal" /* Personal */]: "#7B68EE",
+ ["study" /* Study */]: "#50C878",
+ ["meeting" /* Meeting */]: "#FF6B6B",
+ ["health" /* Health */]: "#FF9F40",
+ ["social" /* Social */]: "#FF69B4",
+ ["other" /* Other */]: "#95A5A6"
+ };
+ return colors[category] || colors["other" /* Other */];
+ }
+};
+
// main.ts
-var SpaceforgePlugin = class extends import_obsidian26.Plugin {
+var SpaceforgePlugin = class extends import_obsidian28.Plugin {
constructor() {
super(...arguments);
this.stylesheetPath = "styles.css";
@@ -10004,6 +11186,9 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
await this.loadPluginData();
this.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
this.pomodoroService = new PomodoroService(this);
+ this.calendarEventService = new CalendarEventService();
+ const eventsArray = Object.values(this.pluginState.calendarEvents);
+ this.calendarEventService.initialize(eventsArray);
this.registerView(
"spaceforge-review-schedule",
(leaf) => new ReviewSidebarView(leaf, this)
@@ -10037,15 +11222,15 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
const viewWithFile = fileExplorerLeaf == null ? void 0 : fileExplorerLeaf.view;
if (viewWithFile == null ? void 0 : viewWithFile.file) {
const selectedFile = viewWithFile.file;
- if (selectedFile instanceof import_obsidian26.TFile && selectedFile.extension === "md") {
+ if (selectedFile instanceof import_obsidian28.TFile && selectedFile.extension === "md") {
await this.reviewScheduleService.scheduleNoteForReview(selectedFile.path);
await this.savePluginData();
- new import_obsidian26.Notice(`Added "${selectedFile.path}" to review schedule.`);
+ new import_obsidian28.Notice(`Added "${selectedFile.path}" to review schedule.`);
} else {
- new import_obsidian26.Notice("Selected item is not a markdown file.");
+ new import_obsidian28.Notice("Selected item is not a markdown file.");
}
} else {
- new import_obsidian26.Notice("No file selected in file explorer.");
+ new import_obsidian28.Notice("No file selected in file explorer.");
}
}
});
@@ -10053,7 +11238,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
}));
this.registerEvent(this.app.vault.on("delete", async (file) => {
var _a2;
- if (file instanceof import_obsidian26.TFile && file.extension === "md") {
+ if (file instanceof import_obsidian28.TFile && file.extension === "md") {
await this.reviewScheduleService.removeFromReview(file.path);
await this.savePluginData();
}
@@ -10061,7 +11246,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
}));
this.registerEvent(this.app.vault.on("rename", async (file, oldPath) => {
var _a2;
- if (file instanceof import_obsidian26.TFile && file.extension === "md") {
+ if (file instanceof import_obsidian28.TFile && file.extension === "md") {
this.reviewScheduleService.handleNoteRename(oldPath, file.path);
await this.savePluginData();
}
@@ -10190,7 +11375,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
pathForJson += "data.json";
const vaultBasePath = this.app.vault.getRoot().path;
let absolutePath = (vaultBasePath ? vaultBasePath + "/" : "") + pathForJson;
- absolutePath = (0, import_obsidian26.normalizePath)(absolutePath);
+ absolutePath = (0, import_obsidian28.normalizePath)(absolutePath);
return absolutePath;
} else {
return null;
@@ -10215,15 +11400,15 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
try {
if (effectivePath) {
const file = this.app.vault.getAbstractFileByPath(effectivePath);
- if (file instanceof import_obsidian26.TFile) {
+ if (file instanceof import_obsidian28.TFile) {
const jsonData = await this.app.vault.read(file);
if (jsonData)
rawLoadedData = JSON.parse(jsonData);
- new import_obsidian26.Notice(`Spaceforge: Loaded data from custom path: ${effectivePath}`, 3e3);
+ new import_obsidian28.Notice(`Spaceforge: Loaded data from custom path: ${effectivePath}`, 3e3);
} else {
const oldFile = this.app.vault.getAbstractFileByPath(defaultPluginDataPath);
- if (oldFile instanceof import_obsidian26.TFile) {
- new import_obsidian26.Notice(`Spaceforge: Custom data file not found at ${effectivePath}. Attempting to migrate from default location.`, 5e3);
+ if (oldFile instanceof import_obsidian28.TFile) {
+ new import_obsidian28.Notice(`Spaceforge: Custom data file not found at ${effectivePath}. Attempting to migrate from default location.`, 5e3);
try {
const oldJsonData = await this.app.vault.read(oldFile);
if (oldJsonData) {
@@ -10233,7 +11418,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
}
}
if (!rawLoadedData) {
- new import_obsidian26.Notice(`Spaceforge: No data file found at custom path ${effectivePath}. New data file will be created on save.`, 3e3);
+ new import_obsidian28.Notice(`Spaceforge: No data file found at custom path ${effectivePath}. New data file will be created on save.`, 3e3);
}
}
} else {
@@ -10256,6 +11441,9 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
} else if (rawLoadedData && !rawLoadedData.pluginState && rawLoadedData.schedules) {
this.pluginState = { ...this.pluginState, ...rawLoadedData };
}
+ if (!this.pluginState.calendarEvents) {
+ this.pluginState.calendarEvents = {};
+ }
this.pluginState.pomodoroCurrentMode = this.pluginState.pomodoroCurrentMode || DEFAULT_PLUGIN_STATE_DATA.pomodoroCurrentMode;
this.pluginState.pomodoroTimeLeftInSeconds = this.pluginState.pomodoroTimeLeftInSeconds || DEFAULT_PLUGIN_STATE_DATA.pomodoroTimeLeftInSeconds;
this.pluginState.pomodoroSessionsCompletedInCycle = this.pluginState.pomodoroSessionsCompletedInCycle || DEFAULT_PLUGIN_STATE_DATA.pomodoroSessionsCompletedInCycle;
@@ -10301,7 +11489,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
if (this.reviewScheduleService) {
this.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
}
- new import_obsidian26.Notice("Spaceforge: Error loading data, initialized with defaults.", 5e3);
+ new import_obsidian28.Notice("Spaceforge: Error loading data, initialized with defaults.", 5e3);
}
}
async savePluginData() {
@@ -10336,6 +11524,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
pomodoroUserOverrideHours: typeof this.pluginState.pomodoroUserOverrideHours === "number" ? this.pluginState.pomodoroUserOverrideHours : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserOverrideHours,
pomodoroUserOverrideMinutes: typeof this.pluginState.pomodoroUserOverrideMinutes === "number" ? this.pluginState.pomodoroUserOverrideMinutes : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserOverrideMinutes,
pomodoroUserAddToEstimation: typeof this.pluginState.pomodoroUserAddToEstimation === "boolean" ? this.pluginState.pomodoroUserAddToEstimation : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserAddToEstimation,
+ calendarEvents: this.calendarEventService ? Object.fromEntries(this.calendarEventService.getAllEvents().map((event) => [event.id, event])) : {},
version: this.manifest.version
};
this.pluginState = currentPluginState;
@@ -10351,33 +11540,33 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
const dirPathOnly = effectiveSavePath.substring(0, effectiveSavePath.lastIndexOf("/"));
if (dirPathOnly && !this.app.vault.getAbstractFileByPath(dirPathOnly)) {
await this.app.vault.createFolder(dirPathOnly);
- new import_obsidian26.Notice(`Spaceforge: Created directory for custom data: ${dirPathOnly}`, 3e3);
+ new import_obsidian28.Notice(`Spaceforge: Created directory for custom data: ${dirPathOnly}`, 3e3);
}
const file = this.app.vault.getAbstractFileByPath(effectiveSavePath);
- if (file instanceof import_obsidian26.TFile) {
+ if (file instanceof import_obsidian28.TFile) {
await this.app.vault.modify(file, JSON.stringify(dataToSave, null, 2));
} else {
await this.app.vault.create(effectiveSavePath, JSON.stringify(dataToSave, null, 2));
}
const oldFile = this.app.vault.getAbstractFileByPath(defaultPluginDataPath);
- if (oldFile instanceof import_obsidian26.TFile) {
+ if (oldFile instanceof import_obsidian28.TFile) {
await this.app.vault.delete(oldFile);
- new import_obsidian26.Notice(`Spaceforge: Removed old data file from default plugin folder as custom path is active.`, 5e3);
+ new import_obsidian28.Notice(`Spaceforge: Removed old data file from default plugin folder as custom path is active.`, 5e3);
}
} catch (writeError) {
- new import_obsidian26.Notice(`Error saving data to custom path ${effectiveSavePath}: ${writeError.message}. Falling back to default path for this save.`, 1e4);
+ new import_obsidian28.Notice(`Error saving data to custom path ${effectiveSavePath}: ${writeError.message}. Falling back to default path for this save.`, 1e4);
try {
await this.saveData(dataToSave);
- new import_obsidian26.Notice(`Spaceforge: Data saved to default plugin folder due to error with custom path.`, 5e3);
+ new import_obsidian28.Notice(`Spaceforge: Data saved to default plugin folder due to error with custom path.`, 5e3);
} catch (fallbackError) {
- new import_obsidian26.Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations.`, 1e4);
+ new import_obsidian28.Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations.`, 1e4);
}
}
} else {
await this.saveData(dataToSave);
}
} catch (error) {
- new import_obsidian26.Notice("Error saving Spaceforge data. Check console for details.", 5e3);
+ new import_obsidian28.Notice("Error saving Spaceforge data. Check console for details.", 5e3);
}
}
async activateSidebarView() {
@@ -10393,7 +11582,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
});
this.app.workspace.revealLeaf(leaf);
} else {
- new import_obsidian26.Notice("Spaceforge: Could not open sidebar view.");
+ new import_obsidian28.Notice("Spaceforge: Could not open sidebar view.");
}
}
}
@@ -10417,10 +11606,10 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
name: "Review Current Note",
callback: () => {
const activeFile = this.app.workspace.getActiveFile();
- if (activeFile && activeFile instanceof import_obsidian26.TFile && activeFile.extension === "md") {
+ if (activeFile && activeFile instanceof import_obsidian28.TFile && activeFile.extension === "md") {
this.reviewController.reviewNote(activeFile.path);
} else {
- new import_obsidian26.Notice("No active markdown file to review.");
+ new import_obsidian28.Notice("No active markdown file to review.");
}
}
});
@@ -10429,12 +11618,12 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
name: "Add Current Note to Review Schedule",
callback: async () => {
const activeFile = this.app.workspace.getActiveFile();
- if (activeFile && activeFile instanceof import_obsidian26.TFile && activeFile.extension === "md") {
+ if (activeFile && activeFile instanceof import_obsidian28.TFile && activeFile.extension === "md") {
await this.reviewScheduleService.scheduleNoteForReview(activeFile.path);
await this.savePluginData();
- new import_obsidian26.Notice(`Added "${activeFile.path}" to review schedule.`);
+ new import_obsidian28.Notice(`Added "${activeFile.path}" to review schedule.`);
} else {
- new import_obsidian26.Notice("No active markdown file to add to review.");
+ new import_obsidian28.Notice("No active markdown file to add to review.");
}
}
});
@@ -10443,11 +11632,11 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
name: "Add Current Note's Folder to Review Schedule",
callback: async () => {
const activeFile = this.app.workspace.getActiveFile();
- if (activeFile && activeFile.parent && activeFile.parent instanceof import_obsidian26.TFolder) {
+ if (activeFile && activeFile.parent && activeFile.parent instanceof import_obsidian28.TFolder) {
const folder = activeFile.parent;
await this.contextMenuHandler.addFolderToReview(folder);
} else {
- new import_obsidian26.Notice("Could not determine the current note's folder, no active file, or parent is not a folder.");
+ new import_obsidian28.Notice("Could not determine the current note's folder, no active file, or parent is not a folder.");
}
}
});
@@ -10462,7 +11651,7 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
if (this.mcqGenerationService) {
this.mcqController = new MCQController(this, this.mcqService, this.mcqGenerationService);
} else {
- new import_obsidian26.Notice("MCQ Generation Service could not be initialized. Check API provider settings in Spaceforge settings.");
+ new import_obsidian28.Notice("MCQ Generation Service could not be initialized. Check API provider settings in Spaceforge settings.");
}
}
}
@@ -10470,42 +11659,42 @@ var SpaceforgePlugin = class extends import_obsidian26.Plugin {
switch (this.settings.mcqApiProvider) {
case "openrouter" /* OpenRouter */:
if (!this.settings.openRouterApiKey) {
- new import_obsidian26.Notice("OpenRouter API key is not set in Spaceforge settings.");
+ new import_obsidian28.Notice("OpenRouter API key is not set in Spaceforge settings.");
return void 0;
}
return new OpenRouterService(this);
case "openai" /* OpenAI */:
if (!this.settings.openaiApiKey) {
- new import_obsidian26.Notice("OpenAI API key is not set in Spaceforge settings.");
+ new import_obsidian28.Notice("OpenAI API key is not set in Spaceforge settings.");
return void 0;
}
return new OpenAIService(this);
case "ollama" /* Ollama */:
if (!this.settings.ollamaApiUrl || !this.settings.ollamaModel) {
- new import_obsidian26.Notice("Ollama API URL or Model is not set in Spaceforge settings.");
+ new import_obsidian28.Notice("Ollama API URL or Model is not set in Spaceforge settings.");
return void 0;
}
return new OllamaService(this);
case "gemini" /* Gemini */:
if (!this.settings.geminiApiKey) {
- new import_obsidian26.Notice("Gemini API key is not set in Spaceforge settings.");
+ new import_obsidian28.Notice("Gemini API key is not set in Spaceforge settings.");
return void 0;
}
return new GeminiService(this);
case "claude" /* Claude */:
if (!this.settings.claudeApiKey || !this.settings.claudeModel) {
- new import_obsidian26.Notice("Claude API key or Model is not set in Spaceforge settings.");
+ new import_obsidian28.Notice("Claude API key or Model is not set in Spaceforge settings.");
return void 0;
}
return new ClaudeService(this);
case "together" /* Together */:
if (!this.settings.togetherApiKey || !this.settings.togetherModel) {
- new import_obsidian26.Notice("Together AI API key or Model is not set in Spaceforge settings.");
+ new import_obsidian28.Notice("Together AI API key or Model is not set in Spaceforge settings.");
return void 0;
}
return new TogetherService(this);
default:
- new import_obsidian26.Notice(`Unsupported MCQ API provider selected: ${this.settings.mcqApiProvider}`);
+ new import_obsidian28.Notice(`Unsupported MCQ API provider selected: ${this.settings.mcqApiProvider}`);
return void 0;
}
}
diff --git a/main.ts b/main.ts
index 72a255b..9b98250 100644
--- a/main.ts
+++ b/main.ts
@@ -24,6 +24,7 @@ import { ReviewHistoryService } from './services/review-history-service';
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';
/**
* Spaceforge: Spaced Repetition Plugin for Obsidian
@@ -37,6 +38,7 @@ export default class SpaceforgePlugin extends Plugin {
reviewSessionService: ReviewSessionService;
mcqService: MCQService;
pomodoroService: PomodoroService;
+ calendarEventService: CalendarEventService;
private readonly stylesheetPath: string = "styles.css";
private readonly stylesheetId: string = "spaceforge-styles";
@@ -85,6 +87,11 @@ export default class SpaceforgePlugin extends Plugin {
// 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);
+ this.calendarEventService.initialize(eventsArray);
this.registerView(
@@ -373,6 +380,11 @@ export default class SpaceforgePlugin extends Plugin {
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;
@@ -471,6 +483,7 @@ export default class SpaceforgePlugin extends Plugin {
pomodoroUserOverrideHours: typeof this.pluginState.pomodoroUserOverrideHours === 'number' ? this.pluginState.pomodoroUserOverrideHours : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserOverrideHours,
pomodoroUserOverrideMinutes: typeof this.pluginState.pomodoroUserOverrideMinutes === 'number' ? this.pluginState.pomodoroUserOverrideMinutes : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserOverrideMinutes,
pomodoroUserAddToEstimation: typeof this.pluginState.pomodoroUserAddToEstimation === 'boolean' ? this.pluginState.pomodoroUserAddToEstimation : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserAddToEstimation,
+ calendarEvents: this.calendarEventService ? Object.fromEntries(this.calendarEventService.getAllEvents().map(event => [event.id, event])) : {},
version: this.manifest.version,
};
this.pluginState = currentPluginState; // Update the live pluginState
diff --git a/manifest.json b/manifest.json
index 71ad997..2b076f0 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"id": "spaceforge",
"name": "Spaceforge",
- "version": "1.0.2",
+ "version": "1.0.3",
"minAppVersion": "0.15.0",
"description": "A spaced repetition FSRS & SM-2 Algorithm powered by AI plugin for efficient knowledge review with pomodoro timer",
"author": "dralkh",
diff --git a/models/calendar-event.ts b/models/calendar-event.ts
new file mode 100644
index 0000000..9c15896
--- /dev/null
+++ b/models/calendar-event.ts
@@ -0,0 +1,124 @@
+/**
+ * Event categories with predefined colors
+ */
+export enum EventCategory {
+ Work = 'work',
+ Personal = 'personal',
+ Study = 'study',
+ Meeting = 'meeting',
+ Health = 'health',
+ Social = 'social',
+ Other = 'other'
+}
+
+/**
+ * Event recurrence patterns
+ */
+export enum EventRecurrence {
+ None = 'none',
+ Daily = 'daily',
+ Weekly = 'weekly',
+ Monthly = 'monthly',
+ Yearly = 'yearly'
+}
+
+/**
+ * Default colors for event categories
+ */
+export const EVENT_CATEGORY_COLORS: Record = {
+ [EventCategory.Work]: '#4A90E2',
+ [EventCategory.Personal]: '#7B68EE',
+ [EventCategory.Study]: '#50C878',
+ [EventCategory.Meeting]: '#FF6B6B',
+ [EventCategory.Health]: '#FF9F40',
+ [EventCategory.Social]: '#FF69B4',
+ [EventCategory.Other]: '#95A5A6'
+};
+
+/**
+ * Calendar event interface
+ */
+export interface CalendarEvent {
+ /**
+ * Unique identifier for the event
+ */
+ id: string;
+
+ /**
+ * Event title
+ */
+ title: string;
+
+ /**
+ * Event description (optional)
+ */
+ description?: string;
+
+ /**
+ * Event date (timestamp)
+ */
+ date: number;
+
+ /**
+ * Event time in HH:MM format (optional)
+ */
+ time?: string;
+
+ /**
+ * Event category
+ */
+ category: EventCategory;
+
+ /**
+ * Custom color (overrides category color if set)
+ */
+ color?: string;
+
+ /**
+ * Event recurrence pattern
+ */
+ recurrence: EventRecurrence;
+
+ /**
+ * End date for recurring events (optional)
+ */
+ recurrenceEndDate?: number;
+
+ /**
+ * Whether the event is all-day
+ */
+ isAllDay: boolean;
+
+ /**
+ * Event location (optional)
+ */
+ location?: string;
+
+ /**
+ * Creation timestamp
+ */
+ createdAt: number;
+
+ /**
+ * Last modification timestamp
+ */
+ updatedAt: number;
+}
+
+/**
+ * Interface for grouped events by date
+ */
+export interface DateEvents {
+ timestamp: number;
+ events: CalendarEvent[];
+}
+
+/**
+ * Interface for upcoming event display
+ */
+export interface UpcomingEvent {
+ event: CalendarEvent;
+ daysUntil: number;
+ isToday: boolean;
+ isTomorrow: boolean;
+}
\ No newline at end of file
diff --git a/models/plugin-data.ts b/models/plugin-data.ts
index f425e54..8f5da30 100644
--- a/models/plugin-data.ts
+++ b/models/plugin-data.ts
@@ -2,6 +2,7 @@ import { SpaceforgeSettings, DEFAULT_SETTINGS } from './settings';
import { ReviewSchedule, ReviewHistoryItem } from './review-schedule';
import { ReviewSessionStore } from './review-session';
import { MCQSet, MCQSession } from './mcq';
+import { CalendarEvent } from './calendar-event';
/**
* Interface for the plugin's operational state (excluding settings).
@@ -34,6 +35,9 @@ export interface PluginStateData {
pomodoroUserOverrideHours: number; // User-specified hours for override
pomodoroUserOverrideMinutes: number; // User-specified minutes for override
pomodoroUserAddToEstimation: boolean; // Whether to add user time to estimation or replace it
+
+ // Calendar Events
+ calendarEvents: Record;
}
/**
@@ -66,6 +70,9 @@ export const DEFAULT_PLUGIN_STATE_DATA: PluginStateData = {
pomodoroUserOverrideHours: 0,
pomodoroUserOverrideMinutes: 0,
pomodoroUserAddToEstimation: false,
+
+ // Calendar Events Defaults
+ calendarEvents: {},
};
/**
diff --git a/models/settings.ts b/models/settings.ts
index 66ce2b5..918c33b 100644
--- a/models/settings.ts
+++ b/models/settings.ts
@@ -310,6 +310,31 @@ export interface SpaceforgeSettings {
key: string | null;
};
navigationCommandDelay: number; // in milliseconds
+
+ // --- Calendar Events Settings ---
+ /**
+ * Enable calendar events feature
+ * Default: true
+ */
+ enableCalendarEvents: boolean;
+
+ /**
+ * Show upcoming events below calendar
+ * Default: true
+ */
+ showUpcomingEvents: boolean;
+
+ /**
+ * Number of days to show in upcoming events list
+ * Default: 7
+ */
+ upcomingEventsDays: number;
+
+ /**
+ * Default event category
+ * Default: 'personal'
+ */
+ defaultEventCategory: string;
}
/**
@@ -403,4 +428,10 @@ export const DEFAULT_SETTINGS: SpaceforgeSettings = {
key: null,
},
navigationCommandDelay: 500,
+
+ // Calendar Events Defaults
+ enableCalendarEvents: true,
+ showUpcomingEvents: true,
+ upcomingEventsDays: 7,
+ defaultEventCategory: 'personal',
};
diff --git a/releases/1.0.2/main.js b/releases/1.0.2/main.js
deleted file mode 100644
index 9429f66..0000000
--- a/releases/1.0.2/main.js
+++ /dev/null
@@ -1,10530 +0,0 @@
-/*
-THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
-if you want to view the source, please visit the github repository of this plugin
-*/
-
-var __defProp = Object.defineProperty;
-var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
-var __getOwnPropNames = Object.getOwnPropertyNames;
-var __hasOwnProp = Object.prototype.hasOwnProperty;
-var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
-var __export = (target, all) => {
- for (var name in all)
- __defProp(target, name, { get: all[name], enumerable: true });
-};
-var __copyProps = (to, from, except, desc) => {
- if (from && typeof from === "object" || typeof from === "function") {
- for (let key of __getOwnPropNames(from))
- if (!__hasOwnProp.call(to, key) && key !== except)
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
- }
- return to;
-};
-var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
-var __publicField = (obj, key, value) => {
- __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
- return value;
-};
-
-// main.ts
-var main_exports = {};
-__export(main_exports, {
- default: () => SpaceforgePlugin
-});
-module.exports = __toCommonJS(main_exports);
-var import_obsidian26 = require("obsidian");
-
-// utils/event-emitter.ts
-var EventEmitter = class {
- constructor() {
- /**
- * Event listeners by event name
- */
- this.listeners = {};
- }
- /**
- * Register a listener for an event
- *
- * @param event Event name
- * @param callback Function to call when event is emitted
- */
- on(event, callback) {
- if (!this.listeners[event]) {
- this.listeners[event] = [];
- }
- this.listeners[event].push(callback);
- }
- /**
- * Emit an event
- *
- * @param event Event name
- * @param args Arguments to pass to listeners
- */
- emit(event, ...args) {
- 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, callback) {
- if (!this.listeners[event]) {
- return;
- }
- this.listeners[event] = this.listeners[event].filter((cb) => cb !== callback);
- }
- /**
- * Remove all listeners for an event
- *
- * @param event Event name
- */
- removeAllListeners(event) {
- if (event) {
- delete this.listeners[event];
- } else {
- this.listeners = {};
- }
- }
-};
-
-// data-storage.ts
-var import_obsidian = require("obsidian");
-
-// models/review-schedule.ts
-var FsrsRating = /* @__PURE__ */ ((FsrsRating2) => {
- FsrsRating2[FsrsRating2["Again"] = 1] = "Again";
- FsrsRating2[FsrsRating2["Hard"] = 2] = "Hard";
- FsrsRating2[FsrsRating2["Good"] = 3] = "Good";
- FsrsRating2[FsrsRating2["Easy"] = 4] = "Easy";
- return FsrsRating2;
-})(FsrsRating || {});
-var ReviewResponse = /* @__PURE__ */ ((ReviewResponse2) => {
- ReviewResponse2[ReviewResponse2["CompleteBlackout"] = 0] = "CompleteBlackout";
- ReviewResponse2[ReviewResponse2["IncorrectResponse"] = 1] = "IncorrectResponse";
- ReviewResponse2[ReviewResponse2["IncorrectButFamiliar"] = 2] = "IncorrectButFamiliar";
- ReviewResponse2[ReviewResponse2["CorrectWithDifficulty"] = 3] = "CorrectWithDifficulty";
- ReviewResponse2[ReviewResponse2["CorrectWithHesitation"] = 4] = "CorrectWithHesitation";
- ReviewResponse2[ReviewResponse2["PerfectRecall"] = 5] = "PerfectRecall";
- ReviewResponse2[ReviewResponse2["Hard"] = 1] = "Hard";
- ReviewResponse2[ReviewResponse2["Fair"] = 3] = "Fair";
- ReviewResponse2[ReviewResponse2["Good"] = 4] = "Good";
- ReviewResponse2[ReviewResponse2["Perfect"] = 5] = "Perfect";
- return ReviewResponse2;
-})(ReviewResponse || {});
-function toSM2Quality(response) {
- if (response >= 0 && response <= 5) {
- return response;
- }
- switch (response) {
- case 1 /* Hard */:
- return 1 /* IncorrectResponse */;
- case 3 /* Fair */:
- return 3 /* CorrectWithDifficulty */;
- case 4 /* Good */:
- return 4 /* CorrectWithHesitation */;
- case 5 /* Perfect */:
- return 5 /* PerfectRecall */;
- default:
- return 3 /* CorrectWithDifficulty */;
- }
-}
-
-// utils/estimation.ts
-var EstimationUtils = class {
- /**
- * Set the plugin reference
- *
- * @param plugin Reference to the main plugin
- */
- static setPlugin(plugin) {
- this.plugin = plugin;
- }
- /**
- * Get the user's reading speed from settings
- *
- * @param contentType Optional content type for adjustment
- * @returns Reading speed in words per minute
- */
- static getReadingSpeed(contentType) {
- var _a;
- const baseSpeed = ((_a = this.plugin) == null ? void 0 : _a.settings.readingSpeed) || 200;
- 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
- *
- * @param file The file to estimate review time for
- * @param fileContent Optional file content (to avoid reading file again)
- * @param contentType Type of content for reading speed adjustment
- * @returns Estimated review time in seconds
- */
- static async estimateReviewTime(file, fileContent, contentType = "notes") {
- if (!file) {
- return this.MIN_REVIEW_TIME;
- }
- if (!fileContent) {
- const sizeEstimate = Math.ceil(file.stat.size / (this.AVG_WORD_LENGTH * 7)) * 60;
- return Math.max(this.MIN_REVIEW_TIME, sizeEstimate);
- }
- const wordCount = this.countWords(fileContent);
- const readingSpeed = this.getReadingSpeed(contentType);
- const readingTimeMinutes = wordCount / readingSpeed;
- const reviewTimeSeconds = Math.ceil(readingTimeMinutes * 60);
- return Math.max(this.MIN_REVIEW_TIME, reviewTimeSeconds);
- }
- /**
- * Calculate aggregate review time for multiple notes
- *
- * @param paths Array of note paths
- * @returns Total estimated review time in seconds
- */
- static async calculateTotalReviewTime(paths) {
- 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
- *
- * @param text Text to count words in
- * @returns Number of words
- */
- static countWords(text) {
- const cleanText = text.replace(/```[\s\S]*?```/g, "").replace(/`.*?`/g, "").replace(/\[.*?\]\(.*?\)/g, "").replace(/\*\*.*?\*\*/g, "$1").replace(/\*.*?\*/g, "$1").replace(/~~.*?~~/g, "$1");
- const words = cleanText.match(/\S+/g) || [];
- return words.length;
- }
- /**
- * Format seconds as a readable time string
- *
- * @param seconds Time in seconds
- * @returns Formatted time string (e.g., "5 min" or "1 hr 30 min")
- */
- static formatTime(seconds) {
- 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 {
- return `${hours} hr ${remainingMinutes} min`;
- }
- }
- }
- /**
- * Format a time estimate with color coding based on duration
- *
- * @param seconds Time in seconds
- * @param element HTML element to update
- * @returns Formatted HTML time string with color coding
- */
- static formatTimeWithColor(seconds, element) {
- const formattedTime = this.formatTime(seconds);
- element.setText(formattedTime);
- if (seconds < 5 * 60) {
- element.addClass("review-time-short");
- element.removeClass("review-time-medium");
- element.removeClass("review-time-long");
- } else if (seconds < 15 * 60) {
- element.addClass("review-time-medium");
- element.removeClass("review-time-short");
- element.removeClass("review-time-long");
- } else {
- element.addClass("review-time-long");
- element.removeClass("review-time-short");
- element.removeClass("review-time-medium");
- }
- }
-};
-/**
- * Reading speeds for different content types (words per minute)
- * Used as fallback and for content-specific adjustments
- */
-EstimationUtils.READING_SPEEDS = {
- notes: 200,
- // General notes
- technical: 100,
- // Technical content
- fiction: 250,
- // Fiction/prose
- simple: 300
- // Simple content
-};
-/**
- * Average English word length in characters (including spaces)
- */
-EstimationUtils.AVG_WORD_LENGTH = 5.5;
-/**
- * Minimum review time in seconds
- */
-EstimationUtils.MIN_REVIEW_TIME = 30;
-
-// data-storage.ts
-var DataStorage = class {
- /**
- * Initialize data storage
- *
- * @param plugin Reference to the main plugin
- * @param reviewScheduleService Instance of ReviewScheduleService
- * @param reviewHistoryService Instance of ReviewHistoryService
- * @param reviewSessionService Instance of ReviewSessionService
- * @param mcqService Instance of MCQService
- */
- constructor(plugin, reviewScheduleService, reviewHistoryService, reviewSessionService, mcqService) {
- this.plugin = plugin;
- this.reviewScheduleService = reviewScheduleService;
- this.reviewHistoryService = reviewHistoryService;
- this.reviewSessionService = reviewSessionService;
- this.mcqService = mcqService;
- }
- // Removed ensureDataLoaded() method
- // Removed loadData() method (and all localStorage logic within it)
- // Removed saveData() method (and all localStorage logic within it)
- /**
- * Initialize default data when no data is available
- * This is now primarily for internal use during integrity checks,
- * as main.ts handles initial default loading.
- */
- initializeDefaultData() {
- this.reviewScheduleService.schedules = {};
- this.reviewHistoryService.history = [];
- this.reviewSessionService.reviewSessions = {
- sessions: {},
- activeSessionId: null
- };
- this.mcqService.mcqSets = {};
- this.mcqService.mcqSessions = {};
- this.reviewScheduleService.customNoteOrder = [];
- this.reviewScheduleService.lastLinkAnalysisTimestamp = null;
- }
- /**
- * Verify data integrity and fix any issues
- * @returns true if data is valid, false if it needed to be fixed
- */
- verifyDataIntegrity() {
- let isValid = true;
- if (!this.reviewScheduleService.schedules || typeof this.reviewScheduleService.schedules !== "object") {
- 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;
- }
- const s = schedule;
- if (!("path" in s) || typeof s.path !== "string" || !("lastReviewDate" in s) || s.lastReviewDate !== null && typeof s.lastReviewDate !== "number" || !("nextReviewDate" in s) || typeof s.nextReviewDate !== "number" || !("ease" in s) || typeof s.ease !== "number") {
- delete this.reviewScheduleService.schedules[path];
- invalidCount++;
- isValid = false;
- }
- }
- }
- if (!Array.isArray(this.reviewHistoryService.history)) {
- this.reviewHistoryService.history = [];
- isValid = false;
- }
- if (!this.reviewSessionService.reviewSessions || typeof this.reviewSessionService.reviewSessions !== "object" || !this.reviewSessionService.reviewSessions.sessions || typeof this.reviewSessionService.reviewSessions.sessions !== "object") {
- this.reviewSessionService.reviewSessions = {
- sessions: {},
- activeSessionId: null
- };
- isValid = false;
- }
- if (!this.mcqService.mcqSets || typeof this.mcqService.mcqSets !== "object") {
- this.mcqService.mcqSets = {};
- isValid = false;
- }
- if (!this.mcqService.mcqSessions || typeof this.mcqService.mcqSessions !== "object") {
- this.mcqService.mcqSessions = {};
- isValid = false;
- }
- if (!Array.isArray(this.reviewScheduleService.customNoteOrder)) {
- this.reviewScheduleService.customNoteOrder = [];
- isValid = false;
- }
- if (this.reviewScheduleService.lastLinkAnalysisTimestamp !== null && typeof this.reviewScheduleService.lastLinkAnalysisTimestamp !== "number") {
- this.reviewScheduleService.lastLinkAnalysisTimestamp = null;
- isValid = false;
- }
- const noSchedules = Object.keys(this.reviewScheduleService.schedules).length === 0;
- const hasMCQs = Object.keys(this.mcqService.mcqSets).length > 0;
- if (noSchedules && hasMCQs) {
- }
- return isValid;
- }
- async cleanupNonExistentFiles() {
- let changesMade = false;
- if (!this.reviewScheduleService.schedules || typeof this.reviewScheduleService.schedules !== "object") {
- this.reviewScheduleService.schedules = {};
- changesMade = true;
- }
- if (!Array.isArray(this.reviewHistoryService.history)) {
- this.reviewHistoryService.history = [];
- changesMade = true;
- }
- if (!this.reviewSessionService.reviewSessions || typeof this.reviewSessionService.reviewSessions !== "object" || !this.reviewSessionService.reviewSessions.sessions) {
- this.reviewSessionService.reviewSessions = {
- sessions: {},
- activeSessionId: null
- };
- changesMade = true;
- }
- let cleanupCount = 0;
- const beforeCount = Object.keys(this.reviewScheduleService.schedules).length;
- const safetyCheck = {
- totalSchedules: beforeCount,
- checkedSchedules: 0,
- missingSchedules: 0,
- preserved: false
- };
- try {
- const allSchedules = { ...this.reviewScheduleService.schedules };
- const allFiles = /* @__PURE__ */ new Set();
- try {
- const mdFiles = this.plugin.app.vault.getMarkdownFiles();
- mdFiles.forEach((file) => allFiles.add(file.path));
- } catch (listError) {
- return changesMade;
- }
- if (allFiles.size === 0 && beforeCount > 0) {
- safetyCheck.preserved = true;
- return changesMade;
- }
- for (const path in allSchedules) {
- try {
- safetyCheck.checkedSchedules++;
- if (allFiles.has(path))
- continue;
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!file) {
- safetyCheck.missingSchedules++;
- delete this.reviewScheduleService.schedules[path];
- cleanupCount++;
- changesMade = true;
- }
- } catch (checkError) {
- }
- if (safetyCheck.missingSchedules > 0 && safetyCheck.missingSchedules === safetyCheck.checkedSchedules && safetyCheck.checkedSchedules >= 5) {
- this.reviewScheduleService.schedules = allSchedules;
- cleanupCount = 0;
- changesMade = false;
- safetyCheck.preserved = true;
- break;
- }
- }
- if (cleanupCount > 0 && !safetyCheck.preserved) {
- }
- const dataState = {
- schedules: Object.keys(this.reviewScheduleService.schedules).length,
- history: this.reviewHistoryService.history.length,
- reviewSessions: Object.keys(this.reviewSessionService.reviewSessions.sessions || {}).length,
- mcqSets: Object.keys(this.mcqService.mcqSets || {}).length,
- mcqSessions: Object.keys(this.mcqService.mcqSessions || {}).length
- };
- if (dataState.schedules === 0 && (dataState.history > 0 || dataState.reviewSessions > 0 || dataState.mcqSets > 0)) {
- }
- } catch (error) {
- }
- return changesMade;
- }
- // The following methods are now delegated to the respective service classes.
- // They are kept here as public methods to maintain the public API of DataStorage,
- // but they now simply call the corresponding method on the service instance.
- // REMOVED await this.saveData() from all these methods.
- async scheduleNoteForReview(path, daysFromNow = 0) {
- await this.reviewScheduleService.scheduleNoteForReview(path, daysFromNow);
- }
- async recordReview(path, response, isSkipped = false) {
- return await this.reviewScheduleService.recordReview(path, response, isSkipped);
- }
- calculateNewSchedule(currentInterval, currentEase, response) {
- const result = this.reviewScheduleService.calculateNewSchedule(currentInterval, currentEase, response);
- return { interval: result.interval, ease: result.ease };
- }
- async skipNote(path, response = 3 /* CorrectWithDifficulty */) {
- await this.reviewScheduleService.skipNote(path, response);
- }
- async postponeNote(path, days = 1) {
- await this.reviewScheduleService.postponeNote(path, days);
- }
- async removeFromReview(path) {
- await this.reviewScheduleService.removeFromReview(path);
- }
- async clearAllSchedules() {
- await this.reviewScheduleService.clearAllSchedules();
- }
- async estimateReviewTime(path) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!(file instanceof import_obsidian.TFile))
- return 60;
- try {
- const content = await this.plugin.app.vault.read(file);
- return EstimationUtils.estimateReviewTime(file, content);
- } catch (error) {
- return 60;
- }
- }
- async createReviewSession(folderPath, name) {
- const session = await this.reviewSessionService.createReviewSession(folderPath, name);
- return session;
- }
- async setActiveSession(sessionId) {
- const success = await this.reviewSessionService.setActiveSession(sessionId);
- return success;
- }
- getActiveSession() {
- return this.reviewSessionService.getActiveSession();
- }
- getNextSessionFile() {
- return this.reviewSessionService.getNextSessionFile();
- }
- async advanceActiveSession() {
- const moreFiles = await this.reviewSessionService.advanceActiveSession();
- return moreFiles;
- }
- async scheduleNotesInOrder(paths, daysFromNow = 0) {
- const count = await this.reviewScheduleService.scheduleNotesInOrder(paths, daysFromNow);
- return count;
- }
- async scheduleSessionForReview(sessionId) {
- return await this.reviewSessionService.scheduleSessionForReview(sessionId);
- }
- async saveMCQSet(mcqSet) {
- const id = this.mcqService.saveMCQSet(mcqSet);
- return id;
- }
- getMCQSetForNote(notePath) {
- return this.mcqService.getMCQSetForNote(notePath);
- }
- async saveMCQSession(session) {
- this.mcqService.saveMCQSession(session);
- }
- getMCQSessionsForNote(notePath) {
- return this.mcqService.getMCQSessionsForNote(notePath);
- }
- getLatestMCQSessionForNote(notePath) {
- return this.mcqService.getLatestMCQSessionForNote(notePath);
- }
- async updateCustomNoteOrder(order) {
- await this.reviewScheduleService.updateCustomNoteOrder(order);
- }
- getDueNotesWithCustomOrder(date = Date.now(), useCustomOrder = true) {
- return this.reviewScheduleService.getDueNotesWithCustomOrder(date, useCustomOrder);
- }
- // Removed internal helper methods that were moved to services
-};
-
-// controllers/review-controller-core.ts
-var import_obsidian3 = require("obsidian");
-
-// ui/review-modal.ts
-var import_obsidian2 = require("obsidian");
-
-// node_modules/ts-fsrs/dist/index.mjs
-var c = ((s) => (s[s.New = 0] = "New", s[s.Learning = 1] = "Learning", s[s.Review = 2] = "Review", s[s.Relearning = 3] = "Relearning", s))(c || {});
-var l = ((s) => (s[s.Manual = 0] = "Manual", s[s.Again = 1] = "Again", s[s.Hard = 2] = "Hard", s[s.Good = 3] = "Good", s[s.Easy = 4] = "Easy", s))(l || {});
-var h = class {
- static card(t) {
- return { ...t, state: h.state(t.state), due: h.time(t.due), last_review: t.last_review ? h.time(t.last_review) : void 0 };
- }
- static rating(t) {
- if (typeof t == "string") {
- const e = t.charAt(0).toUpperCase(), i = t.slice(1).toLowerCase(), r = l[`${e}${i}`];
- if (r === void 0)
- throw new Error(`Invalid rating:[${t}]`);
- return r;
- } else if (typeof t == "number")
- return t;
- throw new Error(`Invalid rating:[${t}]`);
- }
- static state(t) {
- if (typeof t == "string") {
- const e = t.charAt(0).toUpperCase(), i = t.slice(1).toLowerCase(), r = c[`${e}${i}`];
- if (r === void 0)
- throw new Error(`Invalid state:[${t}]`);
- return r;
- } else if (typeof t == "number")
- return t;
- throw new Error(`Invalid state:[${t}]`);
- }
- static time(t) {
- if (typeof t == "object" && t instanceof Date)
- return t;
- if (typeof t == "string") {
- const e = Date.parse(t);
- if (isNaN(e))
- throw new Error(`Invalid date:[${t}]`);
- return new Date(e);
- } else if (typeof t == "number")
- return new Date(t);
- throw new Error(`Invalid date:[${t}]`);
- }
- static review_log(t) {
- return { ...t, due: h.time(t.due), rating: h.rating(t.rating), state: h.state(t.state), review: h.time(t.review) };
- }
-};
-var X = "4.7.1";
-Date.prototype.scheduler = function(s, t) {
- return F(this, s, t);
-}, Date.prototype.diff = function(s, t) {
- return L(this, s, t);
-}, Date.prototype.format = function() {
- return O(this);
-}, Date.prototype.dueFormat = function(s, t, e) {
- return j(this, s, t, e);
-};
-function F(s, t, e) {
- return new Date(e ? h.time(s).getTime() + t * 24 * 60 * 60 * 1e3 : h.time(s).getTime() + t * 60 * 1e3);
-}
-function L(s, t, e) {
- if (!s || !t)
- throw new Error("Invalid date");
- const i = h.time(s).getTime() - h.time(t).getTime();
- let r = 0;
- switch (e) {
- case "days":
- r = Math.floor(i / (24 * 60 * 60 * 1e3));
- break;
- case "minutes":
- r = Math.floor(i / (60 * 1e3));
- break;
- }
- return r;
-}
-function O(s) {
- const t = h.time(s), e = t.getFullYear(), i = t.getMonth() + 1, r = t.getDate(), a = t.getHours(), n = t.getMinutes(), d = t.getSeconds();
- return `${e}-${p(i)}-${p(r)} ${p(a)}:${p(n)}:${p(d)}`;
-}
-function p(s) {
- return s < 10 ? `0${s}` : `${s}`;
-}
-var S = [60, 60, 24, 31, 12];
-var E = ["second", "min", "hour", "day", "month", "year"];
-function j(s, t, e, i = E) {
- s = h.time(s), t = h.time(t), i.length !== E.length && (i = E);
- let r = s.getTime() - t.getTime(), a;
- for (r /= 1e3, a = 0; a < S.length && !(r < S[a]); a++)
- r /= S[a];
- return `${Math.floor(r)}${e ? i[a] : ""}`;
-}
-var I = Object.freeze([l.Again, l.Hard, l.Good, l.Easy]);
-var Z = [{ start: 2.5, end: 7, factor: 0.15 }, { start: 7, end: 20, factor: 0.1 }, { start: 20, end: 1 / 0, factor: 0.05 }];
-function G(s, t, e) {
- let i = 1;
- for (const n of Z)
- i += n.factor * Math.max(Math.min(s, n.end) - n.start, 0);
- s = Math.min(s, e);
- let r = Math.max(2, Math.round(s - i));
- const a = Math.min(Math.round(s + i), e);
- return s > t && (r = Math.max(r, t + 1)), r = Math.min(r, a), { min_ivl: r, max_ivl: a };
-}
-function m(s, t, e) {
- return Math.min(Math.max(s, t), e);
-}
-function N(s, t) {
- const e = Date.UTC(s.getUTCFullYear(), s.getUTCMonth(), s.getUTCDate()), i = Date.UTC(t.getUTCFullYear(), t.getUTCMonth(), t.getUTCDate());
- return Math.floor((i - e) / 864e5);
-}
-var k = 0.9;
-var C = 36500;
-var T = Object.freeze([0.40255, 1.18385, 3.173, 15.69105, 7.1949, 0.5345, 1.4604, 46e-4, 1.54575, 0.1192, 1.01925, 1.9395, 0.11, 0.29605, 2.2698, 0.2315, 2.9898, 0.51655, 0.6621]);
-var U = false;
-var q = true;
-var tt = `v${X} using FSRS-5.0`;
-var _ = 0.01;
-var b = 100;
-var R = Object.freeze([Object.freeze([_, b]), Object.freeze([_, b]), Object.freeze([_, b]), Object.freeze([_, b]), Object.freeze([1, 10]), Object.freeze([1e-3, 4]), Object.freeze([1e-3, 4]), Object.freeze([1e-3, 0.75]), Object.freeze([0, 4.5]), Object.freeze([0, 0.8]), Object.freeze([1e-3, 3.5]), Object.freeze([1e-3, 5]), Object.freeze([1e-3, 0.25]), Object.freeze([1e-3, 0.9]), Object.freeze([0, 4]), Object.freeze([0, 1]), Object.freeze([1, 6]), Object.freeze([0, 2]), Object.freeze([0, 2])]);
-var z = (s) => {
- var _a, _b;
- let t = [...T];
- return (s == null ? void 0 : s.w) && (s.w.length === 19 ? t = [...s.w] : s.w.length === 17 && (t = s == null ? void 0 : s.w.concat([0, 0]), t[4] = +(t[5] * 2 + t[4]).toFixed(8), t[5] = +(Math.log(t[5] * 3 + 1) / 3).toFixed(8), t[6] = +(t[6] + 0.5).toFixed(8), console.debug("[FSRS V5]auto fill w to 19 length"))), t = t.map((e, i) => m(e, R[i][0], R[i][1])), { request_retention: (s == null ? void 0 : s.request_retention) || k, maximum_interval: (s == null ? void 0 : s.maximum_interval) || C, w: t, enable_fuzz: (_a = s == null ? void 0 : s.enable_fuzz) != null ? _a : U, enable_short_term: (_b = s == null ? void 0 : s.enable_short_term) != null ? _b : q };
-};
-function v(s, t) {
- const e = { due: s ? h.time(s) : /* @__PURE__ */ new Date(), stability: 0, difficulty: 0, elapsed_days: 0, scheduled_days: 0, reps: 0, lapses: 0, state: c.New, last_review: void 0 };
- return t && typeof t == "function" ? t(e) : e;
-}
-var et = class {
- constructor(t) {
- __publicField(this, "c");
- __publicField(this, "s0");
- __publicField(this, "s1");
- __publicField(this, "s2");
- const e = it();
- this.c = 1, this.s0 = e(" "), this.s1 = e(" "), this.s2 = e(" "), t == null && (t = +/* @__PURE__ */ new Date()), this.s0 -= e(t), this.s0 < 0 && (this.s0 += 1), this.s1 -= e(t), this.s1 < 0 && (this.s1 += 1), this.s2 -= e(t), this.s2 < 0 && (this.s2 += 1);
- }
- next() {
- const t = 2091639 * this.s0 + this.c * 23283064365386963e-26;
- return this.s0 = this.s1, this.s1 = this.s2, this.s2 = t - (this.c = t | 0), this.s2;
- }
- set state(t) {
- this.c = t.c, this.s0 = t.s0, this.s1 = t.s1, this.s2 = t.s2;
- }
- get state() {
- return { c: this.c, s0: this.s0, s1: this.s1, s2: this.s2 };
- }
-};
-function it() {
- let s = 4022871197;
- return function(t) {
- t = String(t);
- for (let e = 0; e < t.length; e++) {
- s += t.charCodeAt(e);
- let i = 0.02519603282416938 * s;
- s = i >>> 0, i -= s, i *= s, s = i >>> 0, i -= s, s += i * 4294967296;
- }
- return (s >>> 0) * 23283064365386963e-26;
- };
-}
-function rt(s) {
- const t = new et(s), e = () => t.next();
- return e.int32 = () => t.next() * 4294967296 | 0, e.double = () => e() + (e() * 2097152 | 0) * 11102230246251565e-32, e.state = () => t.state, e.importState = (i) => (t.state = i, e), e;
-}
-var $ = -0.5;
-var D = 19 / 81;
-function P(s, t) {
- return +Math.pow(1 + D * s / t, $).toFixed(8);
-}
-var Y = class {
- constructor(t) {
- __publicField(this, "param");
- __publicField(this, "intervalModifier");
- __publicField(this, "_seed");
- __publicField(this, "forgetting_curve", P);
- this.param = new Proxy(z(t), this.params_handler_proxy()), this.intervalModifier = this.calculate_interval_modifier(this.param.request_retention);
- }
- get interval_modifier() {
- return this.intervalModifier;
- }
- set seed(t) {
- this._seed = t;
- }
- calculate_interval_modifier(t) {
- if (t <= 0 || t > 1)
- throw new Error("Requested retention rate should be in the range (0,1]");
- return +((Math.pow(t, 1 / $) - 1) / D).toFixed(8);
- }
- get parameters() {
- return this.param;
- }
- set parameters(t) {
- this.update_parameters(t);
- }
- params_handler_proxy() {
- const t = this;
- return { set: function(e, i, r) {
- return i === "request_retention" && Number.isFinite(r) && (t.intervalModifier = t.calculate_interval_modifier(Number(r))), Reflect.set(e, i, r), true;
- } };
- }
- update_parameters(t) {
- const e = z(t);
- for (const i in e)
- if (i in this.param) {
- const r = i;
- this.param[r] = e[r];
- }
- }
- init_stability(t) {
- return Math.max(this.param.w[t - 1], 0.1);
- }
- init_difficulty(t) {
- return this.constrain_difficulty(this.param.w[4] - Math.exp((t - 1) * this.param.w[5]) + 1);
- }
- apply_fuzz(t, e) {
- if (!this.param.enable_fuzz || t < 2.5)
- return Math.round(t);
- const i = rt(this._seed)(), { min_ivl: r, max_ivl: a } = G(t, e, this.param.maximum_interval);
- return Math.floor(i * (a - r + 1) + r);
- }
- next_interval(t, e) {
- const i = Math.min(Math.max(1, Math.round(t * this.intervalModifier)), this.param.maximum_interval);
- return this.apply_fuzz(i, e);
- }
- linear_damping(t, e) {
- return +(t * (10 - e) / 9).toFixed(8);
- }
- next_difficulty(t, e) {
- const i = -this.param.w[6] * (e - 3), r = t + this.linear_damping(i, t);
- return this.constrain_difficulty(this.mean_reversion(this.init_difficulty(l.Easy), r));
- }
- constrain_difficulty(t) {
- return Math.min(Math.max(+t.toFixed(8), 1), 10);
- }
- mean_reversion(t, e) {
- return +(this.param.w[7] * t + (1 - this.param.w[7]) * e).toFixed(8);
- }
- next_recall_stability(t, e, i, r) {
- const a = l.Hard === r ? this.param.w[15] : 1, n = l.Easy === r ? this.param.w[16] : 1;
- return +m(e * (1 + Math.exp(this.param.w[8]) * (11 - t) * Math.pow(e, -this.param.w[9]) * (Math.exp((1 - i) * this.param.w[10]) - 1) * a * n), _, 36500).toFixed(8);
- }
- next_forget_stability(t, e, i) {
- return +m(this.param.w[11] * Math.pow(t, -this.param.w[12]) * (Math.pow(e + 1, this.param.w[13]) - 1) * Math.exp((1 - i) * this.param.w[14]), _, 36500).toFixed(8);
- }
- next_short_term_stability(t, e) {
- return +m(t * Math.exp(this.param.w[17] * (e - 3 + this.param.w[18])), _, 36500).toFixed(8);
- }
- next_state(t, e, i) {
- const { difficulty: r, stability: a } = t != null ? t : { difficulty: 0, stability: 0 };
- if (e < 0)
- throw new Error(`Invalid delta_t "${e}"`);
- if (i < 0 || i > 4)
- throw new Error(`Invalid grade "${i}"`);
- if (r === 0 && a === 0)
- return { difficulty: this.init_difficulty(i), stability: this.init_stability(i) };
- if (i === 0)
- return { difficulty: r, stability: a };
- if (r < 1 || a < _)
- throw new Error(`Invalid memory state { difficulty: ${r}, stability: ${a} }`);
- const n = this.forgetting_curve(e, a), d = this.next_recall_stability(r, a, n, i), u = this.next_forget_stability(r, a, n), o = this.next_short_term_stability(a, i);
- let f = d;
- if (i === 1) {
- let [y, w] = [0, 0];
- this.param.enable_short_term && (y = this.param.w[17], w = this.param.w[18]);
- const g = a / Math.exp(y * w);
- f = m(+g.toFixed(8), _, u);
- }
- return e === 0 && this.param.enable_short_term && (f = o), { difficulty: this.next_difficulty(r, i), stability: f };
- }
-};
-function H() {
- const s = this.review_time.getTime(), t = this.current.reps, e = this.current.difficulty * this.current.stability;
- return `${s}_${t}_${e}`;
-}
-var x = ((s) => (s.SCHEDULER = "Scheduler", s.SEED = "Seed", s))(x || {});
-var A = class {
- constructor(t, e, i, r = { seed: H }) {
- __publicField(this, "last");
- __publicField(this, "current");
- __publicField(this, "review_time");
- __publicField(this, "next", /* @__PURE__ */ new Map());
- __publicField(this, "algorithm");
- __publicField(this, "initSeedStrategy");
- this.algorithm = i, this.initSeedStrategy = r.seed.bind(this), this.last = h.card(t), this.current = h.card(t), this.review_time = h.time(e), this.init();
- }
- init() {
- const { state: t, last_review: e } = this.current;
- let i = 0;
- t !== c.New && e && (i = N(e, this.review_time)), this.current.last_review = this.review_time, this.current.elapsed_days = i, this.current.reps += 1, this.algorithm.seed = this.initSeedStrategy();
- }
- preview() {
- return { [l.Again]: this.review(l.Again), [l.Hard]: this.review(l.Hard), [l.Good]: this.review(l.Good), [l.Easy]: this.review(l.Easy), [Symbol.iterator]: this.previewIterator.bind(this) };
- }
- *previewIterator() {
- for (const t of I)
- yield this.review(t);
- }
- review(t) {
- const { state: e } = this.last;
- let i;
- switch (e) {
- case c.New:
- i = this.newState(t);
- break;
- case c.Learning:
- case c.Relearning:
- i = this.learningState(t);
- break;
- case c.Review:
- i = this.reviewState(t);
- break;
- }
- if (i)
- return i;
- throw new Error("Invalid grade");
- }
- buildLog(t) {
- const { last_review: e, due: i, elapsed_days: r } = this.last;
- return { rating: t, state: this.current.state, due: e || i, stability: this.current.stability, difficulty: this.current.difficulty, elapsed_days: this.current.elapsed_days, last_elapsed_days: r, scheduled_days: this.current.scheduled_days, review: this.review_time };
- }
-};
-var V = class extends A {
- newState(t) {
- const e = this.next.get(t);
- if (e)
- return e;
- const i = h.card(this.current);
- switch (i.difficulty = this.algorithm.init_difficulty(t), i.stability = this.algorithm.init_stability(t), t) {
- case l.Again:
- i.scheduled_days = 0, i.due = this.review_time.scheduler(1), i.state = c.Learning;
- break;
- case l.Hard:
- i.scheduled_days = 0, i.due = this.review_time.scheduler(5), i.state = c.Learning;
- break;
- case l.Good:
- i.scheduled_days = 0, i.due = this.review_time.scheduler(10), i.state = c.Learning;
- break;
- case l.Easy: {
- const a = this.algorithm.next_interval(i.stability, this.current.elapsed_days);
- i.scheduled_days = a, i.due = this.review_time.scheduler(a, true), i.state = c.Review;
- break;
- }
- default:
- throw new Error("Invalid grade");
- }
- const r = { card: i, log: this.buildLog(t) };
- return this.next.set(t, r), r;
- }
- learningState(t) {
- const e = this.next.get(t);
- if (e)
- return e;
- const { state: i, difficulty: r, stability: a } = this.last, n = h.card(this.current), d = this.current.elapsed_days;
- switch (n.difficulty = this.algorithm.next_difficulty(r, t), n.stability = this.algorithm.next_short_term_stability(a, t), t) {
- case l.Again: {
- n.scheduled_days = 0, n.due = this.review_time.scheduler(5, false), n.state = i;
- break;
- }
- case l.Hard: {
- n.scheduled_days = 0, n.due = this.review_time.scheduler(10), n.state = i;
- break;
- }
- case l.Good: {
- const o = this.algorithm.next_interval(n.stability, d);
- n.scheduled_days = o, n.due = this.review_time.scheduler(o, true), n.state = c.Review;
- break;
- }
- case l.Easy: {
- const o = this.algorithm.next_short_term_stability(a, l.Good), f = this.algorithm.next_interval(o, d), y = Math.max(this.algorithm.next_interval(n.stability, d), f + 1);
- n.scheduled_days = y, n.due = this.review_time.scheduler(y, true), n.state = c.Review;
- break;
- }
- default:
- throw new Error("Invalid grade");
- }
- const u = { card: n, log: this.buildLog(t) };
- return this.next.set(t, u), u;
- }
- reviewState(t) {
- const e = this.next.get(t);
- if (e)
- return e;
- const i = this.current.elapsed_days, { difficulty: r, stability: a } = this.last, n = this.algorithm.forgetting_curve(i, a), d = h.card(this.current), u = h.card(this.current), o = h.card(this.current), f = h.card(this.current);
- this.next_ds(d, u, o, f, r, a, n), this.next_interval(d, u, o, f, i), this.next_state(d, u, o, f), d.lapses += 1;
- const y = { card: d, log: this.buildLog(l.Again) }, w = { card: u, log: super.buildLog(l.Hard) }, g = { card: o, log: super.buildLog(l.Good) }, M = { card: f, log: super.buildLog(l.Easy) };
- return this.next.set(l.Again, y), this.next.set(l.Hard, w), this.next.set(l.Good, g), this.next.set(l.Easy, M), this.next.get(t);
- }
- next_ds(t, e, i, r, a, n, d) {
- t.difficulty = this.algorithm.next_difficulty(a, l.Again);
- const u = n / Math.exp(this.algorithm.parameters.w[17] * this.algorithm.parameters.w[18]), o = this.algorithm.next_forget_stability(a, n, d);
- t.stability = m(+u.toFixed(8), _, o), e.difficulty = this.algorithm.next_difficulty(a, l.Hard), e.stability = this.algorithm.next_recall_stability(a, n, d, l.Hard), i.difficulty = this.algorithm.next_difficulty(a, l.Good), i.stability = this.algorithm.next_recall_stability(a, n, d, l.Good), r.difficulty = this.algorithm.next_difficulty(a, l.Easy), r.stability = this.algorithm.next_recall_stability(a, n, d, l.Easy);
- }
- next_interval(t, e, i, r, a) {
- let n, d;
- n = this.algorithm.next_interval(e.stability, a), d = this.algorithm.next_interval(i.stability, a), n = Math.min(n, d), d = Math.max(d, n + 1);
- const u = Math.max(this.algorithm.next_interval(r.stability, a), d + 1);
- t.scheduled_days = 0, t.due = this.review_time.scheduler(5), e.scheduled_days = n, e.due = this.review_time.scheduler(n, true), i.scheduled_days = d, i.due = this.review_time.scheduler(d, true), r.scheduled_days = u, r.due = this.review_time.scheduler(u, true);
- }
- next_state(t, e, i, r) {
- t.state = c.Relearning, e.state = c.Review, i.state = c.Review, r.state = c.Review;
- }
-};
-var B = class extends A {
- newState(t) {
- const e = this.next.get(t);
- if (e)
- return e;
- this.current.scheduled_days = 0, this.current.elapsed_days = 0;
- const i = h.card(this.current), r = h.card(this.current), a = h.card(this.current), n = h.card(this.current);
- return this.init_ds(i, r, a, n), this.next_interval(i, r, a, n, 0), this.next_state(i, r, a, n), this.update_next(i, r, a, n), this.next.get(t);
- }
- init_ds(t, e, i, r) {
- t.difficulty = this.algorithm.init_difficulty(l.Again), t.stability = this.algorithm.init_stability(l.Again), e.difficulty = this.algorithm.init_difficulty(l.Hard), e.stability = this.algorithm.init_stability(l.Hard), i.difficulty = this.algorithm.init_difficulty(l.Good), i.stability = this.algorithm.init_stability(l.Good), r.difficulty = this.algorithm.init_difficulty(l.Easy), r.stability = this.algorithm.init_stability(l.Easy);
- }
- learningState(t) {
- return this.reviewState(t);
- }
- reviewState(t) {
- const e = this.next.get(t);
- if (e)
- return e;
- const i = this.current.elapsed_days, { difficulty: r, stability: a } = this.last, n = this.algorithm.forgetting_curve(i, a), d = h.card(this.current), u = h.card(this.current), o = h.card(this.current), f = h.card(this.current);
- return this.next_ds(d, u, o, f, r, a, n), this.next_interval(d, u, o, f, i), this.next_state(d, u, o, f), d.lapses += 1, this.update_next(d, u, o, f), this.next.get(t);
- }
- next_ds(t, e, i, r, a, n, d) {
- t.difficulty = this.algorithm.next_difficulty(a, l.Again);
- const u = this.algorithm.next_forget_stability(a, n, d);
- t.stability = m(n, _, u), e.difficulty = this.algorithm.next_difficulty(a, l.Hard), e.stability = this.algorithm.next_recall_stability(a, n, d, l.Hard), i.difficulty = this.algorithm.next_difficulty(a, l.Good), i.stability = this.algorithm.next_recall_stability(a, n, d, l.Good), r.difficulty = this.algorithm.next_difficulty(a, l.Easy), r.stability = this.algorithm.next_recall_stability(a, n, d, l.Easy);
- }
- next_interval(t, e, i, r, a) {
- let n, d, u, o;
- n = this.algorithm.next_interval(t.stability, a), d = this.algorithm.next_interval(e.stability, a), u = this.algorithm.next_interval(i.stability, a), o = this.algorithm.next_interval(r.stability, a), n = Math.min(n, d), d = Math.max(d, n + 1), u = Math.max(u, d + 1), o = Math.max(o, u + 1), t.scheduled_days = n, t.due = this.review_time.scheduler(n, true), e.scheduled_days = d, e.due = this.review_time.scheduler(d, true), i.scheduled_days = u, i.due = this.review_time.scheduler(u, true), r.scheduled_days = o, r.due = this.review_time.scheduler(o, true);
- }
- next_state(t, e, i, r) {
- t.state = c.Review, e.state = c.Review, i.state = c.Review, r.state = c.Review;
- }
- update_next(t, e, i, r) {
- const a = { card: t, log: this.buildLog(l.Again) }, n = { card: e, log: super.buildLog(l.Hard) }, d = { card: i, log: super.buildLog(l.Good) }, u = { card: r, log: super.buildLog(l.Easy) };
- this.next.set(l.Again, a), this.next.set(l.Hard, n), this.next.set(l.Good, d), this.next.set(l.Easy, u);
- }
-};
-var st = class {
- constructor(t) {
- __publicField(this, "fsrs");
- this.fsrs = t;
- }
- replay(t, e, i) {
- return this.fsrs.next(t, e, i);
- }
- handleManualRating(t, e, i, r, a, n, d) {
- if (typeof e > "u")
- throw new Error("reschedule: state is required for manual rating");
- let u, o;
- if (e === c.New)
- u = { rating: l.Manual, state: e, due: d != null ? d : i, stability: t.stability, difficulty: t.difficulty, elapsed_days: r, last_elapsed_days: t.elapsed_days, scheduled_days: t.scheduled_days, review: i }, o = v(i), o.last_review = i;
- else {
- if (typeof d > "u")
- throw new Error("reschedule: due is required for manual rating");
- const f = d.diff(i, "days");
- u = { rating: l.Manual, state: t.state, due: t.last_review || t.due, stability: t.stability, difficulty: t.difficulty, elapsed_days: r, last_elapsed_days: t.elapsed_days, scheduled_days: t.scheduled_days, review: i }, o = { ...t, state: e, due: d, last_review: i, stability: a || t.stability, difficulty: n || t.difficulty, elapsed_days: r, scheduled_days: f, reps: t.reps + 1 };
- }
- return { card: o, log: u };
- }
- reschedule(t, e) {
- const i = [];
- let r = v(t.due);
- for (const a of e) {
- let n;
- if (a.review = h.time(a.review), a.rating === l.Manual) {
- let d = 0;
- r.state !== c.New && r.last_review && (d = a.review.diff(r.last_review, "days")), n = this.handleManualRating(r, a.state, a.review, d, a.stability, a.difficulty, a.due ? h.time(a.due) : void 0);
- } else
- n = this.replay(r, a.review, a.rating);
- i.push(n), r = n.card;
- }
- return i;
- }
- calculateManualRecord(t, e, i, r) {
- if (!i)
- return null;
- const { card: a, log: n } = i, d = h.card(t);
- return d.due.getTime() === a.due.getTime() ? null : (d.scheduled_days = a.due.diff(d.due, "days"), this.handleManualRating(d, a.state, h.time(e), n.elapsed_days, r ? a.stability : void 0, r ? a.difficulty : void 0, a.due));
- }
-};
-var W = class extends Y {
- constructor(t) {
- super(t);
- __publicField(this, "strategyHandler", /* @__PURE__ */ new Map());
- __publicField(this, "Scheduler");
- const { enable_short_term: e } = this.parameters;
- this.Scheduler = e ? V : B;
- }
- params_handler_proxy() {
- const t = this;
- return { set: function(e, i, r) {
- return i === "request_retention" && Number.isFinite(r) ? t.intervalModifier = t.calculate_interval_modifier(Number(r)) : i === "enable_short_term" && (t.Scheduler = r === true ? V : B), Reflect.set(e, i, r), true;
- } };
- }
- useStrategy(t, e) {
- return this.strategyHandler.set(t, e), this;
- }
- clearStrategy(t) {
- return t ? this.strategyHandler.delete(t) : this.strategyHandler.clear(), this;
- }
- getScheduler(t, e) {
- const i = this.strategyHandler.get(x.SEED), r = this.strategyHandler.get(x.SCHEDULER) || this.Scheduler, a = i || H;
- return new r(t, e, this, { seed: a });
- }
- repeat(t, e, i) {
- const r = this.getScheduler(t, e).preview();
- return i && typeof i == "function" ? i(r) : r;
- }
- next(t, e, i, r) {
- const a = this.getScheduler(t, e), n = h.rating(i);
- if (n === l.Manual)
- throw new Error("Cannot review a manual rating");
- const d = a.review(n);
- return r && typeof r == "function" ? r(d) : d;
- }
- get_retrievability(t, e, i = true) {
- const r = h.card(t);
- e = e ? h.time(e) : /* @__PURE__ */ new Date();
- const a = r.state !== c.New ? Math.max(e.diff(r.last_review, "days"), 0) : 0, n = r.state !== c.New ? this.forgetting_curve(a, +r.stability.toFixed(8)) : 0;
- return i ? `${(n * 100).toFixed(2)}%` : n;
- }
- rollback(t, e, i) {
- const r = h.card(t), a = h.review_log(e);
- if (a.rating === l.Manual)
- throw new Error("Cannot rollback a manual rating");
- let n, d, u;
- switch (a.state) {
- case c.New:
- n = a.due, d = void 0, u = 0;
- break;
- case c.Learning:
- case c.Relearning:
- case c.Review:
- n = a.review, d = a.due, u = r.lapses - (a.rating === l.Again && a.state === c.Review ? 1 : 0);
- break;
- }
- const o = { ...r, due: n, stability: a.stability, difficulty: a.difficulty, elapsed_days: a.last_elapsed_days, scheduled_days: a.scheduled_days, reps: Math.max(0, r.reps - 1), lapses: Math.max(0, u), state: a.state, last_review: d };
- return i && typeof i == "function" ? i(o) : o;
- }
- forget(t, e, i = false, r) {
- const a = h.card(t);
- e = h.time(e);
- const n = a.state === c.New ? 0 : e.diff(a.last_review, "days"), d = { rating: l.Manual, state: a.state, due: a.due, stability: a.stability, difficulty: a.difficulty, elapsed_days: 0, last_elapsed_days: a.elapsed_days, scheduled_days: n, review: e }, u = { card: { ...a, due: e, stability: 0, difficulty: 0, elapsed_days: 0, scheduled_days: 0, reps: i ? 0 : a.reps, lapses: i ? 0 : a.lapses, state: c.New, last_review: a.last_review }, log: d };
- return r && typeof r == "function" ? r(u) : u;
- }
- reschedule(t, e = [], i = {}) {
- const { recordLogHandler: r, reviewsOrderBy: a, skipManual: n = true, now: d = /* @__PURE__ */ new Date(), update_memory_state: u = false } = i;
- a && typeof a == "function" && e.sort(a), n && (e = e.filter((M) => M.rating !== l.Manual));
- const o = new st(this), f = o.reschedule(i.first_card || v(), e), y = f.length, w = h.card(t), g = o.calculateManualRecord(w, d, y ? f[y - 1] : void 0, u);
- return r && typeof r == "function" ? { collections: f.map(r), reschedule_item: g ? r(g) : null } : { collections: f, reschedule_item: g };
- }
-};
-
-// ui/review-modal.ts
-var ReviewModal = class extends import_obsidian2.Modal {
- constructor(app, plugin, path) {
- super(app);
- this.plugin = plugin;
- this.path = path;
- }
- onOpen() {
- const { contentEl } = this;
- new import_obsidian2.Setting(contentEl).setName("Review Note").setHeading();
- const buttonsContainer = contentEl.createDiv("review-buttons-container");
- const schedule = this.plugin.reviewScheduleService.schedules[this.path];
- if (schedule && schedule.schedulingAlgorithm === "fsrs") {
- const createFsrsButton = (text, clsSuffix, rating) => {
- const button = buttonsContainer.createEl("button", { text, cls: `review-button review-button-fsrs-${clsSuffix}` });
- button.addEventListener("click", () => {
- this.plugin.reviewController.processReviewResponse(this.path, rating);
- this.close();
- });
- };
- createFsrsButton("1: Again", "again", 1 /* Again */);
- createFsrsButton("2: Hard", "hard", 2 /* Hard */);
- createFsrsButton("3: Good", "good", 3 /* Good */);
- createFsrsButton("4: Easy", "easy", 4 /* Easy */);
- } else {
- const createSm2Button = (text, cls, response) => {
- const button = buttonsContainer.createEl("button", { text, cls });
- button.addEventListener("click", () => {
- this.plugin.reviewController.processReviewResponse(this.path, response);
- this.close();
- });
- };
- createSm2Button("0: Complete Blackout", "review-button review-button-complete-blackout", 0 /* CompleteBlackout */);
- createSm2Button("1: Incorrect Response", "review-button review-button-incorrect", 1 /* IncorrectResponse */);
- createSm2Button("2: Incorrect but Familiar", "review-button review-button-incorrect-familiar", 2 /* IncorrectButFamiliar */);
- createSm2Button("3: Correct with Difficulty", "review-button review-button-correct-difficulty", 3 /* CorrectWithDifficulty */);
- createSm2Button("4: Correct with Hesitation", "review-button review-button-correct-hesitation", 4 /* CorrectWithHesitation */);
- createSm2Button("5: Perfect Recall", "review-button review-button-perfect-recall", 5 /* PerfectRecall */);
- }
- buttonsContainer.createEl("div", { cls: "review-button-separator" });
- const postponeButton = buttonsContainer.createEl("button", { text: "Postpone to Tomorrow", cls: "review-button review-button-postpone" });
- postponeButton.addEventListener("click", () => {
- this.plugin.reviewController.skipReview(this.path);
- this.close();
- });
- const skipButton = buttonsContainer.createEl("button", { text: "Skip/Next", cls: "review-button review-button-skip" });
- skipButton.addEventListener("click", async () => {
- this.close();
- if (this.plugin.navigationController) {
- await this.plugin.navigationController.navigateToNextNoteWithoutRating();
- }
- });
- 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");
- (0, import_obsidian2.setIcon)(mcqIconSpan, "mcq-quiz");
- const textSpan = mcqButton.createSpan("mcq-button-text");
- textSpan.setText("Test with MCQs");
- mcqButton.addEventListener("click", () => {
- const mcqController2 = this.plugin.mcqController;
- if (mcqController2) {
- mcqController2.startMCQReview(this.path);
- this.close();
- } else {
- this.plugin.initializeMCQComponents();
- const initializedMcqController = this.plugin.mcqController;
- if (initializedMcqController) {
- initializedMcqController.startMCQReview(this.path);
- this.close();
- } else {
- new import_obsidian2.Notice("MCQ feature could not be initialized. Please check settings.");
- }
- }
- });
- const mcqController = this.plugin.mcqController;
- 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");
- (0, import_obsidian2.setIcon)(refreshIconSpan, "refresh-cw");
- const refreshTextSpan = refreshMcqButton.createSpan("mcq-button-text");
- refreshTextSpan.setText("Generate New MCQs");
- refreshMcqButton.addEventListener("click", async () => {
- if (mcqController) {
- new import_obsidian2.Notice("Generating new MCQs...");
- const success = await mcqController.generateMCQs(this.path, true);
- if (success) {
- mcqController.startMCQReview(this.path);
- this.close();
- } else {
- new import_obsidian2.Notice("Failed to generate new MCQs");
- }
- }
- });
- }
- }
- const infoText = contentEl.createDiv("review-info-text");
- infoText.empty();
- if (schedule) {
- const file = this.app.vault.getAbstractFileByPath(this.path);
- const fileName = file instanceof import_obsidian2.TFile ? file.basename : this.path;
- infoText.createEl("p", { text: `Reviewing: ${fileName}` });
- const activeSession = this.plugin.reviewSessionService.getActiveSession();
- if (activeSession) {
- const currentIndex = activeSession.currentIndex;
- const totalFiles = activeSession.hierarchy.traversalOrder.length;
- infoText.createEl("p", { text: `Session: ${activeSession.name} (${currentIndex + 1}/${totalFiles})`, cls: "review-session-info" });
- }
- 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: `Stability: ${schedule.fsrsData.stability.toFixed(2)}` });
- infoText.createEl("p", { text: `Difficulty: ${schedule.fsrsData.difficulty.toFixed(2)}` });
- infoText.createEl("p", { text: `State: ${c[schedule.fsrsData.state]}` });
- infoText.createEl("p", { text: `Interval: ${schedule.fsrsData.scheduled_days} days` });
- } else {
- infoText.createEl("p", { text: `Algorithm: SM-2`, cls: "review-phase-sm2" });
- let phaseText;
- let phaseClass;
- if (schedule.scheduleCategory === "initial") {
- const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length;
- const currentStepDisplay = (schedule.reviewCount || 0) < totalInitialSteps ? (schedule.reviewCount || 0) + 1 : totalInitialSteps;
- phaseText = `Initial phase (${currentStepDisplay}/${totalInitialSteps})`;
- phaseClass = "review-phase-initial";
- } else if (schedule.scheduleCategory === "graduated") {
- phaseText = "Graduated (Spaced Repetition)";
- phaseClass = "review-phase-graduated";
- } else {
- phaseText = "Spaced Repetition";
- phaseClass = "review-phase-spaced";
- }
- infoText.createEl("p", { text: phaseText, cls: phaseClass });
- infoText.createEl("p", { text: `Current ease: ${schedule.ease}` });
- infoText.createEl("p", { text: `Current interval: ${schedule.interval} days` });
- }
- }
- }
- onClose() {
- const { contentEl } = this;
- contentEl.empty();
- }
-};
-
-// controllers/review-controller-core.ts
-var ReviewControllerCore = class {
- /**
- * Initialize review controller
- *
- * @param plugin Reference to the main plugin
- */
- constructor(plugin) {
- /**
- * Currently loaded notes due for review
- */
- this.todayNotes = [];
- /**
- * Current index in today's notes
- */
- this.currentNoteIndex = 0;
- /**
- * Cache of linked notes to improve performance
- */
- this.linkedNoteCache = /* @__PURE__ */ new Map();
- /**
- * Traversal order of notes for hierarchical navigation
- */
- this.traversalOrder = [];
- /**
- * Map of paths to their position in the traversal order
- * Used for fast lookups during navigation
- */
- this.traversalPositions = /* @__PURE__ */ new Map();
- /**
- * Optional override for the current date, for testing or reviewing past/future notes.
- * If null, Date.now() is used.
- */
- this.currentReviewDateOverride = null;
- this.plugin = plugin;
- this.updateTodayNotes();
- }
- /**
- * Sets an override for the current review date.
- * @param date Timestamp of the date to simulate, or null to use actual Date.now().
- */
- async setReviewDateOverride(date) {
- this.currentReviewDateOverride = date;
- await this.updateTodayNotes();
- }
- /**
- * Gets the effective review date (override or actual Date.now()).
- * @returns Timestamp for the effective review date.
- */
- getEffectiveReviewDate() {
- var _a;
- return (_a = this.currentReviewDateOverride) != null ? _a : Date.now();
- }
- /**
- * Gets the current review date override.
- * @returns Timestamp of the override, or null if no override is set.
- */
- getCurrentReviewDateOverride() {
- return this.currentReviewDateOverride;
- }
- /**
- * Get the currently loaded notes due for review
- */
- getTodayNotes() {
- return this.todayNotes;
- }
- /**
- * Get the current index in today's notes
- */
- getCurrentNoteIndex() {
- return this.currentNoteIndex;
- }
- /**
- * Set the current index in today's notes
- *
- * @param index The new index
- */
- setCurrentNoteIndex(index) {
- if (index >= 0 && index < this.todayNotes.length) {
- this.currentNoteIndex = index;
- } else {
- this.currentNoteIndex = Math.max(0, Math.min(index, this.todayNotes.length - 1));
- }
- }
- /**
- * Update the list of today's due notes
- *
- * @param preserveCurrentIndex Whether to try to preserve the current note index
- */
- async updateTodayNotes(preserveCurrentIndex = false) {
- let currentNotePath = null;
- if (preserveCurrentIndex && this.todayNotes.length > 0 && this.currentNoteIndex < this.todayNotes.length) {
- currentNotePath = this.todayNotes[this.currentNoteIndex].path;
- }
- this.linkedNoteCache.clear();
- this.traversalOrder = [];
- this.traversalPositions.clear();
- const originalTraversalOrder = [...this.traversalOrder];
- const originalPositions = new Map(this.traversalPositions);
- const effectiveDate = this.getEffectiveReviewDate();
- const matchExactDate = this.currentReviewDateOverride !== null;
- let newDueNotes;
- if (matchExactDate) {
- newDueNotes = this.plugin.dataStorage.reviewScheduleService.getDueNotesWithCustomOrder(effectiveDate, true, true);
- } else {
- const dueNotes = this.plugin.dataStorage.reviewScheduleService.getDueNotesWithCustomOrder(effectiveDate, true, false);
- const todayOnlyNotes = this.plugin.dataStorage.reviewScheduleService.getDueNotesWithCustomOrder(effectiveDate, true, true);
- const combinedNotes = [...dueNotes, ...todayOnlyNotes];
- const uniqueNotes = /* @__PURE__ */ new Map();
- for (const note of combinedNotes) {
- if (!uniqueNotes.has(note.path)) {
- uniqueNotes.set(note.path, note);
- }
- }
- newDueNotes = Array.from(uniqueNotes.values());
- }
- this.todayNotes = newDueNotes;
- this.traversalOrder = this.todayNotes.map((note) => note.path);
- this.traversalPositions = /* @__PURE__ */ new Map();
- this.traversalOrder.forEach((path, index) => {
- this.traversalPositions.set(path, index);
- });
- if (!preserveCurrentIndex) {
- this.currentNoteIndex = 0;
- }
- this.linkedNoteCache.clear();
- if (this.todayNotes.length === 0) {
- return;
- }
- if (preserveCurrentIndex && currentNotePath) {
- const newIndex = this.todayNotes.findIndex((note) => note.path === currentNotePath);
- if (newIndex !== -1) {
- this.currentNoteIndex = newIndex;
- } else {
- this.currentNoteIndex = 0;
- }
- this.currentNoteIndex = Math.min(Math.max(0, newIndex), this.todayNotes.length - 1);
- }
- }
- /**
- * Review the current note
- */
- async reviewCurrentNote() {
- if (this.todayNotes.length === 0) {
- await this.updateTodayNotes();
- if (this.todayNotes.length === 0) {
- new import_obsidian3.Notice("No notes due for review today!");
- return;
- }
- }
- const note = this.todayNotes[this.currentNoteIndex];
- await this.reviewNote(note.path);
- }
- /**
- * Start a review for a note
- *
- * @param path Path to the note file
- */
- async reviewNote(path) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!(file instanceof import_obsidian3.TFile)) {
- new import_obsidian3.Notice("Cannot review: file not found");
- return;
- }
- await this.plugin.app.workspace.getLeaf().openFile(file);
- this.showReviewModal(path);
- }
- /**
- * Postpone a note's review
- *
- * @param path Path to the note file
- * @param days Number of days to postpone (default: 1)
- */
- async postponeNote(path, days = 1) {
- var _a;
- await this.plugin.dataStorage.reviewScheduleService.postponeNote(path, days);
- await this.plugin.savePluginData();
- await this.handleNotePostponed(path);
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- }
- /**
- * Advance a note's review by one day, if eligible.
- *
- * @param path Path to the note file
- */
- async advanceNote(path) {
- var _a;
- const advanced = await this.plugin.dataStorage.reviewScheduleService.advanceNote(path);
- if (advanced) {
- await this.plugin.savePluginData();
- await this.handleNoteAdvanced(path);
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- new import_obsidian3.Notice(`Note advanced.`);
- } else {
- new import_obsidian3.Notice(`Note is not eligible to be advanced.`);
- }
- }
- /**
- * Handle a note being advanced, updating navigation state.
- * This primarily involves re-evaluating the todayNotes list.
- *
- * @param path Path to the advanced note
- */
- async handleNoteAdvanced(path) {
- await this.updateTodayNotes(true);
- }
- /**
- * Handle a note being postponed, updating navigation state
- *
- * @param path Path to the postponed note
- */
- async handleNotePostponed(path) {
- if (this.todayNotes.length === 0)
- return;
- const postponedIndex = this.todayNotes.findIndex((note) => note.path === path);
- if (postponedIndex === -1) {
- return;
- }
- const wasCurrentNote = postponedIndex === this.currentNoteIndex;
- const currentNotePath = this.currentNoteIndex < this.todayNotes.length ? this.todayNotes[this.currentNoteIndex].path : null;
- this.traversalOrder = this.traversalOrder.filter((p2) => p2 !== path);
- this.traversalPositions.delete(path);
- this.traversalOrder.forEach((p2, i) => this.traversalPositions.set(p2, i));
- this.todayNotes = this.todayNotes.filter((n) => n.path !== path);
- if (wasCurrentNote) {
- this.currentNoteIndex = Math.min(this.currentNoteIndex, this.todayNotes.length - 1);
- } else if (currentNotePath) {
- const newIndex = this.todayNotes.findIndex((n) => n.path === currentNotePath);
- this.currentNoteIndex = newIndex !== -1 ? newIndex : Math.min(this.currentNoteIndex, this.todayNotes.length - 1);
- }
- await this.plugin.dataStorage.reviewScheduleService.updateCustomNoteOrder(this.traversalOrder);
- if (wasCurrentNote) {
- this.currentNoteIndex = Math.min(this.currentNoteIndex, this.todayNotes.length - 1);
- } else if (currentNotePath) {
- const newIndex = this.todayNotes.findIndex((note) => note.path === currentNotePath);
- if (newIndex !== -1) {
- this.currentNoteIndex = newIndex;
- } else {
- this.currentNoteIndex = Math.min(this.currentNoteIndex, this.todayNotes.length - 1);
- }
- } else {
- if (this.currentNoteIndex >= this.todayNotes.length) {
- this.currentNoteIndex = Math.max(0, this.todayNotes.length - 1);
- }
- }
- }
- /**
- * Show the review modal for a note
- *
- * @param path Path to the note file
- */
- showReviewModal(path) {
- const modal = new ReviewModal(this.plugin.app, this.plugin, path);
- modal.open();
- }
- /**
- * Skip the review of a note and reschedule for tomorrow with penalty
- *
- * @param path Path to the note file
- */
- async skipReview(path) {
- var _a;
- const effectiveDate = this.getEffectiveReviewDate();
- await this.plugin.dataStorage.reviewScheduleService.skipNote(path, 3 /* CorrectWithDifficulty */, effectiveDate);
- await this.plugin.savePluginData();
- new import_obsidian3.Notice("Review postponed to tomorrow. Note will be easier to recover with a small penalty applied.");
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- await this.updateTodayNotes(true);
- if (this.todayNotes.length > 0) {
- this.currentNoteIndex = (this.currentNoteIndex + 1) % this.todayNotes.length;
- const currentPath = this.todayNotes[this.currentNoteIndex].path;
- if (currentPath === path) {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- return;
- }
- if (this.plugin.navigationController) {
- if (this.todayNotes.length > 0 && this.currentNoteIndex < this.todayNotes.length) {
- const nextNotePath = this.todayNotes[this.currentNoteIndex].path;
- await this.plugin.navigationController.openNoteWithoutReview(nextNotePath);
- this.showReviewModal(nextNotePath);
- } else {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- }
- } else {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- }
- } else {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- }
- }
- /**
- * Process a review response
- *
- * @param path Path to the note file
- * @param response User's response during review (SM-2 or FSRS)
- */
- async processReviewResponse(path, response) {
- var _a;
- const effectiveDate = this.getEffectiveReviewDate();
- const wasRecorded = await this.plugin.dataStorage.reviewScheduleService.recordReview(path, response, false, effectiveDate);
- if (!wasRecorded) {
- new import_obsidian3.Notice("Note previewed, not recorded");
- return;
- }
- const schedule = this.plugin.dataStorage.reviewScheduleService.schedules[path];
- let triggerRegeneration = false;
- if (schedule && this.plugin.settings.enableQuestionRegenerationOnRating && this.plugin.mcqService && typeof response === "number") {
- if (schedule.schedulingAlgorithm === "fsrs") {
- if (response >= this.plugin.settings.minFsrsRatingForQuestionRegeneration) {
- triggerRegeneration = true;
- }
- } else {
- if (response >= this.plugin.settings.minSm2RatingForQuestionRegeneration) {
- triggerRegeneration = true;
- }
- }
- }
- if (triggerRegeneration) {
- this.plugin.mcqService.flagMCQSetForRegeneration(path);
- }
- let responseText;
- if (schedule && schedule.schedulingAlgorithm === "fsrs") {
- switch (response) {
- case 1 /* Again */:
- responseText = "Again (1)";
- break;
- case 2 /* Hard */:
- responseText = "Hard (2)";
- break;
- case 3 /* Good */:
- responseText = "Good (3)";
- break;
- case 4 /* Easy */:
- responseText = "Easy (4)";
- break;
- default:
- responseText = "Unknown FSRS Rating";
- }
- } else {
- switch (response) {
- case 0 /* CompleteBlackout */:
- responseText = "Complete Blackout (0)";
- break;
- case 1 /* IncorrectResponse */:
- responseText = "Incorrect Response (1)";
- break;
- case 2 /* IncorrectButFamiliar */:
- responseText = "Incorrect but Familiar (2)";
- break;
- case 3 /* CorrectWithDifficulty */:
- responseText = "Correct with Difficulty (3)";
- break;
- case 4 /* CorrectWithHesitation */:
- responseText = "Correct with Hesitation (4)";
- break;
- case 5 /* PerfectRecall */:
- responseText = "Perfect Recall (5)";
- break;
- default:
- responseText = "Unknown SM-2 Rating";
- }
- }
- new import_obsidian3.Notice(`Note review recorded: ${responseText}`);
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- await this.updateTodayNotes(true);
- const activeSession = this.plugin.dataStorage.getActiveSession();
- if (activeSession) {
- await this.plugin.dataStorage.advanceActiveSession();
- const nextFilePath = this.plugin.dataStorage.getNextSessionFile();
- if (nextFilePath) {
- await this.reviewNote(nextFilePath);
- } else {
- new import_obsidian3.Notice("Hierarchical review session complete!");
- }
- } else if (this.todayNotes.length > 0) {
- this.currentNoteIndex = (this.currentNoteIndex + 1) % this.todayNotes.length;
- const currentPath = this.todayNotes[this.currentNoteIndex].path;
- if (currentPath === path) {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- return;
- }
- if (this.todayNotes.length > 0 && this.currentNoteIndex < this.todayNotes.length) {
- const nextNotePath = this.todayNotes[this.currentNoteIndex].path;
- if (this.plugin.navigationController) {
- await this.plugin.navigationController.openNoteWithoutReview(nextNotePath);
- this.showReviewModal(nextNotePath);
- } else {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- }
- } else {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- }
- } else {
- new import_obsidian3.Notice("All caught up! No more notes due for review.");
- }
- await this.plugin.savePluginData();
- }
-};
-
-// controllers/review-navigation-controller.ts
-var import_obsidian4 = require("obsidian");
-var ReviewNavigationController = class {
- /**
- * Initialize navigation controller
- *
- * @param plugin Reference to the main plugin
- */
- constructor(plugin) {
- this.plugin = plugin;
- }
- /**
- * Navigate to the current note without showing review modal
- */
- async navigateToCurrentNoteWithoutModal() {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const todayNotes = reviewController.getTodayNotes();
- const currentNoteIndex = reviewController.getCurrentNoteIndex();
- if (todayNotes.length === 0) {
- await reviewController.updateTodayNotes();
- if (todayNotes.length === 0) {
- new import_obsidian4.Notice("No notes due for review today!");
- return;
- }
- }
- const note = todayNotes[currentNoteIndex];
- await this.openNoteWithoutReview(note.path);
- }
- /**
- * Navigate to the next note following the current order
- */
- async navigateToNextNote() {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const todayNotes = reviewController.getTodayNotes();
- let currentNoteIndex = reviewController.getCurrentNoteIndex();
- if (todayNotes.length === 0) {
- await reviewController.updateTodayNotes(false);
- if (todayNotes.length === 0) {
- new import_obsidian4.Notice("No notes due for review today!");
- return;
- }
- }
- if (todayNotes.length === 1) {
- await this.navigateToCurrentNoteWithoutModal();
- return;
- }
- const nextIndex = (currentNoteIndex + 1) % todayNotes.length;
- const nextNote = todayNotes[nextIndex];
- const nextPath = nextNote.path;
- let messageType = "next note";
- const currentFile = this.plugin.app.vault.getAbstractFileByPath(todayNotes[currentNoteIndex].path);
- const nextFile = this.plugin.app.vault.getAbstractFileByPath(nextPath);
- if (currentFile instanceof import_obsidian4.TFile && nextFile instanceof import_obsidian4.TFile) {
- const currentFolder = currentFile.parent ? currentFile.parent.path : null;
- const nextFolder = nextFile.parent ? nextFile.parent.path : null;
- if (this.plugin.sessionController && this.plugin.sessionController.getDueLinkedNotes(todayNotes[currentNoteIndex].path).includes(nextPath)) {
- messageType = "linked note";
- } else if (currentFolder === nextFolder) {
- messageType = "next note in folder";
- } else {
- messageType = "next note in different folder";
- }
- }
- if (this.plugin.reviewController) {
- this.plugin.reviewController.setCurrentNoteIndex(nextIndex);
- }
- await this.openNoteWithoutReview(nextPath);
- if (this.plugin.settings.showNavigationNotifications) {
- new import_obsidian4.Notice(`Navigated to ${messageType} (${nextIndex + 1}/${todayNotes.length})`);
- }
- if (this.plugin.settings.enableNavigationCommands && this.plugin.settings.navigationCommand.key) {
- setTimeout(() => this.executeCommand(), this.plugin.settings.navigationCommandDelay);
- }
- }
- /**
- * Navigate to the next note without recording a review
- * Uses the same traversal logic as navigateToNextNote
- */
- async navigateToNextNoteWithoutRating() {
- await this.navigateToNextNote();
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const todayNotes = reviewController.getTodayNotes();
- const currentNoteIndex = reviewController.getCurrentNoteIndex();
- if (todayNotes.length > 0) {
- const nextNote = todayNotes[currentNoteIndex];
- reviewController.showReviewModal(nextNote.path);
- }
- }
- /**
- * Navigate to the previous note in the current order
- */
- async navigateToPreviousNote() {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const todayNotes = reviewController.getTodayNotes();
- let currentNoteIndex = reviewController.getCurrentNoteIndex();
- if (todayNotes.length === 0) {
- const hasCustomOrder = this.plugin.reviewScheduleService.customNoteOrder.length > 0;
- await reviewController.updateTodayNotes(hasCustomOrder);
- if (todayNotes.length === 0) {
- new import_obsidian4.Notice("No notes due for review today!");
- return;
- }
- }
- if (todayNotes.length === 1) {
- await this.navigateToCurrentNoteWithoutModal();
- return;
- }
- const prevIndex = (currentNoteIndex - 1 + todayNotes.length) % todayNotes.length;
- const prevNote = todayNotes[prevIndex];
- const prevPath = prevNote.path;
- if (this.plugin.reviewController) {
- this.plugin.reviewController.setCurrentNoteIndex(prevIndex);
- }
- await this.openNoteWithoutReview(prevPath);
- if (this.plugin.settings.showNavigationNotifications) {
- new import_obsidian4.Notice(`Navigated to previous note (${prevIndex + 1}/${todayNotes.length})`);
- }
- if (this.plugin.settings.enableNavigationCommands && this.plugin.settings.navigationCommand.key) {
- setTimeout(() => this.executeCommand(), this.plugin.settings.navigationCommandDelay);
- }
- }
- /**
- * Open a note without showing the review modal
- *
- * @param path Path to the note file
- */
- async openNoteWithoutReview(path) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!(file instanceof import_obsidian4.TFile)) {
- new import_obsidian4.Notice("Cannot navigate: file not found");
- return;
- }
- await this.plugin.app.workspace.getLeaf().openFile(file);
- }
- /**
- * Swap two notes in the traversal order
- *
- * @param path1 Path to the first note
- * @param path2 Path to the second note
- */
- async swapNotes(path1, path2) {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const todayNotes = reviewController.getTodayNotes();
- let currentNoteIndex = reviewController.getCurrentNoteIndex();
- const index1 = todayNotes.findIndex((n) => n.path === path1);
- const index2 = todayNotes.findIndex((n) => n.path === path2);
- if (index1 < 0 || index2 < 0)
- return;
- const newTodayNotes = [...todayNotes];
- const temp = newTodayNotes[index1];
- newTodayNotes[index1] = newTodayNotes[index2];
- newTodayNotes[index2] = temp;
- const newOrder = newTodayNotes.map((note) => note.path);
- await this.plugin.reviewScheduleService.updateCustomNoteOrder(newOrder);
- await this.plugin.savePluginData();
- await reviewController.updateTodayNotes(true);
- }
- executeCommand() {
- const command = this.plugin.settings.navigationCommand;
- if (!command || !command.key)
- return;
- const eventOptions = {
- key: command.key,
- ctrlKey: command.modifiers.includes("Ctrl"),
- altKey: command.modifiers.includes("Alt"),
- shiftKey: command.modifiers.includes("Shift"),
- metaKey: command.modifiers.includes("Meta"),
- bubbles: true,
- cancelable: true
- };
- const event = new KeyboardEvent("keydown", eventOptions);
- window.dispatchEvent(event);
- }
-};
-
-// controllers/review-batch-controller.ts
-var import_obsidian7 = require("obsidian");
-
-// ui/batch-review-modal.ts
-var import_obsidian6 = require("obsidian");
-
-// ui/consolidated-mcq-modal.ts
-var import_obsidian5 = require("obsidian");
-var ConsolidatedMCQModal = class extends import_obsidian5.Modal {
- /**
- * Initialize consolidated MCQ modal
- *
- * @param plugin Reference to the main plugin
- * @param mcqSets Collection of all MCQ sets
- * @param onComplete Callback for when review is completed
- */
- constructor(plugin, mcqSets, onComplete) {
- super(plugin.app);
- /**
- * All questions from all MCQ sets, flattened
- */
- this.allQuestions = [];
- /**
- * Current question index
- */
- this.currentQuestionIndex = 0;
- /**
- * User's answers
- */
- this.answers = [];
- /**
- * Start time for current question
- */
- this.questionStartTime = 0;
- this.plugin = plugin;
- this.mcqSets = mcqSets;
- this.onComplete = onComplete;
- for (const set of mcqSets) {
- set.mcqSet.questions.forEach((question, index) => {
- this.allQuestions.push({
- ...question,
- notePath: set.path,
- fileName: set.fileName,
- mcqSetId: `${set.path}_${set.mcqSet.generatedAt}`,
- originalIndex: index
- });
- });
- }
- }
- /**
- * Called when the modal is opened
- */
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- contentEl.addClass("spaceforge-mcq-modal");
- const headerContainer = contentEl.createDiv("mcq-header-container");
- new import_obsidian5.Setting(headerContainer).setName("Multiple Choice Review").setHeading();
- const progressEl = contentEl.createDiv("mcq-progress");
- const progressPercent = Math.round((this.currentQuestionIndex + 1) / this.allQuestions.length * 100);
- contentEl.setAttribute("data-progress", progressPercent.toString());
- progressEl.setText(`Question ${this.currentQuestionIndex + 1} of ${this.allQuestions.length}`);
- const progressCounter = contentEl.createDiv("mcq-progress-counter");
- progressCounter.setText(`${this.allQuestions.length} questions from ${this.mcqSets.length} notes`);
- const noteInfoEl = contentEl.createDiv("mcq-note-info");
- noteInfoEl.setText(`Question from: ${this.allQuestions[this.currentQuestionIndex].fileName}`);
- this.questionStartTime = Date.now();
- this.displayCurrentQuestion(contentEl);
- this.registerKeyboardShortcuts();
- }
- /**
- * Register keyboard shortcuts for answering questions
- */
- registerKeyboardShortcuts() {
- const keyDownHandler = (event) => {
- const questionIndex = this.currentQuestionIndex;
- if (questionIndex >= this.allQuestions.length)
- return;
- const question = this.allQuestions[questionIndex];
- if (!question || !question.choices)
- return;
- const num = parseInt(event.key);
- if (!isNaN(num) && num >= 1 && num <= question.choices.length) {
- this.handleAnswer(num - 1);
- return;
- }
- if (event.key.length === 1) {
- const letterCode = event.key.toUpperCase().charCodeAt(0);
- const index = letterCode - 65;
- if (index >= 0 && index < question.choices.length) {
- this.handleAnswer(index);
- return;
- }
- }
- };
- document.addEventListener("keydown", keyDownHandler);
- this.onClose = () => {
- document.removeEventListener("keydown", keyDownHandler);
- const { contentEl } = this;
- contentEl.empty();
- };
- }
- /**
- * Display the current question
- *
- * @param containerEl Container element
- */
- displayCurrentQuestion(containerEl) {
- const questionIndex = this.currentQuestionIndex;
- if (questionIndex >= this.allQuestions.length) {
- this.completeReview();
- return;
- }
- const question = this.allQuestions[questionIndex];
- if (!question || !question.choices || question.choices.length < 2) {
- new import_obsidian5.Notice("Error: Invalid question data. Moving to next question.");
- this.currentQuestionIndex++;
- if (this.currentQuestionIndex < this.allQuestions.length) {
- this.displayCurrentQuestion(containerEl);
- } else {
- this.completeReview();
- }
- return;
- }
- const questionContainer = containerEl.querySelector(".mcq-question-container");
- if (questionContainer) {
- questionContainer.remove();
- }
- const noteInfoEl = containerEl.querySelector(".mcq-note-info");
- if (noteInfoEl instanceof HTMLElement) {
- noteInfoEl.setText(`Question from: ${question.fileName}`);
- }
- const newQuestionContainer = containerEl.createDiv("mcq-question-container");
- const questionEl = newQuestionContainer.createDiv("mcq-question-text");
- questionEl.setText(question.question);
- const choicesContainer = newQuestionContainer.createDiv("mcq-choices-container");
- question.choices.forEach((choice, index) => {
- const choiceEl = choicesContainer.createDiv("mcq-choice");
- const choiceBtn = choiceEl.createEl("button", {
- cls: "mcq-choice-btn"
- });
- const letterLabel = choiceBtn.createSpan("mcq-choice-letter");
- letterLabel.setText(String.fromCharCode(65 + index) + ") ");
- const textSpan = choiceBtn.createSpan("mcq-choice-text");
- textSpan.setText(choice || "(Empty choice)");
- choiceBtn.addEventListener("click", () => {
- this.handleAnswer(index);
- });
- });
- }
- /**
- * Handle user's answer selection
- *
- * @param selectedIndex Index of the selected answer
- */
- handleAnswer(selectedIndex) {
- const questionIndex = this.currentQuestionIndex;
- const question = this.allQuestions[questionIndex];
- const isCorrect = selectedIndex === question.correctAnswerIndex;
- const timeToAnswer = (Date.now() - this.questionStartTime) / 1e3;
- const existingAnswerIndex = this.answers.findIndex(
- (a) => a.questionIndex === questionIndex
- );
- let answer;
- if (existingAnswerIndex >= 0) {
- answer = this.answers[existingAnswerIndex];
- answer.selectedAnswerIndex = selectedIndex;
- answer.correct = isCorrect;
- answer.timeToAnswer = timeToAnswer;
- answer.attempts += 1;
- } else {
- answer = {
- questionIndex,
- selectedAnswerIndex: selectedIndex,
- // Always record the selected index
- correct: isCorrect,
- timeToAnswer,
- attempts: 1,
- notePath: question.notePath,
- fileName: question.fileName
- };
- this.answers.push(answer);
- }
- this.highlightAnswer(selectedIndex, isCorrect);
- setTimeout(() => {
- if (isCorrect) {
- this.currentQuestionIndex++;
- this.questionStartTime = Date.now();
- const { contentEl } = this;
- 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)`;
- }
- const newProgressPercent = Math.round((this.currentQuestionIndex + 1) / this.allQuestions.length * 100);
- contentEl.setAttribute("data-progress", newProgressPercent.toString());
- if (this.currentQuestionIndex < this.allQuestions.length) {
- this.displayCurrentQuestion(contentEl);
- } else {
- this.completeReview();
- }
- } else {
- new import_obsidian5.Notice("Incorrect answer. Try again to proceed to the next question.");
- setTimeout(() => {
- const choiceButtons = document.querySelectorAll(".mcq-choice-btn");
- if (choiceButtons.length <= selectedIndex)
- return;
- const selectedBtn = choiceButtons[selectedIndex];
- selectedBtn.classList.remove("mcq-choice-incorrect");
- }, 500);
- }
- }, 1e3);
- }
- /**
- * Highlight the selected answer
- *
- * @param selectedIndex Index of the selected answer
- * @param isCorrect Whether the answer is correct
- */
- highlightAnswer(selectedIndex, isCorrect) {
- const choiceButtons = document.querySelectorAll(".mcq-choice-btn");
- if (choiceButtons.length <= selectedIndex)
- return;
- const selectedBtn = choiceButtons[selectedIndex];
- if (isCorrect) {
- selectedBtn.classList.add("mcq-choice-correct");
- } else {
- selectedBtn.classList.add("mcq-choice-incorrect");
- }
- }
- /**
- * Complete the review and show results
- */
- completeReview() {
- const noteScores = {};
- for (const answer of this.answers) {
- if (!noteScores[answer.notePath]) {
- noteScores[answer.notePath] = {
- totalQuestions: 0,
- correctAnswers: 0,
- score: 0,
- notePath: answer.notePath,
- fileName: answer.fileName
- };
- }
- }
- for (const question of this.allQuestions) {
- if (!noteScores[question.notePath]) {
- noteScores[question.notePath] = {
- totalQuestions: 0,
- correctAnswers: 0,
- score: 0,
- notePath: question.notePath,
- fileName: question.fileName
- // Assuming fileName is consistent for the notePath
- };
- }
- noteScores[question.notePath].totalQuestions++;
- }
- const finalAnswersCorrectCount = {};
- for (const question of this.allQuestions) {
- finalAnswersCorrectCount[question.notePath] = 0;
- }
- for (const answer of this.answers) {
- if (answer.correct && answer.attempts <= 1) {
- if (noteScores[answer.notePath]) {
- noteScores[answer.notePath].correctAnswers++;
- }
- }
- }
- for (const notePath in noteScores) {
- const noteScore = noteScores[notePath];
- if (noteScore.totalQuestions > 0) {
- noteScore.score = noteScore.correctAnswers / noteScore.totalQuestions;
- } else {
- noteScore.score = 0;
- }
- }
- const results = [];
- for (const notePath in noteScores) {
- const noteScore = noteScores[notePath];
- const score = noteScore.score;
- let success = false;
- let response = 1 /* Hard */;
- if (score >= 0.9) {
- success = true;
- response = 5 /* Perfect */;
- } else if (score >= 0.7) {
- success = true;
- response = 4 /* Good */;
- } else if (score >= 0.5) {
- success = true;
- response = 3 /* Fair */;
- } else {
- success = false;
- response = 1 /* Hard */;
- }
- results.push({
- path: notePath,
- success,
- response,
- score
- });
- }
- this.onComplete(results);
- const { contentEl } = this;
- contentEl.empty();
- const headerEl = new import_obsidian5.Setting(contentEl).setName("MCQ Review Complete").setHeading().setClass("mcq-review-complete-header");
- 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}%`);
- const performanceIndicator = scoreEl.createDiv("mcq-performance-indicator");
- if (scorePercentOverall >= 90) {
- performanceIndicator.setText("\u{1F393} Excellent Performance!");
- performanceIndicator.addClass("excellent");
- } else if (scorePercentOverall >= 70) {
- performanceIndicator.setText("\u{1F44D} Good Work!");
- performanceIndicator.addClass("good");
- } else if (scorePercentOverall >= 50) {
- performanceIndicator.setText("\u{1F504} Keep Practicing");
- performanceIndicator.addClass("needs-improvement");
- } else {
- performanceIndicator.setText("\u{1F4DA} More Review Recommended");
- performanceIndicator.addClass("review-recommended");
- }
- const statsEl = scoreEl.createDiv("mcq-stats-summary");
- statsEl.setText(`${totalCorrectOverall} correct out of ${totalQuestionsOverall} questions`);
- const noteScoresEl = contentEl.createDiv("mcq-note-scores");
- const scoreHeading = noteScoresEl.createEl("h3", { text: "Scores by Note", cls: "mcq-note-scores-heading" });
- const sortedNotes = Object.keys(noteScores).sort((a, b2) => noteScores[b2].score - noteScores[a].score);
- for (const notePath of sortedNotes) {
- const noteScore = noteScores[notePath];
- if (noteScore.totalQuestions === 0)
- continue;
- 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})`,
- cls: "mcq-note-score-value"
- });
- if (noteScore.score >= 0.7) {
- scoreTextValueEl.addClass("high-score");
- } else if (noteScore.score >= 0.5) {
- scoreTextValueEl.addClass("medium-score");
- } else {
- scoreTextValueEl.addClass("low-score");
- }
- const progressBar = noteScoreEl.createDiv("mcq-progress-bar");
- const progressFill = progressBar.createDiv("mcq-progress-fill");
- progressFill.style.width = `${scorePercent}%`;
- if (noteScore.score >= 0.7) {
- progressFill.addClass("high-score");
- } else if (noteScore.score >= 0.5) {
- progressFill.addClass("medium-score");
- } else {
- progressFill.addClass("low-score");
- }
- }
- const breakdownContainer = contentEl.createDiv("mcq-detailed-breakdown");
- breakdownContainer.createEl("h3", { text: "Detailed Question Breakdown" });
- this.allQuestions.forEach((question, index) => {
- const questionEl = breakdownContainer.createDiv("mcq-breakdown-item");
- const questionHeader = questionEl.createDiv();
- questionHeader.createSpan({ text: `Q${index + 1} (from ${question.fileName}): `, cls: "mcq-breakdown-q-header" });
- questionHeader.createSpan({ text: question.question });
- const userAnswer = this.answers.find((a) => a.questionIndex === index);
- const userAnswerTextEl = questionEl.createDiv("mcq-user-answer-text");
- let userAnswerDisplay = "Not answered";
- if (userAnswer && userAnswer.selectedAnswerIndex !== -1 && userAnswer.selectedAnswerIndex < question.choices.length) {
- userAnswerDisplay = question.choices[userAnswer.selectedAnswerIndex];
- } else if (userAnswer && userAnswer.selectedAnswerIndex === -1) {
- userAnswerDisplay = "Attempted, but no valid choice recorded";
- }
- if (userAnswer) {
- const correctnessText = userAnswer.correct ? " (Correct)" : " (Incorrect)";
- userAnswerTextEl.createSpan({ text: "Your answer: " });
- const userAnswerSpan = userAnswerTextEl.createSpan({ text: userAnswerDisplay });
- const correctnessSpan = userAnswerTextEl.createSpan({ text: correctnessText, cls: "mcq-correctness-indicator" });
- if (userAnswer.correct) {
- userAnswerSpan.addClass("correct");
- correctnessSpan.addClass("correct");
- } else {
- userAnswerSpan.addClass("incorrect");
- correctnessSpan.addClass("incorrect");
- }
- } else {
- userAnswerTextEl.createSpan({ text: "Your answer: " + userAnswerDisplay });
- userAnswerTextEl.style.fontStyle = "italic";
- }
- const correctAnswerEl = questionEl.createDiv("mcq-correct-answer");
- correctAnswerEl.createSpan({ text: "Correct answer: " });
- correctAnswerEl.createSpan({ text: question.choices[question.correctAnswerIndex], cls: "mcq-correct-answer-text" });
- });
- const closeBtn = contentEl.createEl("button", { cls: "mcq-close-btn", text: "Close" });
- closeBtn.addEventListener("click", () => {
- this.close();
- });
- }
- /**
- * Shuffle an array
- *
- * @param array Array to shuffle
- * @returns Shuffled array
- */
- shuffleArray(array) {
- const newArray = [...array];
- for (let i = newArray.length - 1; i > 0; i--) {
- const j2 = Math.floor(Math.random() * (i + 1));
- [newArray[i], newArray[j2]] = [newArray[j2], newArray[i]];
- }
- return newArray;
- }
-};
-
-// ui/batch-review-modal.ts
-var BatchReviewModal = class extends import_obsidian6.Modal {
- constructor(app, plugin, notes, useMCQ = false) {
- super(app);
- this.currentIndex = 0;
- this.results = [];
- this.started = false;
- this.allMCQSets = [];
- this.collectingMCQs = false;
- this.plugin = plugin;
- this.notes = notes;
- this.useMCQ = useMCQ;
- }
- async onOpen() {
- const { contentEl } = this;
- this.renderStartScreen(contentEl);
- }
- renderStartScreen(contentEl) {
- contentEl.empty();
- new import_obsidian6.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);
- const buttonsEl = contentEl.createDiv("batch-review-buttons");
- const startButton = buttonsEl.createEl("button", {
- text: this.useMCQ ? "Start MCQ Review" : "Start Manual Review",
- cls: "batch-review-start-button"
- });
- startButton.addEventListener("click", () => this.startBatchReview());
- const toggleMCQButton = buttonsEl.createEl("button", {
- text: this.useMCQ ? "Switch to Manual Review" : "Switch to MCQ Review",
- cls: "batch-review-toggle-button"
- });
- toggleMCQButton.addEventListener("click", () => {
- this.useMCQ = !this.useMCQ;
- this.renderStartScreen(contentEl);
- });
- if (this.useMCQ) {
- const regenerateButton = buttonsEl.createEl("button", {
- text: "Regenerate All MCQs",
- cls: "batch-review-regenerate-button"
- });
- regenerateButton.addEventListener("click", () => {
- this.close();
- if (this.plugin.batchController) {
- this.plugin.batchController.regenerateAllMCQs();
- }
- });
- }
- const cancelButton = buttonsEl.createEl("button", { text: "Cancel", cls: "batch-review-cancel-button" });
- cancelButton.addEventListener("click", () => this.close());
- }
- async estimateAndShowTime(containerEl) {
- let totalTime = 0;
- for (const note of this.notes) {
- totalTime += await this.plugin.dataStorage.estimateReviewTime(note.path);
- }
- if (this.useMCQ) {
- totalTime += this.notes.length * 15;
- }
- containerEl.createEl("p", { text: `Estimated time: ${EstimationUtils.formatTime(totalTime)}`, cls: "batch-review-time" });
- }
- async startBatchReview() {
- this.started = true;
- if (this.useMCQ) {
- this.collectingMCQs = true;
- await this.collectAllMCQs();
- if (this.allMCQSets.length > 0) {
- this.showConsolidatedMCQUI();
- } else {
- new import_obsidian6.Notice("Could not generate any MCQs. Falling back to manual review.");
- await this.processNextManual();
- }
- } else {
- await this.processNextManual();
- }
- }
- async collectAllMCQs() {
- const { contentEl } = this;
- contentEl.empty();
- new import_obsidian6.Setting(contentEl).setName("Collecting MCQs").setHeading();
- const progressEl = contentEl.createDiv("batch-review-progress");
- progressEl.createEl("p", { text: `Preparing MCQs for ${this.notes.length} notes...` });
- const progressBar = contentEl.createDiv("mcq-collection-progress sf-progress-bar-container-batch");
- const progressFill = progressBar.createDiv({ cls: "sf-progress-bar-fill-batch" });
- const statusEl = contentEl.createDiv("batch-review-status");
- this.allMCQSets = [];
- for (let i = 0; i < this.notes.length; i++) {
- const note = this.notes[i];
- const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
- const fileName = file instanceof import_obsidian6.TFile ? file.basename : note.path;
- progressFill.style.width = `${(i + 1) / this.notes.length * 100}%`;
- statusEl.setText(`Processing ${i + 1}/${this.notes.length}: ${fileName}`);
- await new Promise((resolve) => setTimeout(resolve, 10));
- let mcqSet = this.plugin.dataStorage.getMCQSetForNote(note.path);
- if (!mcqSet && this.plugin.mcqGenerationService && this.plugin.mcqController) {
- statusEl.setText(`Generating MCQs for ${i + 1}/${this.notes.length}: ${fileName}...`);
- try {
- mcqSet = await this.plugin.mcqController.generateMCQs(note.path);
- } catch (error) {
- statusEl.setText(`Error generating MCQs for ${fileName}`);
- await new Promise((resolve) => setTimeout(resolve, 1e3));
- }
- }
- if (mcqSet) {
- this.allMCQSets.push({ path: note.path, mcqSet, fileName });
- }
- }
- statusEl.setText(`Collected MCQs for ${this.allMCQSets.length}/${this.notes.length} notes`);
- await new Promise((resolve) => setTimeout(resolve, 1e3));
- this.collectingMCQs = false;
- }
- showConsolidatedMCQUI() {
- if (this.allMCQSets.length === 0) {
- this.showSummary();
- return;
- }
- if (this.plugin.mcqController) {
- try {
- this.close();
- const consolidatedModal = new ConsolidatedMCQModal(
- this.plugin,
- this.allMCQSets,
- async (results) => {
- this.results = results;
- await this.recordAllReviews(results);
- this.open();
- this.showSummary();
- }
- );
- consolidatedModal.open();
- } catch (error) {
- new import_obsidian6.Notice("Error showing MCQ review. Falling back to manual review.");
- this.open();
- this.processNextManual();
- }
- } else {
- new import_obsidian6.Notice("MCQ controller not available. Falling back to manual review.");
- this.open();
- this.processNextManual();
- }
- }
- async recordAllReviews(results) {
- for (const result of results) {
- await this.plugin.dataStorage.recordReview(result.path, result.response);
- }
- }
- // This method is likely unused now due to the consolidated modal approach, but kept for reference/potential fallback
- async processNextMCQ() {
- if (this.currentIndex >= this.notes.length) {
- this.showSummary();
- return;
- }
- const note = this.notes[this.currentIndex];
- const { contentEl } = this;
- contentEl.empty();
- new import_obsidian6.Setting(contentEl).setName("MCQ Review in Progress").setHeading();
- const progressEl = contentEl.createDiv("batch-review-progress");
- progressEl.createEl("p", { text: `Processing note ${this.currentIndex + 1}/${this.notes.length}` });
- const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
- const fileName = file instanceof import_obsidian6.TFile ? file.basename : note.path;
- progressEl.createEl("p", { text: `Current note: ${fileName}`, cls: "batch-review-current-note" });
- this.close();
- if (this.plugin.mcqController) {
- let mcqSet = this.plugin.dataStorage.getMCQSetForNote(note.path);
- if (!mcqSet && this.plugin.mcqGenerationService) {
- new import_obsidian6.Notice(`Generating MCQs for ${fileName}...`);
- mcqSet = await this.plugin.mcqController.generateMCQs(note.path);
- }
- if (mcqSet) {
- this.plugin.mcqController.startMCQReview(
- note.path
- /* Removed callback */
- );
- this.open();
- this.showSummary();
- } else {
- new import_obsidian6.Notice(`Couldn't generate MCQs for ${fileName}, falling back to manual review`);
- this.open();
- this.processNextManual();
- }
- } else {
- new import_obsidian6.Notice("MCQ controller not available, falling back to manual review");
- this.open();
- this.processNextManual();
- }
- }
- getLatestMCQScore(path) {
- const session = this.plugin.dataStorage.getLatestMCQSessionForNote(path);
- return session == null ? void 0 : session.score;
- }
- async processNextManual() {
- if (this.currentIndex >= this.notes.length) {
- this.showSummary();
- return;
- }
- const note = this.notes[this.currentIndex];
- const { contentEl } = this;
- contentEl.empty();
- new import_obsidian6.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);
- const fileName = file instanceof import_obsidian6.TFile ? file.basename : note.path;
- progressEl.createEl("p", { text: `Current note: ${fileName}`, cls: "batch-review-current-note" });
- if (this.plugin.navigationController) {
- await this.plugin.navigationController.openNoteWithoutReview(note.path);
- }
- 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, 0 /* CompleteBlackout */));
- const incorrectButton = buttonsContainer.createEl("button", { text: "1: Incorrect Response", cls: "review-button review-button-incorrect" });
- incorrectButton.addEventListener("click", () => this.recordManualResult(note.path, 1 /* 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, 2 /* 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, 3 /* 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, 4 /* CorrectWithHesitation */));
- const perfectRecallButton = buttonsContainer.createEl("button", { text: "5: Perfect Recall", cls: "review-button review-button-perfect-recall" });
- perfectRecallButton.addEventListener("click", () => this.recordManualResult(note.path, 5 /* PerfectRecall */));
- const skipButton = buttonsContainer.createEl("button", { text: "Skip", cls: "review-button review-button-skip" });
- skipButton.addEventListener("click", () => {
- this.currentIndex++;
- this.processNextManual();
- });
- }
- async recordManualResult(path, response) {
- this.results.push({ path, success: response >= 3 /* CorrectWithDifficulty */, response });
- const wasRecorded = await this.plugin.dataStorage.recordReview(path, response);
- if (!wasRecorded) {
- new import_obsidian6.Notice("Note previewed, not recorded");
- }
- this.currentIndex++;
- this.processNextManual();
- }
- showSummary() {
- const { contentEl } = this;
- contentEl.empty();
- new import_obsidian6.Setting(contentEl).setName("Batch Review Complete").setHeading();
- const statsEl = contentEl.createDiv("batch-review-summary-stats");
- const totalNotes = this.results.length;
- const successfulNotes = this.results.filter((r) => r.success).length;
- const successRate = totalNotes > 0 ? Math.round(successfulNotes / totalNotes * 100) : 0;
- 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");
- resultsEl.createEl("h3", { text: "Individual Results" });
- const resultsListEl = resultsEl.createDiv("batch-review-results-list");
- for (const result of this.results) {
- const resultItemEl = resultsListEl.createDiv("batch-review-result-item");
- const file = this.plugin.app.vault.getAbstractFileByPath(result.path);
- const fileName = file instanceof import_obsidian6.TFile ? file.basename : result.path;
- resultItemEl.createEl("div", { text: fileName, cls: "batch-review-result-filename" });
- let responseText;
- let responseClass;
- switch (result.response) {
- case 0 /* CompleteBlackout */:
- responseText = "Complete Blackout (0)";
- responseClass = "batch-review-complete-blackout";
- break;
- case 1 /* IncorrectResponse */:
- responseText = "Incorrect Response (1)";
- responseClass = "batch-review-incorrect";
- break;
- case 2 /* IncorrectButFamiliar */:
- responseText = "Incorrect but Familiar (2)";
- responseClass = "batch-review-incorrect-familiar";
- break;
- case 3 /* CorrectWithDifficulty */:
- responseText = "Correct with Difficulty (3)";
- responseClass = "batch-review-correct-difficulty";
- break;
- case 4 /* CorrectWithHesitation */:
- responseText = "Correct with Hesitation (4)";
- responseClass = "batch-review-correct-hesitation";
- break;
- case 5 /* PerfectRecall */:
- responseText = "Perfect Recall (5)";
- responseClass = "batch-review-perfect-recall";
- break;
- default:
- responseText = "Unknown";
- responseClass = "";
- }
- resultItemEl.createEl("div", { text: responseText, cls: `batch-review-result-response ${responseClass}` });
- if (result.score !== void 0) {
- resultItemEl.createEl("div", { text: `MCQ Score: ${Math.round(result.score * 100)}%`, cls: "batch-review-result-mcq-score" });
- }
- }
- const closeButton = contentEl.createEl("button", { text: "Close", cls: "batch-review-close-button" });
- closeButton.addEventListener("click", () => {
- var _a;
- this.close();
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- });
- }
- onClose() {
- const { contentEl } = this;
- contentEl.empty();
- if (this.started && this.currentIndex < this.notes.length && this.results.length > 0) {
- new import_obsidian6.Notice(`Batch review interrupted after ${this.results.length} notes`);
- }
- }
-};
-
-// controllers/review-batch-controller.ts
-var ReviewBatchController = class {
- /**
- * Initialize batch controller
- *
- * @param plugin Reference to the main plugin
- */
- constructor(plugin) {
- this.plugin = plugin;
- }
- /**
- * Start reviewing all of today's notes
- */
- async reviewAllTodaysNotes() {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- await reviewController.updateTodayNotes(true);
- const todayNotes = reviewController.getTodayNotes();
- if (todayNotes.length === 0) {
- new import_obsidian7.Notice("No notes due for review today!");
- return;
- }
- if (reviewController) {
- await reviewController.updateTodayNotes(false);
- const note = todayNotes[0];
- await reviewController.reviewNote(note.path);
- }
- new import_obsidian7.Notice(`Starting review of all ${todayNotes.length} notes due today`);
- }
- /**
- * Review a specific set of notes
- *
- * @param paths Array of note paths to review
- * @param useMCQ Whether to use MCQs for testing (default: false)
- */
- async reviewNotes(paths, useMCQ = false) {
- if (paths.length === 0) {
- new import_obsidian7.Notice("No notes selected for review.");
- return;
- }
- const notesToReview = this.plugin.dataStorage.getDueNotesWithCustomOrder().filter((note) => paths.includes(note.path));
- if (notesToReview.length === 0) {
- new import_obsidian7.Notice("Selected notes are not currently due for review.");
- return;
- }
- if (useMCQ) {
- new import_obsidian7.Notice(`Preparing MCQs for ${notesToReview.length} selected notes. This may take a moment...`);
- } else {
- new import_obsidian7.Notice(`Starting review of ${notesToReview.length} selected notes.`);
- }
- const modal = new BatchReviewModal(this.plugin.app, this.plugin, notesToReview, useMCQ);
- modal.open();
- }
- /**
- * Review all notes with MCQs in a batch
- *
- * @param useMCQ Whether to use MCQs for testing
- */
- async reviewAllNotesWithMCQ(useMCQ = true) {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const hasCustomOrder = this.plugin.reviewScheduleService.customNoteOrder.length > 0;
- await reviewController.updateTodayNotes(hasCustomOrder);
- const todayNotes = reviewController.getTodayNotes();
- if (todayNotes.length === 0) {
- new import_obsidian7.Notice("No notes due for review today!");
- return;
- }
- if (useMCQ) {
- new import_obsidian7.Notice("Preparing all MCQs. This may take a moment...");
- }
- const orderedNotes = [...todayNotes];
- const modal = new BatchReviewModal(this.plugin.app, this.plugin, orderedNotes, useMCQ);
- modal.open();
- }
- /**
- * Regenerate MCQs for all notes due today
- */
- async regenerateAllMCQs() {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- const hasCustomOrder = this.plugin.reviewScheduleService.customNoteOrder.length > 0;
- await reviewController.updateTodayNotes(hasCustomOrder);
- const todayNotes = reviewController.getTodayNotes();
- if (todayNotes.length === 0) {
- new import_obsidian7.Notice("No notes due for review today!");
- return;
- }
- if (!this.plugin.mcqController) {
- new import_obsidian7.Notice("MCQ controller not initialized. Please check MCQ settings.");
- return;
- }
- new import_obsidian7.Notice(`Regenerating MCQs for ${todayNotes.length} notes...`);
- let generatedCount = 0;
- for (const note of todayNotes) {
- const success = await this.plugin.mcqController.generateMCQs(note.path);
- if (success) {
- generatedCount++;
- }
- }
- new import_obsidian7.Notice(`Generated MCQs for ${generatedCount} out of ${todayNotes.length} notes`);
- this.reviewAllNotesWithMCQ(true);
- }
- /**
- * Postpone a specific set of notes
- *
- * @param paths Array of note paths to postpone
- * @param days Number of days to postpone (default: 1)
- */
- async postponeNotes(paths, days = 1) {
- var _a;
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- if (paths.length === 0) {
- new import_obsidian7.Notice("No notes selected to postpone.");
- return;
- }
- new import_obsidian7.Notice(`Postponing ${paths.length} notes by ${days} day(s)...`);
- for (const path of paths) {
- await this.plugin.dataStorage.postponeNote(path, days);
- }
- await this.plugin.savePluginData();
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- await reviewController.updateTodayNotes(true);
- new import_obsidian7.Notice(`Postponed ${paths.length} notes by ${days} day(s).`);
- }
- /**
- * Advance a specific set of notes by one day each, if eligible.
- *
- * @param paths Array of note paths to advance
- */
- async advanceNotes(paths) {
- var _a;
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- if (paths.length === 0) {
- new import_obsidian7.Notice("No notes selected to advance.");
- return;
- }
- let advancedCount = 0;
- for (const path of paths) {
- const advanced = await this.plugin.dataStorage.reviewScheduleService.advanceNote(path);
- if (advanced) {
- advancedCount++;
- }
- }
- if (advancedCount > 0) {
- await this.plugin.savePluginData();
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- await reviewController.updateTodayNotes(true);
- new import_obsidian7.Notice(`Advanced ${advancedCount} note(s).`);
- } else {
- new import_obsidian7.Notice("No eligible notes were advanced.");
- }
- }
- /**
- * Remove a specific set of notes from the review schedule
- *
- * @param paths Array of note paths to remove
- */
- async removeNotes(paths) {
- var _a;
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return;
- if (paths.length === 0) {
- new import_obsidian7.Notice("No notes selected to remove.");
- return;
- }
- new import_obsidian7.Notice(`Removing ${paths.length} notes from review schedule...`);
- for (const path of paths) {
- await this.plugin.dataStorage.removeFromReview(path);
- }
- await this.plugin.savePluginData();
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- await reviewController.updateTodayNotes(true);
- new import_obsidian7.Notice(`Removed ${paths.length} notes from review schedule.`);
- }
-};
-
-// controllers/review-controller-mcq.ts
-var import_obsidian9 = require("obsidian");
-
-// ui/mcq-modal.ts
-var import_obsidian8 = require("obsidian");
-var MCQModal = class extends import_obsidian8.Modal {
- constructor(plugin, notePath, mcqSet, onCompleteCallback = null) {
- super(plugin.app);
- this.questionStartTime = 0;
- this.isFreshGeneration = false;
- this.selectedAnswerIndex = -1;
- this.plugin = plugin;
- this.notePath = notePath;
- this.mcqSet = mcqSet;
- this.onCompleteCallback = onCompleteCallback;
- this.isFreshGeneration = mcqSet.generatedAt > Date.now() - 6e4;
- this.session = {
- mcqSetId: `${mcqSet.notePath}_${mcqSet.generatedAt}`,
- notePath,
- answers: [],
- score: 0,
- currentQuestionIndex: 0,
- completed: false,
- startedAt: Date.now(),
- completedAt: null
- };
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- contentEl.addClass("spaceforge-mcq-modal");
- const headerContainer = contentEl.createDiv("mcq-header-container");
- new import_obsidian8.Setting(headerContainer).setName("Multiple Choice Review").setHeading();
- if (!this.isFreshGeneration) {
- const refreshBtn = headerContainer.createDiv("mcq-refresh-btn");
- (0, import_obsidian8.setIcon)(refreshBtn, "refresh-cw");
- refreshBtn.setAttribute("aria-label", "Generate new questions");
- refreshBtn.addEventListener("click", async () => {
- this.close();
- const mcqGenerationService = this.plugin.mcqGenerationService;
- if (mcqGenerationService && this.plugin.mcqController) {
- const file = this.plugin.app.vault.getAbstractFileByPath(this.notePath);
- if (file instanceof import_obsidian8.TFile) {
- const content = await this.plugin.app.vault.read(file);
- const newMcqSet = await mcqGenerationService.generateMCQs(this.notePath, content, this.plugin.settings);
- if (newMcqSet) {
- this.plugin.mcqService.saveMCQSet(newMcqSet);
- await this.plugin.savePluginData();
- const newModal = new MCQModal(this.plugin, this.notePath, newMcqSet, this.onCompleteCallback);
- newModal.open();
- } else {
- new import_obsidian8.Notice("Failed to regenerate MCQs.");
- }
- } else {
- new import_obsidian8.Notice("Could not find note file to regenerate MCQs.");
- }
- } else {
- new import_obsidian8.Notice("MCQ generation service not available.");
- }
- });
- }
- const questionIndex = this.session.currentQuestionIndex;
- const existingAnswer = this.session.answers.find((a) => a.questionIndex === questionIndex);
- const progressEl = contentEl.createDiv("mcq-progress");
- const progressPercent = Math.round((questionIndex + 1) / this.mcqSet.questions.length * 100);
- contentEl.setAttribute("data-progress", progressPercent.toString());
- if (existingAnswer) {
- progressEl.setText(`Question ${questionIndex + 1} of ${this.mcqSet.questions.length} (Attempt ${existingAnswer.attempts + 1})`);
- if (existingAnswer.attempts === 1) {
- const warningEl = contentEl.createDiv("mcq-attempt-warning");
- const warningIcon = warningEl.createSpan();
- warningIcon.setText("\u26A0\uFE0F ");
- const warningText = warningEl.createSpan();
- warningText.setText("This is your last attempt before scoring 0 points for this question.");
- warningEl.addClass("mcq-warning");
- }
- } else {
- progressEl.setText(`Question ${questionIndex + 1} of ${this.mcqSet.questions.length}`);
- }
- this.questionStartTime = Date.now();
- this.displayCurrentQuestion(contentEl);
- this.registerKeyboardShortcuts();
- }
- registerKeyboardShortcuts() {
- const keyDownHandler = (event) => {
- if (this.session.completed)
- return;
- const questionIndex = this.session.currentQuestionIndex;
- if (questionIndex >= this.mcqSet.questions.length)
- return;
- const question = this.mcqSet.questions[questionIndex];
- if (!question || !question.choices)
- return;
- const choiceButtons = this.contentEl.querySelectorAll(".mcq-choice-btn");
- if (choiceButtons.length === 0)
- return;
- if (this.selectedAnswerIndex !== -1 && this.selectedAnswerIndex < choiceButtons.length) {
- choiceButtons[this.selectedAnswerIndex].classList.remove("mcq-choice-selected");
- }
- let processAnswer = false;
- if (event.key === "ArrowDown") {
- event.preventDefault();
- this.selectedAnswerIndex = (this.selectedAnswerIndex + 1) % question.choices.length;
- } else if (event.key === "ArrowUp") {
- event.preventDefault();
- this.selectedAnswerIndex = (this.selectedAnswerIndex - 1 + question.choices.length) % question.choices.length;
- } else if (event.key === "ArrowRight" || event.key === "Enter") {
- event.preventDefault();
- if (this.selectedAnswerIndex !== -1)
- processAnswer = true;
- }
- const num = parseInt(event.key);
- if (!isNaN(num) && num >= 1 && num <= question.choices.length) {
- this.selectedAnswerIndex = num - 1;
- processAnswer = true;
- } else if (event.key.length === 1) {
- const letterCode = event.key.toUpperCase().charCodeAt(0);
- const index = letterCode - 65;
- if (index >= 0 && index < question.choices.length) {
- this.selectedAnswerIndex = index;
- processAnswer = true;
- }
- }
- if (this.selectedAnswerIndex !== -1 && this.selectedAnswerIndex < choiceButtons.length) {
- choiceButtons[this.selectedAnswerIndex].classList.add("mcq-choice-selected");
- }
- if (processAnswer && this.selectedAnswerIndex !== -1) {
- if (this.selectedAnswerIndex < choiceButtons.length) {
- const button = choiceButtons[this.selectedAnswerIndex];
- button.classList.add("mcq-key-pressed");
- setTimeout(() => {
- button.classList.remove("mcq-key-pressed");
- this.handleAnswer(this.selectedAnswerIndex);
- this.selectedAnswerIndex = -1;
- }, 150);
- } else {
- this.handleAnswer(this.selectedAnswerIndex);
- this.selectedAnswerIndex = -1;
- }
- }
- };
- this.scope.register([], "ArrowDown", keyDownHandler);
- this.scope.register([], "ArrowUp", keyDownHandler);
- this.scope.register([], "ArrowRight", keyDownHandler);
- this.scope.register([], "Enter", keyDownHandler);
- for (let i = 1; i <= 9; i++) {
- this.scope.register([], i.toString(), keyDownHandler);
- }
- for (let i = 0; i < 9; i++) {
- this.scope.register([], String.fromCharCode(65 + i), keyDownHandler);
- }
- const originalOnClose = this.onClose;
- this.onClose = () => {
- originalOnClose.call(this);
- if (!this.session.completed && this.session.answers.length > 0) {
- this.session.completedAt = Date.now();
- this.calculateScore();
- this.plugin.mcqService.saveMCQSession(this.session);
- this.plugin.savePluginData();
- if (this.onCompleteCallback) {
- this.onCompleteCallback(this.notePath, this.session.score, false);
- }
- } else if (!this.session.completed && this.onCompleteCallback) {
- this.onCompleteCallback(this.notePath, 0, false);
- }
- };
- }
- displayCurrentQuestion(containerEl) {
- const questionIndex = this.session.currentQuestionIndex;
- if (questionIndex >= this.mcqSet.questions.length) {
- this.completeSession();
- return;
- }
- const question = this.mcqSet.questions[questionIndex];
- if (!question || !question.choices || question.choices.length < 2) {
- new import_obsidian8.Notice("Error: Invalid question data. Moving to next question.");
- this.session.currentQuestionIndex++;
- if (this.session.currentQuestionIndex < this.mcqSet.questions.length) {
- this.displayCurrentQuestion(containerEl);
- } else {
- this.completeSession();
- }
- return;
- }
- const existingQuestionContainer = containerEl.querySelector(".mcq-question-container");
- if (existingQuestionContainer)
- existingQuestionContainer.remove();
- const newQuestionContainer = containerEl.createDiv("mcq-question-container");
- const questionEl = newQuestionContainer.createDiv("mcq-question-text");
- questionEl.setText(question.question);
- 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" });
- 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:");
- const correctText = correctAnswerDisplay.createDiv({ cls: "sf-mcq-correct-text" });
- correctText.setText(String.fromCharCode(65 + correctIndex) + ") " + question.choices[correctIndex]);
- if (existingAnswer) {
- existingAnswer.selectedAnswerIndex = -1;
- existingAnswer.correct = false;
- existingAnswer.attempts += 1;
- } else {
- this.session.answers.push({
- questionIndex,
- selectedAnswerIndex: -1,
- correct: false,
- timeToAnswer: (Date.now() - this.questionStartTime) / 1e3,
- attempts: 3
- });
- }
- const continueBtn = correctAnswerDisplay.createEl("button", {
- text: "Continue to Next Question",
- cls: "mcq-continue-button"
- });
- continueBtn.addEventListener("click", () => {
- this.session.currentQuestionIndex++;
- this.questionStartTime = Date.now();
- const { contentEl } = this;
- const progressEl = contentEl.querySelector(".mcq-progress");
- if (progressEl instanceof HTMLElement)
- progressEl.textContent = `Question ${this.session.currentQuestionIndex + 1} of ${this.mcqSet.questions.length}`;
- if (this.session.currentQuestionIndex < this.mcqSet.questions.length) {
- this.displayCurrentQuestion(contentEl);
- } else {
- this.completeSession();
- }
- });
- const choicesContainer2 = this.contentEl.querySelector(".mcq-choices-container");
- if (choicesContainer2 instanceof HTMLElement)
- choicesContainer2.style.display = "none";
- skipButton.style.display = "none";
- });
- }
- const choicesContainer = newQuestionContainer.createDiv("mcq-choices-container");
- question.choices.forEach((choice, index) => {
- const choiceEl = choicesContainer.createDiv("mcq-choice");
- const choiceBtn = choiceEl.createEl("button", { cls: "mcq-choice-btn" });
- const letterLabel = choiceBtn.createSpan("mcq-choice-letter");
- letterLabel.setText(String.fromCharCode(65 + index) + ") ");
- const textSpan = choiceBtn.createSpan("mcq-choice-text");
- textSpan.setText(choice || "(Empty choice)");
- const shortcutHint = choiceBtn.createSpan("mcq-shortcut-hint");
- shortcutHint.setText(`${String.fromCharCode(65 + index)} or ${index + 1}`);
- choiceBtn.addEventListener("click", () => this.handleAnswer(index));
- });
- this.selectedAnswerIndex = -1;
- }
- handleAnswer(selectedIndex) {
- const questionIndex = this.session.currentQuestionIndex;
- const question = this.mcqSet.questions[questionIndex];
- const isCorrect = selectedIndex === question.correctAnswerIndex;
- const timeToAnswer = (Date.now() - this.questionStartTime) / 1e3;
- const existingAnswerIndex = this.session.answers.findIndex((a) => a.questionIndex === questionIndex);
- let answer;
- if (existingAnswerIndex >= 0) {
- answer = this.session.answers[existingAnswerIndex];
- if (isCorrect) {
- answer.selectedAnswerIndex = selectedIndex;
- answer.correct = true;
- }
- answer.timeToAnswer = timeToAnswer;
- answer.attempts += 1;
- if (answer.attempts >= 2 && !answer.correct) {
- answer.selectedAnswerIndex = -1;
- }
- } else {
- answer = { questionIndex, selectedAnswerIndex: isCorrect ? selectedIndex : -1, correct: isCorrect, timeToAnswer, attempts: 1 };
- this.session.answers.push(answer);
- }
- this.highlightAnswer(selectedIndex, isCorrect);
- setTimeout(() => {
- if (isCorrect) {
- this.session.currentQuestionIndex++;
- this.questionStartTime = Date.now();
- const { contentEl } = this;
- const progressEl = contentEl.querySelector(".mcq-progress");
- if (progressEl instanceof HTMLElement)
- progressEl.textContent = `Question ${this.session.currentQuestionIndex + 1} of ${this.mcqSet.questions.length}`;
- const newProgressPercent = Math.round((this.session.currentQuestionIndex + 1) / this.mcqSet.questions.length * 100);
- contentEl.setAttribute("data-progress", newProgressPercent.toString());
- if (this.session.currentQuestionIndex < this.mcqSet.questions.length) {
- this.displayCurrentQuestion(contentEl);
- } else {
- this.completeSession();
- }
- }
- }, 1e3);
- }
- highlightAnswer(selectedIndex, isCorrect) {
- const choiceButtons = this.contentEl.querySelectorAll(".mcq-choice-btn");
- choiceButtons.forEach((button) => button.classList.remove("mcq-choice-correct", "mcq-choice-incorrect", "mcq-choice-selected"));
- if (selectedIndex < choiceButtons.length) {
- const selectedBtn = choiceButtons[selectedIndex];
- selectedBtn.classList.add(isCorrect ? "mcq-choice-correct" : "mcq-choice-incorrect");
- }
- }
- completeSession() {
- const { contentEl } = this;
- contentEl.empty();
- try {
- this.calculateScore();
- this.session.completed = true;
- this.session.completedAt = Date.now();
- this.plugin.mcqService.saveMCQSession(this.session);
- this.plugin.savePluginData();
- new import_obsidian8.Setting(contentEl).setName("Review Complete").setHeading();
- const scoreEl = contentEl.createDiv("mcq-score");
- const scoreTextEl = scoreEl.createDiv("mcq-score-text");
- const scorePercentage = this.session.score;
- const reviewSchedule = this.plugin.reviewScheduleService.schedules[this.notePath];
- let ratingText = "";
- let ratingDetails = "";
- if ((reviewSchedule == null ? void 0 : reviewSchedule.schedulingAlgorithm) === "fsrs") {
- let fsrsRating = 1;
- if (scorePercentage === 1)
- fsrsRating = 4;
- else if (scorePercentage >= 0.75)
- fsrsRating = 3;
- else if (scorePercentage >= 0.5)
- fsrsRating = 2;
- ratingText = `FSRS Rating: ${getFsrsRatingText(fsrsRating)} (${fsrsRating}/4)`;
- if (reviewSchedule.fsrsData) {
- const fsrs = reviewSchedule.fsrsData;
- ratingDetails += `Stability: ${fsrs.stability.toFixed(2)}, Difficulty: ${fsrs.difficulty.toFixed(2)}, Interval: ${fsrs.scheduled_days}d, State: ${mapFsrsStateToString(fsrs.state)}, Reps: ${fsrs.reps}, Lapses: ${fsrs.lapses}`;
- if (fsrs.last_review) {
- const nextDueDate = new Date(fsrs.last_review);
- nextDueDate.setDate(nextDueDate.getDate() + fsrs.scheduled_days);
- ratingDetails += `, Next Due: ${nextDueDate.toLocaleDateString()}`;
- }
- }
- } else if ((reviewSchedule == null ? void 0 : reviewSchedule.schedulingAlgorithm) === "sm2") {
- let sm2Rating = 0;
- if (scorePercentage >= 0.9)
- sm2Rating = 5;
- else if (scorePercentage >= 0.8)
- sm2Rating = 4;
- else if (scorePercentage >= 0.6)
- sm2Rating = 3;
- else if (scorePercentage >= 0.4)
- sm2Rating = 2;
- else if (scorePercentage >= 0.2)
- sm2Rating = 1;
- ratingText = `SM-2 Rating: ${getSm2RatingText(sm2Rating)} (${sm2Rating}/5)`;
- if (reviewSchedule) {
- ratingDetails += `Ease: ${(reviewSchedule.ease / 100).toFixed(2)}, Interval: ${reviewSchedule.interval}d, Next Due: ${new Date(reviewSchedule.nextReviewDate).toLocaleDateString()}`;
- if (reviewSchedule.repetitionCount !== void 0)
- ratingDetails += `, Reps: ${reviewSchedule.repetitionCount}`;
- }
- } else {
- ratingText = `Score: ${Math.round(scorePercentage * 100)}%`;
- }
- scoreTextEl.setText(ratingText);
- if (ratingDetails) {
- const detailsEl = scoreEl.createDiv("mcq-score-details");
- detailsEl.setText(ratingDetails);
- }
- const resultsEl = contentEl.createDiv("mcq-results");
- resultsEl.createEl("h3", { text: "Question Results" });
- if (this.session.answers.length === 0) {
- resultsEl.createDiv({ cls: "mcq-no-answers", text: "No questions were answered in this session." });
- } else {
- this.session.answers.forEach((answer) => {
- try {
- const question = this.mcqSet.questions[answer.questionIndex];
- if (!question || !question.choices)
- return;
- const resultItem = resultsEl.createDiv("mcq-result-item");
- resultItem.createDiv({ cls: "mcq-result-question", text: question.question || "Question text missing" });
- if (answer.attempts > 1) {
- if (answer.selectedAnswerIndex !== -1) {
- const yourAnswer = resultItem.createDiv("mcq-result-your-answer");
- yourAnswer.createSpan({ cls: "mcq-result-label", text: "Your final answer (correct after multiple attempts): " });
- yourAnswer.createSpan({ cls: "mcq-result-correct", text: question.choices[answer.selectedAnswerIndex] || "(invalid choice)" });
- } else {
- const yourAnswer = resultItem.createDiv("mcq-result-your-answer");
- yourAnswer.createSpan({ cls: "mcq-result-label", text: "Your answer: " });
- yourAnswer.createSpan({ cls: "mcq-result-incorrect", text: '(Incorrect - used "Show Answer" option)' });
- }
- const correctAnswer = resultItem.createDiv("mcq-result-correct-answer");
- correctAnswer.createSpan({ cls: "mcq-result-label", text: "Correct answer: " });
- correctAnswer.createSpan({ cls: "mcq-result-correct", text: question.choices[question.correctAnswerIndex] || "(invalid choice)" });
- } else {
- const yourAnswer = resultItem.createDiv("mcq-result-your-answer");
- yourAnswer.createSpan({ cls: "mcq-result-label", text: "Your answer: " });
- yourAnswer.createSpan({ cls: answer.correct ? "mcq-result-correct" : "mcq-result-incorrect", text: question.choices[answer.selectedAnswerIndex] || "(invalid choice)" });
- if (!answer.correct) {
- const correctAnswer = resultItem.createDiv("mcq-result-correct-answer");
- correctAnswer.createSpan({ cls: "mcq-result-label", text: "Correct answer: " });
- correctAnswer.createSpan({ cls: "mcq-result-correct", text: question.choices[question.correctAnswerIndex] || "(invalid choice)" });
- }
- }
- resultItem.createDiv({ cls: "mcq-result-attempts", text: `Attempts: ${answer.attempts}` });
- resultItem.createDiv({ cls: "mcq-result-time", text: `Time: ${Math.round(answer.timeToAnswer)} seconds` });
- } catch (error) {
- }
- });
- }
- const closeBtn = contentEl.createEl("button", { cls: "mcq-close-btn", text: "Close" });
- closeBtn.addEventListener("click", () => {
- if (this.onCompleteCallback) {
- this.onCompleteCallback(this.notePath, this.session.score, true);
- }
- this.close();
- });
- } catch (error) {
- new import_obsidian8.Setting(contentEl).setName("Error Completing Session").setHeading();
- contentEl.createEl("p", { text: "There was an error completing the MCQ session. Please try again." });
- const errorCloseBtn = contentEl.createEl("button", { cls: "mcq-close-btn", text: "Close" });
- errorCloseBtn.addEventListener("click", () => this.close());
- }
- }
- calculateScore() {
- let totalScore = 0;
- this.session.answers.forEach((answer) => {
- let questionScore = 0;
- if (this.plugin.settings.mcqDeductFullMarkOnFirstFailure) {
- if (answer.attempts === 1 && answer.correct) {
- questionScore = 1;
- } else {
- questionScore = 0;
- }
- } else {
- if (answer.correct && answer.selectedAnswerIndex !== -1) {
- if (answer.attempts === 1) {
- questionScore = 1;
- } else if (answer.attempts === 2) {
- questionScore = 0.5;
- }
- }
- }
- if (questionScore > 0 && answer.timeToAnswer > this.plugin.settings.mcqTimeDeductionSeconds) {
- questionScore -= this.plugin.settings.mcqTimeDeductionAmount;
- questionScore = Math.max(0, questionScore);
- }
- totalScore += questionScore;
- });
- this.session.score = this.mcqSet.questions.length > 0 ? totalScore / this.mcqSet.questions.length : 0;
- }
- // onClose is now handled by the keyboard shortcut registration cleanup
-};
-function mapFsrsStateToString(state) {
- switch (state) {
- case 0:
- return "New";
- case 1:
- return "Learning";
- case 2:
- return "Review";
- case 3:
- return "Relearning";
- default:
- return "Unknown";
- }
-}
-function getFsrsRatingText(rating) {
- switch (rating) {
- case 1:
- return "Again";
- case 2:
- return "Hard";
- case 3:
- return "Good";
- case 4:
- return "Easy";
- default:
- return "Unknown";
- }
-}
-function getSm2RatingText(rating) {
- 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";
- default:
- return "Unknown";
- }
-}
-
-// controllers/review-controller-mcq.ts
-var MCQController = class {
- /**
- * Initialize MCQ controller
- *
- * @param plugin Reference to the main plugin
- * @param mcqService Reference to the MCQ data service
- * @param mcqGenerationService Reference to the MCQ generation service
- */
- constructor(plugin, mcqService, mcqGenerationService) {
- this.plugin = plugin;
- this.mcqService = mcqService;
- this.mcqGenerationService = mcqGenerationService;
- }
- /**
- * Start an MCQ review session for a note
- *
- * @param notePath Path to the note
- * @param onCompleteCallback Optional callback when the modal closes
- */
- // Helper methods to map score to ratings
- mapScoreToSm2Response(score) {
- if (score >= 0.95)
- return 5 /* PerfectRecall */;
- if (score >= 0.8)
- return 4 /* CorrectWithHesitation */;
- if (score >= 0.6)
- return 3 /* CorrectWithDifficulty */;
- if (score >= 0.4)
- return 2 /* IncorrectButFamiliar */;
- if (score > 0)
- return 1 /* IncorrectResponse */;
- return 0 /* CompleteBlackout */;
- }
- mapScoreToFsrsRating(score) {
- if (score >= 0.9)
- return 4 /* Easy */;
- if (score >= 0.7)
- return 3 /* Good */;
- if (score >= 0.5)
- return 2 /* Hard */;
- return 1 /* Again */;
- }
- async startMCQReview(notePath, externalOnCompleteCallback) {
- if (!this.plugin.settings.enableMCQ) {
- new import_obsidian9.Notice("MCQ feature is disabled in settings.");
- if (externalOnCompleteCallback)
- externalOnCompleteCallback(notePath, false);
- return;
- }
- if (!this.mcqGenerationService) {
- new import_obsidian9.Notice("MCQ generation service is not available. Check API provider settings.");
- return;
- }
- try {
- let mcqSet = this.mcqService.getMCQSetForNote(notePath);
- if (mcqSet && mcqSet.needsQuestionRegeneration) {
- new import_obsidian9.Notice("Questions for this note are flagged for regeneration. Generating new set...");
- mcqSet = await this.generateMCQs(notePath, true);
- if (mcqSet) {
- mcqSet.needsQuestionRegeneration = false;
- this.mcqService.saveMCQSet(mcqSet);
- await this.plugin.savePluginData();
- } else {
- new import_obsidian9.Notice("Failed to regenerate MCQs. Using existing set if available.");
- mcqSet = this.mcqService.getMCQSetForNote(notePath);
- }
- }
- if (!mcqSet || mcqSet.questions.length === 0) {
- new import_obsidian9.Notice("No MCQs found for this note. Generating new set...");
- mcqSet = await this.generateMCQs(notePath);
- if (!mcqSet) {
- new import_obsidian9.Notice("Failed to generate MCQs for this note.");
- return;
- }
- }
- const session = {
- 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)
- };
- const internalOnComplete = async (path, score, completed) => {
- if (completed) {
- const schedule = this.plugin.reviewScheduleService.schedules[path];
- if (schedule) {
- let rating;
- if (schedule.schedulingAlgorithm === "fsrs") {
- rating = this.mapScoreToFsrsRating(score);
- new import_obsidian9.Notice(`MCQ complete for FSRS card. Score: ${(score * 100).toFixed(0)}%. Rating: ${FsrsRating[rating]}(${rating}).`);
- } else {
- rating = this.mapScoreToSm2Response(score);
- new import_obsidian9.Notice(`MCQ complete for SM-2 card. Score: ${(score * 100).toFixed(0)}%. Rating: ${ReviewResponse[rating]}(${rating}).`);
- }
- await this.plugin.reviewController.processReviewResponse(path, rating);
- } else {
- new import_obsidian9.Notice(`MCQ complete. Score: ${(score * 100).toFixed(0)}%. Could not find schedule to update review status.`);
- }
- } else {
- new import_obsidian9.Notice(`MCQ session for ${path} was not fully completed. Score (partial): ${(score * 100).toFixed(0)}%. Review not recorded.`);
- }
- if (externalOnCompleteCallback) {
- externalOnCompleteCallback(path, completed && score >= 0.7);
- }
- };
- new MCQModal(this.plugin, notePath, mcqSet, internalOnComplete).open();
- } catch (error) {
- new import_obsidian9.Notice("Error starting MCQ review. Please check console for details.");
- if (externalOnCompleteCallback)
- externalOnCompleteCallback(notePath, false);
- }
- }
- /**
- * Generate MCQs for a note
- *
- * @param notePath Path to the note
- * @param forceRegeneration If true, will ignore existing fresh sets and generate new ones.
- * @returns Generated MCQ set or null if failed
- */
- async generateMCQs(notePath, forceRegeneration = false) {
- if (!this.plugin.settings.enableMCQ || !this.mcqGenerationService) {
- new import_obsidian9.Notice("MCQ feature is disabled or the generation service is not available. Check API provider settings.");
- return null;
- }
- if (!forceRegeneration) {
- const existingSet = this.mcqService.getMCQSetForNote(notePath);
- if (existingSet) {
- const twentyFourHours = 24 * 60 * 60 * 1e3;
- if (Date.now() - existingSet.generatedAt < twentyFourHours && !existingSet.needsQuestionRegeneration) {
- new import_obsidian9.Notice("Using recently generated MCQs for this note.");
- return existingSet;
- }
- }
- }
- const file = this.plugin.app.vault.getAbstractFileByPath(notePath);
- if (!(file instanceof import_obsidian9.TFile)) {
- new import_obsidian9.Notice("Cannot generate MCQs: file not found");
- return null;
- }
- const content = await this.plugin.app.vault.read(file);
- const mcqSet = await this.mcqGenerationService.generateMCQs(notePath, content, this.plugin.settings);
- if (mcqSet) {
- this.mcqService.saveMCQSet(mcqSet);
- await this.plugin.savePluginData();
- new import_obsidian9.Notice("MCQs generated and saved successfully.");
- return mcqSet;
- } else {
- return null;
- }
- }
- // Methods to access MCQ sessions, delegated to mcqService
- getMCQSessionsForNote(notePath) {
- return this.mcqService.getMCQSessionsForNote(notePath);
- }
- getLatestMCQSessionForNote(notePath) {
- return this.mcqService.getLatestMCQSessionForNote(notePath);
- }
- async saveMCQSession(session) {
- this.mcqService.saveMCQSession(session);
- await this.plugin.savePluginData();
- }
- /**
- * Starts a consolidated MCQ review session for all notes due on the currently selected review date.
- */
- async startConsolidatedMCQReviewForSelectedDate() {
- if (!this.plugin.settings.enableMCQ) {
- new import_obsidian9.Notice("MCQ feature is disabled in settings.");
- return;
- }
- if (!this.mcqGenerationService) {
- new import_obsidian9.Notice("MCQ generation service is not available. Check API provider settings.");
- return;
- }
- const dueNotes = this.plugin.reviewController.getTodayNotes();
- if (dueNotes.length === 0) {
- new import_obsidian9.Notice("No notes due for review on the selected date.");
- return;
- }
- const mcqSetsForReview = [];
- let notesWithMCQsCount = 0;
- new import_obsidian9.Notice(`Fetching MCQs for ${dueNotes.length} due note(s)...`);
- for (const noteSchedule of dueNotes) {
- const notePath = noteSchedule.path;
- let mcqSet = this.mcqService.getMCQSetForNote(notePath);
- if (mcqSet && mcqSet.needsQuestionRegeneration) {
- new import_obsidian9.Notice(`Regenerating flagged MCQs for ${notePath}...`);
- const regeneratedMcqSet = await this.generateMCQs(notePath, true);
- if (regeneratedMcqSet) {
- mcqSet = regeneratedMcqSet;
- mcqSet.needsQuestionRegeneration = false;
- this.mcqService.saveMCQSet(mcqSet);
- } else {
- new import_obsidian9.Notice(`Failed to regenerate MCQs for ${notePath}. Using existing set if available (might be outdated or empty).`);
- }
- }
- if (!mcqSet || mcqSet.questions.length === 0) {
- new import_obsidian9.Notice(`No MCQs found or set is empty for ${notePath}. Attempting to generate new set...`);
- const newMcqSet = await this.generateMCQs(notePath, false);
- if (newMcqSet) {
- mcqSet = newMcqSet;
- } else {
- new import_obsidian9.Notice(`Failed to generate MCQs for ${notePath}. This note will be skipped in MCQ review.`);
- }
- }
- if (mcqSet && mcqSet.questions.length > 0) {
- const file = this.plugin.app.vault.getAbstractFileByPath(notePath);
- mcqSetsForReview.push({
- path: notePath,
- mcqSet,
- fileName: file instanceof import_obsidian9.TFile ? file.basename : notePath
- });
- notesWithMCQsCount++;
- }
- }
- if (mcqSetsForReview.length === 0) {
- new import_obsidian9.Notice("No MCQs available or generated for the due notes.");
- return;
- }
- await this.plugin.savePluginData();
- new import_obsidian9.Notice(`Starting consolidated MCQ review for ${notesWithMCQsCount} note(s) with ${mcqSetsForReview.reduce((sum, s) => sum + s.mcqSet.questions.length, 0)} questions.`);
- const onConsolidatedComplete = async (results) => {
- let reviewsProcessed = 0;
- for (const result of results) {
- const schedule = this.plugin.reviewScheduleService.schedules[result.path];
- if (schedule && typeof result.score === "number") {
- let rating;
- if (schedule.schedulingAlgorithm === "fsrs") {
- rating = this.mapScoreToFsrsRating(result.score);
- new import_obsidian9.Notice(`MCQ for ${result.path} (FSRS) - Score: ${(result.score * 100).toFixed(0)}%, Rating: ${FsrsRating[rating]}(${rating})`);
- } else {
- rating = this.mapScoreToSm2Response(result.score);
- new import_obsidian9.Notice(`MCQ for ${result.path} (SM-2) - Score: ${(result.score * 100).toFixed(0)}%, Rating: ${ReviewResponse[rating]}(${rating})`);
- }
- await this.plugin.reviewController.processReviewResponse(result.path, rating);
- reviewsProcessed++;
- } else if (schedule) {
- }
- }
- if (reviewsProcessed > 0) {
- new import_obsidian9.Notice(`${reviewsProcessed} note review(s) updated based on consolidated MCQ session.`);
- } else {
- new import_obsidian9.Notice("No note reviews were updated from the MCQ session.");
- }
- };
- new ConsolidatedMCQModal(this.plugin, mcqSetsForReview, onConsolidatedComplete).open();
- }
-};
-
-// controllers/review-controller.ts
-var ReviewController = class {
- // Add batch controller
- /**
- * Constructor initializes the review controller
- *
- * @param plugin Reference to the main plugin
- * @param mcqService Reference to the MCQ service
- */
- constructor(plugin, mcqService) {
- this.plugin = plugin;
- this.coreController = new ReviewControllerCore(plugin);
- this.navigationController = new ReviewNavigationController(plugin);
- if (plugin.mcqGenerationService) {
- this.mcqController = new MCQController(plugin, mcqService, plugin.mcqGenerationService);
- } else {
- }
- this.batchController = new ReviewBatchController(plugin);
- }
- /**
- * Update the list of today's due notes
- * Delegates to core controller
- *
- * @param preserveCurrentIndex Whether to try to preserve the current note index
- */
- async updateTodayNotes(preserveCurrentIndex = false) {
- await this.coreController.updateTodayNotes(preserveCurrentIndex);
- }
- /**
- * Get the currently loaded notes due for review
- * Delegates to core controller
- */
- getTodayNotes() {
- return this.coreController.getTodayNotes();
- }
- /**
- * Get the current index in today's notes
- * Delegates to core controller
- */
- getCurrentNoteIndex() {
- return this.coreController.getCurrentNoteIndex();
- }
- /**
- * Set the current index in today's notes
- * Delegates to core controller
- *
- * @param index The new index
- */
- setCurrentNoteIndex(index) {
- this.coreController.setCurrentNoteIndex(index);
- }
- /**
- * Review the current note
- * Delegates to core controller
- */
- async reviewCurrentNote() {
- await this.coreController.reviewCurrentNote();
- }
- /**
- * Review a specific note
- * Delegates to core controller
- *
- * @param path Path to the note file
- */
- async reviewNote(path) {
- await this.coreController.reviewNote(path);
- }
- /**
- * Postpone a note's review
- * Delegates to core controller
- *
- * @param path Path to the note file
- * @param days Number of days to postpone (default: 1)
- */
- async postponeNote(path, days = 1) {
- await this.coreController.postponeNote(path, days);
- }
- /**
- * Advance a note's review by one day, if eligible.
- * Delegates to core controller.
- *
- * @param path Path to the note file
- */
- async advanceNote(path) {
- await this.coreController.advanceNote(path);
- }
- /**
- * Handle a note being postponed, updating navigation state
- * Delegates to core controller
- *
- * @param path Path to the postponed note
- */
- async handleNotePostponed(path) {
- await this.coreController.handleNotePostponed(path);
- }
- /**
- * Show the review modal for a note
- * Delegates to core controller
- *
- * @param path Path to the note file
- */
- showReviewModal(path) {
- this.coreController.showReviewModal(path);
- }
- /**
- * Skip the review of a note and reschedule for tomorrow with penalty
- * Delegates to core controller
- *
- * @param path Path to the note file
- */
- async skipReview(path) {
- await this.coreController.skipReview(path);
- }
- /**
- * Process a review response
- * Delegates to core controller
- *
- * @param path Path to the note file
- * @param response User's response during review (SM-2 or FSRS)
- */
- async processReviewResponse(path, response) {
- await this.coreController.processReviewResponse(path, response);
- }
- /**
- * Sets an override for the current review date.
- * Delegates to core controller.
- * @param date Timestamp of the date to simulate, or null to use actual Date.now().
- */
- async setReviewDateOverride(date) {
- await this.coreController.setReviewDateOverride(date);
- }
- /**
- * Gets the current review date override.
- * Delegates to core controller.
- * @returns Timestamp of the override, or null if no override is set.
- */
- getCurrentReviewDateOverride() {
- return this.coreController.getCurrentReviewDateOverride();
- }
- // Delegate methods to specialized controllers
- /**
- * Start reviewing all of today's notes
- * Delegates to batch controller
- */
- async reviewAllTodaysNotes() {
- await this.batchController.reviewAllTodaysNotes();
- }
- /**
- * Navigate to the next note
- * Delegates to navigation controller
- */
- async navigateToNextNote() {
- await this.navigationController.navigateToNextNote();
- }
- /**
- * Navigate to the previous note
- * Delegates to navigation controller
- */
- async navigateToPreviousNote() {
- await this.navigationController.navigateToPreviousNote();
- }
- /**
- * Start an MCQ review session for a note
- * Delegates to MCQ controller
- *
- * @param notePath Path to the note
- * @param onComplete Optional callback for when MCQ review is completed
- */
- async startMCQReview(notePath, onComplete) {
- if (this.mcqController) {
- await this.mcqController.startMCQReview(notePath, onComplete);
- } else {
- if (onComplete) {
- onComplete(notePath, false);
- }
- }
- }
- /**
- * Review a specific set of notes
- * Delegates to batch controller
- *
- * @param paths Array of note paths to review
- * @param useMCQ Whether to use MCQs for testing (default: false)
- */
- async reviewNotes(paths, useMCQ = false) {
- await this.batchController.reviewNotes(paths, useMCQ);
- }
- /**
- * Postpone a specific set of notes
- * Delegates to batch controller
- *
- * @param paths Array of note paths to postpone
- * @param days Number of days to postpone (default: 1)
- */
- async postponeNotes(paths, days = 1) {
- await this.batchController.postponeNotes(paths, days);
- }
- /**
- * Advance a specific set of notes by one day each, if eligible.
- * Delegates to batch controller.
- *
- * @param paths Array of note paths to advance
- */
- async advanceNotes(paths) {
- await this.batchController.advanceNotes(paths);
- }
- /**
- * Remove a specific set of notes from the review schedule
- * Delegates to batch controller
- *
- * @param paths Array of note paths to remove
- */
- async removeNotes(paths) {
- await this.batchController.removeNotes(paths);
- }
- /**
- * Open a note without showing the review modal
- * Delegates to navigation controller
- *
- * @param path Path to the note file
- */
- async openNoteWithoutReview(path) {
- await this.navigationController.openNoteWithoutReview(path);
- }
- /**
- * Swap two notes in the traversal order
- * Delegates to navigation controller
- *
- * @param path1 Path to the first note
- * @param path2 Path to the second note
- */
- async swapNotes(path1, path2) {
- await this.navigationController.swapNotes(path1, path2);
- }
- /**
- * Review all notes with MCQs in a batch
- * Delegates to batch controller
- *
- * @param useMCQ Whether to use MCQs for testing
- */
- async reviewAllNotesWithMCQ(useMCQ = true) {
- await this.batchController.reviewAllNotesWithMCQ(useMCQ);
- }
-};
-
-// utils/link-analyzer.ts
-var import_obsidian10 = require("obsidian");
-var LinkAnalyzer = class {
- /**
- * Analyze links in a folder and build a review hierarchy
- *
- * @param vault Obsidian vault
- * @param folder Folder to analyze
- * @param includeSubfolders Whether to include subfolders
- * @returns Review hierarchy for the folder
- */
- static async analyzeFolder(vault, folder, includeSubfolders) {
- const files = vault.getMarkdownFiles().filter((file) => {
- if (includeSubfolders) {
- return file.path.startsWith(folder.path);
- } else {
- const parentPath = file.parent ? file.parent.path : "";
- return parentPath === folder.path;
- }
- });
- const nodes = {};
- for (const file of files) {
- nodes[file.path] = {
- path: file.path,
- outgoingLinks: [],
- regularLinks: [],
- embedLinks: [],
- incomingLinks: [],
- incomingLinkCount: 0
- };
- }
- for (const file of files) {
- try {
- const content = await vault.read(file);
- const node = nodes[file.path];
- node.content = content;
- const noteLinks = this.extractLinks(content);
- for (const noteLink of noteLinks) {
- const resolvedPath = this.resolveLink(noteLink.text, file.path, files);
- if (resolvedPath && nodes[resolvedPath]) {
- if (!node.outgoingLinks.includes(resolvedPath)) {
- node.outgoingLinks.push(resolvedPath);
- if (noteLink.isEmbed) {
- if (!node.embedLinks.includes(resolvedPath)) {
- node.embedLinks.push(resolvedPath);
- }
- } else {
- if (!node.regularLinks.includes(resolvedPath)) {
- node.regularLinks.push(resolvedPath);
- }
- }
- }
- const targetNode = nodes[resolvedPath];
- if (!targetNode.incomingLinks.includes(file.path)) {
- targetNode.incomingLinks.push(file.path);
- targetNode.incomingLinkCount++;
- }
- }
- }
- } catch (error) {
- }
- }
- const startingNode = this.findStartingNode(nodes);
- const traversalOrder = this.createTraversalOrder(nodes, startingNode);
- return {
- rootNodes: startingNode,
- nodes,
- traversalOrder
- };
- }
- /**
- * Extract links from markdown content in the order they appear
- *
- * @param content Markdown content
- * @returns Array of note links with information about whether they are embeds
- */
- static extractLinks(content) {
- const links = [];
- let match;
- this.LINK_REGEX.lastIndex = 0;
- while ((match = this.LINK_REGEX.exec(content)) !== null) {
- links.push({
- text: match[2],
- // The link text is now in the second capture group
- isEmbed: match[1] === "!"
- // True if it has an exclamation mark
- });
- }
- return links;
- }
- /**
- * Resolve a link to a full file path
- *
- * @param link Link text
- * @param sourcePath Path of the source file
- * @param allFiles All available files
- * @returns Resolved file path or null if not found
- */
- static resolveLink(link, sourcePath, allFiles) {
- if (link.endsWith(".md")) {
- const exactFile = allFiles.find((f) => f.path.endsWith("/" + link) || f.path === link);
- if (exactFile) {
- return exactFile.path;
- }
- }
- const basename = link.split("/").pop();
- if (!basename)
- return null;
- const matchingFiles = allFiles.filter((f) => f.basename === basename);
- if (matchingFiles.length === 0) {
- return null;
- }
- if (matchingFiles.length === 1) {
- return matchingFiles[0].path;
- }
- 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
- *
- * @param nodes All file nodes
- * @returns Array with the path of the best starting node
- */
- static findStartingNode(nodes) {
- var _a, _b, _c, _d;
- if (Object.keys(nodes).length === 0) {
- return [];
- }
- const folderNodes = {};
- for (const path in nodes) {
- const folderPath = path.substring(0, path.lastIndexOf("/"));
- if (!folderNodes[folderPath]) {
- folderNodes[folderPath] = [];
- }
- folderNodes[folderPath].push(path);
- }
- for (const folderPath in folderNodes) {
- const folderName = ((_a = folderPath.split("/").pop()) == null ? void 0 : _a.toLowerCase()) || "";
- const filesInFolder = folderNodes[folderPath];
- for (const path of filesInFolder) {
- const fileName = ((_b = path.split("/").pop()) == null ? void 0 : _b.toLowerCase().replace(/\.md$/, "")) || "";
- if (fileName === folderName) {
- return [path];
- }
- }
- for (const path of filesInFolder) {
- const fileName = ((_c = path.split("/").pop()) == null ? void 0 : _c.toLowerCase().replace(/\.md$/, "")) || "";
- if (fileName.includes(folderName) || folderName.includes(fileName)) {
- return [path];
- }
- }
- for (const path of filesInFolder) {
- const fileName = ((_d = path.split("/").pop()) == null ? void 0 : _d.toLowerCase().replace(/\.md$/, "")) || "";
- if (fileName === "index" || fileName === "main" || fileName.includes("index") || fileName.includes("readme") || fileName.includes("main")) {
- return [path];
- }
- }
- }
- const sortedNodes = Object.values(nodes).sort((a, b2) => {
- const regularLinkDiff = b2.regularLinks.length - a.regularLinks.length;
- if (regularLinkDiff !== 0) {
- return regularLinkDiff;
- }
- return b2.outgoingLinks.length - a.outgoingLinks.length;
- });
- if (sortedNodes.length > 0 && (sortedNodes[0].regularLinks.length > 0 || sortedNodes[0].outgoingLinks.length > 0)) {
- return [sortedNodes[0].path];
- }
- 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
- *
- * @param nodes All file nodes
- * @returns Array of root node paths
- */
- static findRootNodes(nodes) {
- return this.findStartingNode(nodes);
- }
- /**
- * Create a traversal order for reviewing files that respects the exact order of links
- *
- * @param nodes All file nodes
- * @param startNodePath Path to the starting node
- * @returns Array of file paths in traversal order
- */
- static createOrderedTraversal(nodes, startNodePath) {
- const visited = /* @__PURE__ */ new Set();
- const traversalOrder = [];
- const fileFolders = /* @__PURE__ */ new Map();
- for (const path in nodes) {
- const folderPath = path.substring(0, path.lastIndexOf("/"));
- fileFolders.set(path, folderPath);
- }
- const mainFolder = fileFolders.get(startNodePath) || "";
- const traverse = (currentNodePath, currentDepth = 0, currentMainFolder) => {
- if (visited.has(currentNodePath)) {
- return;
- }
- const node = nodes[currentNodePath];
- if (!node) {
- return;
- }
- visited.add(currentNodePath);
- traversalOrder.push(currentNodePath);
- const currentNodeFolder = fileFolders.get(currentNodePath) || "";
- const linksToFollow = node.regularLinks.length > 0 ? node.regularLinks : node.outgoingLinks;
- for (const linkedPath of linksToFollow) {
- if (!nodes[linkedPath]) {
- continue;
- }
- const linkedNodeFolder = fileFolders.get(linkedPath) || "";
- if (nodes[linkedPath] && linkedNodeFolder.startsWith(currentMainFolder)) {
- if (!visited.has(linkedPath)) {
- traverse(linkedPath, currentDepth + 1, currentMainFolder);
- } else {
- }
- } else {
- }
- }
- };
- if (nodes[startNodePath]) {
- traverse(startNodePath, 0, mainFolder);
- } else {
- }
- return traversalOrder;
- }
- /**
- * Create a traversal order for reviewing files
- *
- * @param nodes All file nodes
- * @param rootNodes Root node paths
- * @returns Array of file paths in traversal order
- */
- static createTraversalOrder(nodes, rootNodes) {
- if (rootNodes.length === 1) {
- return this.createOrderedTraversal(nodes, rootNodes[0]);
- }
- const visited = /* @__PURE__ */ new Set();
- const traversalOrder = [];
- const traverse = (nodePath) => {
- if (visited.has(nodePath))
- return;
- visited.add(nodePath);
- traversalOrder.push(nodePath);
- const node = nodes[nodePath];
- if (!node)
- return;
- for (const linkedPath of node.outgoingLinks) {
- traverse(linkedPath);
- }
- };
- for (const rootPath of rootNodes) {
- traverse(rootPath);
- }
- for (const nodePath of Object.keys(nodes)) {
- if (!visited.has(nodePath)) {
- traversalOrder.push(nodePath);
- visited.add(nodePath);
- }
- }
- return traversalOrder;
- }
- /**
- * Analyze links in a single note
- *
- * @param vault Obsidian vault
- * @param filePath Path to the note file
- * @param regularOnly Whether to include only regular wiki links (not embeds)
- * @returns Array of resolved link paths in the order they appear
- */
- static async analyzeNoteLinks(vault, filePath, regularOnly = false) {
- const file = vault.getAbstractFileByPath(filePath);
- if (!(file instanceof import_obsidian10.TFile)) {
- return [];
- }
- try {
- const content = await vault.read(file);
- const noteLinks = this.extractLinks(content);
- const resolvedLinks = [];
- const seenLinks = /* @__PURE__ */ new Set();
- const filteredLinks = regularOnly ? noteLinks.filter((link) => !link.isEmbed) : noteLinks;
- for (const link of filteredLinks) {
- const resolvedPath = this.resolveLink(
- link.text,
- filePath,
- vault.getMarkdownFiles()
- );
- if (resolvedPath && !seenLinks.has(resolvedPath)) {
- resolvedLinks.push(resolvedPath);
- seenLinks.add(resolvedPath);
- }
- }
- return resolvedLinks;
- } catch (error) {
- return [];
- }
- }
-};
-/**
- * Regular expression for finding wikilinks of both forms: [[filename]] and ![[filename]]
- * First capture group matches the exclamation mark (if present)
- * Second capture group matches the content inside the brackets
- */
-LinkAnalyzer.LINK_REGEX = /(!?)(?:\[\[(.*?)\]\])/g;
-
-// controllers/review-session-controller.ts
-var ReviewSessionController = class {
- /**
- * Initialize session controller
- *
- * @param plugin Reference to the main plugin
- */
- constructor(plugin) {
- /**
- * Cache of linked notes to improve performance
- */
- this.linkedNoteCache = /* @__PURE__ */ new Map();
- this.plugin = plugin;
- }
- /**
- * Get linked notes that are due today
- *
- * @param notePath Path of the note to get links from
- * @returns Array of paths to linked notes that are due today
- */
- getDueLinkedNotes(notePath) {
- const reviewController = this.plugin.reviewController;
- if (!reviewController)
- return [];
- const todayNotes = reviewController.getTodayNotes();
- const links = this.linkedNoteCache.get(notePath) || [];
- const duePaths = todayNotes.map((n) => n.path);
- if (links.length === 0) {
- (async () => {
- const newLinks = await this.analyzeNoteLinks(notePath);
- if (newLinks.length > 0) {
- this.linkedNoteCache.set(notePath, newLinks);
- }
- })();
- return [];
- }
- return links.filter((link) => duePaths.includes(link));
- }
- /**
- * Analyze links in a note and cache the results
- *
- * @param notePath Path to the note
- * @returns Array of linked note paths
- */
- async analyzeNoteLinks(notePath) {
- try {
- const links = await LinkAnalyzer.analyzeNoteLinks(
- this.plugin.app.vault,
- notePath,
- true
- // regularOnly = true - only include regular wiki links, not embeds
- );
- this.linkedNoteCache.set(notePath, links);
- return links;
- } catch (error) {
- return [];
- }
- }
- /**
- * Clear the link cache for a specific note or all notes
- *
- * @param notePath Optional path to clear cache for specific note
- */
- clearLinkCache(notePath) {
- if (notePath) {
- this.linkedNoteCache.delete(notePath);
- } else {
- this.linkedNoteCache.clear();
- }
- }
-};
-
-// ui/context-menu.ts
-var import_obsidian11 = require("obsidian");
-var ContextMenuHandler = class {
- /**
- * Initialize context menu handler
- *
- * @param plugin Reference to the main plugin
- */
- constructor(plugin) {
- this.plugin = plugin;
- }
- /**
- * Register context menu handlers
- */
- register() {
- this.plugin.registerEvent(
- this.plugin.app.workspace.on("file-menu", this.handleFileMenuEvent.bind(this))
- );
- }
- /**
- * Handles the 'file-menu' event for TAbstractFile (could be TFile or TFolder).
- *
- * @param menu Context menu
- * @param abstractFile Target file or folder
- */
- handleFileMenuEvent(menu, abstractFile) {
- if (abstractFile instanceof import_obsidian11.TFolder) {
- this.addFolderMenuItems(menu, abstractFile);
- } else if (abstractFile instanceof import_obsidian11.TFile && abstractFile.extension === "md") {
- this.addFileMenuItems(menu, abstractFile);
- }
- }
- /**
- * Adds context menu items for a TFile.
- *
- * @param menu Context menu
- * @param file Target file
- */
- addFileMenuItems(menu, file) {
- const isScheduled = !!this.plugin.reviewScheduleService.schedules[file.path];
- 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();
- });
- });
- if (isScheduled) {
- menu.addItem((item) => {
- item.setTitle("Review now").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();
- });
- });
- }
- if (file.parent) {
- menu.addItem((item) => {
- item.setTitle("Add note's folder to review schedule").setIcon("folder-plus").onClick(async () => {
- if (file.parent) {
- new import_obsidian11.Notice(`Adding folder "${file.parent.name}" to review schedule...`);
- if (file.parent instanceof import_obsidian11.TFolder) {
- await this.addFolderToReview(file.parent);
- } else {
- new import_obsidian11.Notice("Error: Parent is not a folder.");
- }
- }
- });
- });
- }
- }
- /**
- * Adds context menu items for a TFolder.
- *
- * @param menu Context menu
- * @param folder Target folder
- */
- addFolderMenuItems(menu, folder) {
- try {
- menu.addItem((item) => {
- item.setTitle("Add folder to review").setIcon("calendar-plus").onClick(() => {
- this.addFolderToReview(folder);
- });
- });
- } catch (error) {
- }
- }
- /**
- * Add all markdown files in a folder to the review schedule
- *
- * @param folder Target folder
- */
- async addFolderToReview(folder) {
- new import_obsidian11.Notice(`Analyzing folder structure for "${folder.name}"...`);
- try {
- const allFiles = this.plugin.app.vault.getMarkdownFiles().filter((file) => {
- if (this.plugin.settings.includeSubfolders) {
- const folderPath = folder.path === "/" ? "/" : `${folder.path}/`;
- return file.path.startsWith(folderPath);
- } else {
- const parentPath = file.parent ? file.parent.path : "";
- return parentPath === folder.path;
- }
- });
- if (allFiles.length === 0) {
- new import_obsidian11.Notice("No markdown files found in folder.");
- return;
- }
- const includeSubfolders = this.plugin.settings.includeSubfolders;
- let mainFilePath = null;
- for (const file of allFiles) {
- const fileName = file.basename.toLowerCase();
- const folderName = folder.name.toLowerCase();
- if (fileName === folderName) {
- mainFilePath = file.path;
- break;
- }
- }
- if (!mainFilePath) {
- for (const file of allFiles) {
- const fileName = file.basename.toLowerCase();
- const folderName = folder.name.toLowerCase();
- if (fileName.includes(folderName) || folderName.includes(fileName) || fileName === "index" || fileName === "main" || fileName.includes("index") || fileName.includes("main")) {
- mainFilePath = file.path;
- break;
- }
- }
- }
- if (!mainFilePath) {
- const activeFile = this.plugin.app.workspace.getActiveFile();
- if (activeFile && activeFile.extension === "md" && allFiles.some((f) => f.path === activeFile.path)) {
- mainFilePath = activeFile.path;
- }
- }
- let traversalOrder = [];
- const visited = /* @__PURE__ */ new Set();
- const processLinksRecursively = async (path) => {
- if (visited.has(path)) {
- return;
- }
- visited.add(path);
- traversalOrder.push(path);
- const links = await LinkAnalyzer.analyzeNoteLinks(
- this.plugin.app.vault,
- path,
- false
- );
- for (const link of links) {
- const linkFile = this.plugin.app.vault.getAbstractFileByPath(link);
- if (!(linkFile instanceof import_obsidian11.TFile) || linkFile.extension !== "md") {
- continue;
- }
- if (allFiles.some((f) => f.path === linkFile.path)) {
- await processLinksRecursively(linkFile.path);
- } else {
- if (!visited.has(linkFile.path)) {
- visited.add(linkFile.path);
- traversalOrder.push(linkFile.path);
- }
- }
- }
- };
- if (mainFilePath) {
- await processLinksRecursively(mainFilePath);
- }
- const sortedAllFiles = [...allFiles].sort((a, b2) => a.path.localeCompare(b2.path));
- for (const file of sortedAllFiles) {
- if (!visited.has(file.path)) {
- await processLinksRecursively(file.path);
- }
- }
- const count = await this.plugin.reviewScheduleService.scheduleNotesInOrder(traversalOrder);
- if (count > 0) {
- await this.plugin.savePluginData();
- }
- const startingFileName = traversalOrder.length > 0 ? traversalOrder[0].split("/").pop() : "unknown";
- new import_obsidian11.Notice(`Added ${count} notes from "${folder.name}" to review schedule, starting with ${startingFileName}`);
- await this.plugin.reviewController.updateTodayNotes();
- } catch (error) {
- new import_obsidian11.Notice("Error adding folder to review schedule");
- }
- }
- /**
- * Create a hierarchical review session for a folder
- *
- * @param folder Target folder
- */
- async createHierarchicalSession(folder) {
- new import_obsidian11.Notice("Analyzing folder structure and links...");
- const session = await this.plugin.reviewSessionService.createReviewSession(
- folder.path,
- folder.name
- );
- if (session) {
- await this.plugin.savePluginData();
- }
- if (!session) {
- new import_obsidian11.Notice("Failed to create review session");
- return;
- }
- const fileCount = session.hierarchy.traversalOrder.length;
- await this.plugin.reviewSessionService.setActiveSession(session.id);
- await this.plugin.savePluginData();
- const firstFilePath = this.plugin.reviewSessionService.getNextSessionFile();
- if (firstFilePath) {
- const scheduledCount = await this.plugin.reviewSessionService.scheduleSessionForReview(session.id);
- if (scheduledCount > 0) {
- await this.plugin.savePluginData();
- }
- new import_obsidian11.Notice(`Created hierarchical review session with ${fileCount} files.`);
- const file = this.plugin.app.vault.getAbstractFileByPath(firstFilePath);
- if (file instanceof import_obsidian11.TFile) {
- this.plugin.reviewController.reviewNote(firstFilePath);
- }
- } else {
- new import_obsidian11.Notice("No files found for review in this folder.");
- await this.plugin.reviewSessionService.setActiveSession(null);
- await this.plugin.savePluginData();
- }
- }
-};
-
-// ui/sidebar-view.ts
-var import_obsidian16 = require("obsidian");
-
-// ui/sidebar/pomodoro-ui-manager.ts
-var import_obsidian12 = require("obsidian");
-var PomodoroUIManager = class {
- constructor(plugin) {
- this.attachedContainer = null;
- // The container provided by ListViewRenderer
- // References to Pomodoro UI elements
- this.pomodoroVisibilityToggleBtnContainer = null;
- this.pomodoroVisibilityToggleBtn = null;
- this.pomodoroRootEl = null;
- // Container for the actual timer content
- this.pomodoroTimerDisplayEl = null;
- this.pomodoroStartBtn = null;
- this.pomodoroStopBtn = null;
- this.pomodoroSkipBtn = null;
- this.pomodoroQuickSettingsPanelEl = null;
- // private pomodoroQuickSettingsToggleBtn: HTMLElement | null = null; // Removed
- this.pomodoroQuickWorkInput = null;
- this.pomodoroQuickShortInput = null;
- this.pomodoroQuickLongInput = null;
- this.pomodoroQuickSessionsInput = null;
- this.pomodoroCalculationResultEl = null;
- // New estimation and cycle tracking elements (moved to calculation panel)
- this.pomodoroCycleProgressEl = null;
- // User override input elements
- this.pomodoroUserOverrideHoursInput = null;
- this.pomodoroUserOverrideMinutesInput = null;
- this.pomodoroUserAddToEstimationCheckbox = null;
- // private isPomodoroSectionOpen: boolean = false; // No longer needed, section is always "open"
- this.areButtonsVisible = true;
- // For Play/Pause/Skip buttons
- this.isTimerTextVisible = true;
- // For the timer countdown text
- this.longPressTimer = null;
- this.veryLongPressTimer = null;
- this.LONG_PRESS_DURATION = 500;
- // ms
- this.VERY_LONG_PRESS_DURATION = 1500;
- // ms
- this.didLongPress = false;
- this.didVeryLongPress = false;
- this.plugin = plugin;
- }
- /**
- * Saves the current values from the Pomodoro quick settings input fields.
- * @returns true if settings were valid and saved, false otherwise.
- */
- _savePomodoroSettings() {
- var _a, _b, _c, _d;
- const work = parseInt(((_a = this.pomodoroQuickWorkInput) == null ? void 0 : _a.value) || "0");
- const short = parseInt(((_b = this.pomodoroQuickShortInput) == null ? void 0 : _b.value) || "0");
- const long = parseInt(((_c = this.pomodoroQuickLongInput) == null ? void 0 : _c.value) || "0");
- const sessions = parseInt(((_d = this.pomodoroQuickSessionsInput) == null ? void 0 : _d.value) || "0");
- if (work > 0 && short > 0 && long > 0 && sessions > 0) {
- this.plugin.pomodoroService.updateDurations(work, short, long, sessions);
- return true;
- } else {
- new import_obsidian12.Notice("Invalid Pomodoro durations. Settings not saved. Please enter positive numbers.");
- 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;
- }
- }
- /**
- * Attaches the Pomodoro UI to a given container and renders its initial state.
- * Creates necessary sub-containers if they don't exist.
- * @param container The parent element where the Pomodoro UI should be placed.
- */
- attachAndRender(container) {
- var _a;
- this.attachedContainer = container;
- let rootElWasCreated = false;
- if (!this.pomodoroRootEl || this.pomodoroRootEl.parentElement !== this.attachedContainer) {
- (_a = this.pomodoroRootEl) == null ? void 0 : _a.remove();
- this.pomodoroRootEl = this.attachedContainer.createDiv("pomodoro-section-content");
- rootElWasCreated = true;
- }
- if (this.pomodoroRootEl) {
- this.pomodoroRootEl.style.display = "";
- }
- if (rootElWasCreated || this.pomodoroRootEl && this.pomodoroRootEl.children.length === 0) {
- this.renderPomodoroTimer(this.pomodoroRootEl);
- }
- this.updatePomodoroUI();
- }
- // getIsPomodoroSectionOpen, setIsPomodoroSectionOpen, setupPomodoroVisibilityToggleButton, updatePomodoroVisibility are no longer needed
- // as the section is always visible and the toggle button is removed.
- /** Controls the visibility of the entire attached Pomodoro UI section (e.g. for global plugin enable/disable) */
- showPomodoroSection(show) {
- if (this.attachedContainer) {
- this.attachedContainer.style.display = show ? "" : "none";
- }
- }
- /**
- * Renders or updates the Pomodoro Timer UI elements into pomodoroRootEl.
- * @param container The parent element to render into (this.pomodoroRootEl).
- */
- renderPomodoroTimer(container) {
- var _a, _b, _c, _d, _e, _f;
- let mainControlsRow = container.querySelector(".pomodoro-main-controls");
- if (!mainControlsRow) {
- mainControlsRow = container.createDiv("pomodoro-main-controls");
- }
- if (!this.pomodoroStartBtn || this.pomodoroStartBtn.parentElement !== mainControlsRow) {
- (_a = this.pomodoroStartBtn) == null ? void 0 : _a.remove();
- this.pomodoroStartBtn = mainControlsRow.createEl("button", { cls: "pomodoro-start-btn" });
- (0, import_obsidian12.setIcon)(this.pomodoroStartBtn, "play");
- this.pomodoroStartBtn.addEventListener("click", () => this.plugin.pomodoroService.start());
- }
- if (!this.pomodoroStopBtn || this.pomodoroStopBtn.parentElement !== mainControlsRow) {
- (_b = this.pomodoroStopBtn) == null ? void 0 : _b.remove();
- this.pomodoroStopBtn = mainControlsRow.createEl("button", { cls: "pomodoro-stop-btn" });
- (0, import_obsidian12.setIcon)(this.pomodoroStopBtn, "pause");
- this.pomodoroStopBtn.addEventListener("click", () => this.plugin.pomodoroService.stop());
- }
- this.pomodoroStopBtn.hide();
- if (!this.pomodoroTimerDisplayEl || this.pomodoroTimerDisplayEl.parentElement !== mainControlsRow) {
- (_c = this.pomodoroTimerDisplayEl) == null ? void 0 : _c.remove();
- this.pomodoroTimerDisplayEl = mainControlsRow.createDiv("pomodoro-timer-display");
- this.pomodoroTimerDisplayEl.addClass("pomodoro-timer-fade");
- }
- this.pomodoroTimerDisplayEl.setText(this.plugin.pomodoroService.getFormattedTimeLeft());
- if (this.pomodoroTimerDisplayEl) {
- this.pomodoroTimerDisplayEl.addEventListener("mousedown", (e) => {
- e.preventDefault();
- this.didLongPress = false;
- this.didVeryLongPress = false;
- this.longPressTimer = window.setTimeout(() => {
- this.didLongPress = true;
- }, this.LONG_PRESS_DURATION);
- this.veryLongPressTimer = window.setTimeout(() => {
- this.didVeryLongPress = true;
- this.isTimerTextVisible = !this.isTimerTextVisible;
- this.areButtonsVisible = this.isTimerTextVisible;
- this.updateTimerTextDisplay();
- this.updateButtonVisibility();
- }, this.VERY_LONG_PRESS_DURATION);
- });
- const handlePressEnd = () => {
- if (this.veryLongPressTimer)
- clearTimeout(this.veryLongPressTimer);
- if (this.longPressTimer)
- clearTimeout(this.longPressTimer);
- this.veryLongPressTimer = null;
- this.longPressTimer = null;
- if (this.didVeryLongPress) {
- } else if (this.didLongPress) {
- if (!this.isTimerTextVisible) {
- this.isTimerTextVisible = true;
- this.areButtonsVisible = true;
- } else {
- this.areButtonsVisible = !this.areButtonsVisible;
- }
- this.updateTimerTextDisplay();
- this.updateButtonVisibility();
- } else {
- if (this.isTimerTextVisible) {
- this.toggleSettingsPanel();
- }
- }
- this.didLongPress = false;
- this.didVeryLongPress = false;
- };
- this.pomodoroTimerDisplayEl.addEventListener("mouseup", handlePressEnd);
- this.pomodoroTimerDisplayEl.addEventListener("touchend", handlePressEnd);
- const cancelPress = () => {
- if (this.veryLongPressTimer)
- clearTimeout(this.veryLongPressTimer);
- if (this.longPressTimer)
- clearTimeout(this.longPressTimer);
- this.veryLongPressTimer = null;
- this.longPressTimer = null;
- this.didLongPress = false;
- this.didVeryLongPress = false;
- };
- this.pomodoroTimerDisplayEl.addEventListener("mouseleave", cancelPress);
- this.pomodoroTimerDisplayEl.addEventListener("touchmove", cancelPress);
- this.pomodoroTimerDisplayEl.addEventListener("touchstart", (e) => {
- e.preventDefault();
- this.didLongPress = false;
- this.didVeryLongPress = false;
- this.longPressTimer = window.setTimeout(() => {
- this.didLongPress = true;
- }, this.LONG_PRESS_DURATION);
- this.veryLongPressTimer = window.setTimeout(() => {
- this.didVeryLongPress = true;
- this.isTimerTextVisible = !this.isTimerTextVisible;
- this.areButtonsVisible = this.isTimerTextVisible;
- this.updateTimerTextDisplay();
- this.updateButtonVisibility();
- }, this.VERY_LONG_PRESS_DURATION);
- }, { passive: false });
- }
- if (!this.pomodoroSkipBtn || this.pomodoroSkipBtn.parentElement !== mainControlsRow) {
- (_d = this.pomodoroSkipBtn) == null ? void 0 : _d.remove();
- this.pomodoroSkipBtn = mainControlsRow.createEl("button", { cls: "pomodoro-skip-btn" });
- (0, import_obsidian12.setIcon)(this.pomodoroSkipBtn, "skip-forward");
- this.pomodoroSkipBtn.addEventListener("click", () => this.plugin.pomodoroService.skipSession());
- }
- if (!this.pomodoroCycleProgressEl || this.pomodoroCycleProgressEl.parentElement !== container) {
- (_e = this.pomodoroCycleProgressEl) == null ? void 0 : _e.remove();
- this.pomodoroCycleProgressEl = container.createDiv("pomodoro-cycle-progress");
- }
- let settingsPanelContainer = container.querySelector(".pomodoro-settings-panel-container");
- if (!settingsPanelContainer) {
- settingsPanelContainer = container.createDiv("pomodoro-settings-panel-container");
- }
- if (!this.pomodoroQuickSettingsPanelEl || this.pomodoroQuickSettingsPanelEl.parentElement !== settingsPanelContainer) {
- (_f = this.pomodoroQuickSettingsPanelEl) == null ? void 0 : _f.remove();
- this.pomodoroQuickSettingsPanelEl = settingsPanelContainer.createDiv("pomodoro-quick-settings-panel");
- this.pomodoroQuickSettingsPanelEl.style.display = "none";
- const createQuickSetting = (labelText, inputType = "number") => {
- const settingDiv = this.pomodoroQuickSettingsPanelEl.createDiv("pomodoro-quick-setting");
- settingDiv.createEl("label", { text: labelText });
- const input = settingDiv.createEl("input", { type: inputType });
- input.setAttr("min", "1");
- return input;
- };
- this.pomodoroQuickWorkInput = createQuickSetting("Work (min):");
- this.pomodoroQuickShortInput = createQuickSetting("Short Break (min):");
- this.pomodoroQuickLongInput = createQuickSetting("Long Break (min):");
- this.pomodoroQuickSessionsInput = createQuickSetting("Sessions/Long Break:");
- const buttonsContainer = this.pomodoroQuickSettingsPanelEl.createDiv({ cls: "pomodoro-quick-settings-buttons" });
- const overrideContainer = this.pomodoroQuickSettingsPanelEl.createDiv("pomodoro-override-container");
- const overrideLabel = 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");
- 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");
- 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 settingsSaved = this._savePomodoroSettings();
- if (settingsSaved) {
- await this.calculateAndDisplayPomodoroEstimate();
- }
- });
- this.pomodoroCalculationResultEl = this.pomodoroQuickSettingsPanelEl.createDiv({ cls: "pomodoro-calculation-result" });
- this.pomodoroCalculationResultEl.style.display = "none";
- }
- this.updatePomodoroUI();
- }
- /**
- * Calculates the estimated Pomodoro cycles for today's notes and displays it.
- */
- async calculateAndDisplayPomodoroEstimate() {
- if (!this.plugin || !this.pomodoroCalculationResultEl)
- return;
- this.saveUserOverrideSettings();
- const notesForEstimate = this.plugin.reviewController.getTodayNotes();
- 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 ? `No notes scheduled for ${new Date(activeDate).toLocaleDateString()} to calculate.` : "No notes currently due to calculate.";
- this.pomodoroCalculationResultEl.setText(message);
- this.pomodoroCalculationResultEl.style.display = "block";
- return;
- }
- const result = await this.plugin.pomodoroService.calculateEstimationFromNotes(notesForEstimate);
- if (!result) {
- this.pomodoroCalculationResultEl.setText("Unable to calculate estimation.");
- this.pomodoroCalculationResultEl.style.display = "block";
- return;
- }
- const { totalReadingTimeInSeconds, totalReadingTimeInMinutes, pomodorosNeeded, totalTimeWithBreaksMinutes } = result;
- const addToEstimation = this.plugin.pluginState.pomodoroUserAddToEstimation || false;
- const formattedTotalReadingTime = EstimationUtils.formatTime(totalReadingTimeInSeconds);
- const formattedTotalTimeWithBreaks = EstimationUtils.formatTime(Math.ceil(totalTimeWithBreaksMinutes * 60));
- this.pomodoroCalculationResultEl.empty();
- if (notesForEstimate.length > 0 && totalReadingTimeInSeconds > 0 && (!userOverrideTimeInMinutes || addToEstimation)) {
- let baseReadingTimeInSeconds = 0;
- for (const note of notesForEstimate) {
- baseReadingTimeInSeconds += await this.plugin.reviewScheduleService.estimateReviewTime(note.path);
- }
- const formattedBaseReadingTime = EstimationUtils.formatTime(baseReadingTimeInSeconds);
- this.pomodoroCalculationResultEl.createEl("p", { text: `Base reading time for ${notesForEstimate.length} note(s): ${formattedBaseReadingTime}.` });
- } else if (notesForEstimate.length === 0 && userOverrideTimeInMinutes > 0) {
- this.pomodoroCalculationResultEl.createEl("p", { text: `Using override time only (no notes).` });
- }
- if (userOverrideTimeInMinutes > 0) {
- 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.updateCycleProgressDisplay();
- }
- /**
- * Saves user override settings to plugin state
- */
- saveUserOverrideSettings() {
- var _a, _b, _c;
- const hours = parseInt(((_a = this.pomodoroUserOverrideHoursInput) == null ? void 0 : _a.value) || "0");
- const minutes = parseInt(((_b = this.pomodoroUserOverrideMinutesInput) == null ? void 0 : _b.value) || "0");
- const addToEstimation = ((_c = this.pomodoroUserAddToEstimationCheckbox) == null ? void 0 : _c.checked) || false;
- this.plugin.pluginState.pomodoroUserOverrideHours = hours;
- this.plugin.pluginState.pomodoroUserOverrideMinutes = minutes;
- this.plugin.pluginState.pomodoroUserAddToEstimation = addToEstimation;
- this.plugin.savePluginData();
- }
- /**
- * Updates the Pomodoro UI based on the current state from PomodoroService.
- */
- toggleSettingsPanel() {
- const panel = this.pomodoroQuickSettingsPanelEl;
- if (!panel)
- return;
- const isCurrentlyHidden = panel.style.display === "none" || !panel.style.display;
- panel.style.display = isCurrentlyHidden ? "flex" : "none";
- if (isCurrentlyHidden) {
- 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 {
- this._savePomodoroSettings();
- }
- }
- updateTimerTextDisplay() {
- if (this.pomodoroTimerDisplayEl) {
- this.pomodoroTimerDisplayEl.style.opacity = this.isTimerTextVisible ? "1" : "0";
- }
- }
- updateButtonVisibility() {
- 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;
- }
- /**
- * Updates the Pomodoro UI based on the current state from PomodoroService.
- */
- updatePomodoroUI() {
- var _a;
- if (!this.attachedContainer || !this.pomodoroRootEl) {
- return;
- }
- this.pomodoroRootEl.style.display = "";
- const state = this.plugin.pluginState;
- const service = this.plugin.pomodoroService;
- if (this.pomodoroTimerDisplayEl) {
- this.pomodoroTimerDisplayEl.setText(service.getFormattedTimeLeft());
- this.pomodoroTimerDisplayEl.className = "pomodoro-timer-display pomodoro-timer-fade";
- if (state.pomodoroCurrentMode !== "idle") {
- this.pomodoroTimerDisplayEl.addClass(`mode-${state.pomodoroCurrentMode}`);
- } else {
- this.pomodoroTimerDisplayEl.addClass("mode-idle");
- }
- if (state.pomodoroIsRunning) {
- this.pomodoroTimerDisplayEl.addClass("timer-visible");
- } else {
- this.pomodoroTimerDisplayEl.removeClass("timer-visible");
- }
- }
- if (this.pomodoroTimerDisplayEl) {
- this.pomodoroTimerDisplayEl.setText(service.getFormattedTimeLeft());
- this.updateTimerTextDisplay();
- this.pomodoroTimerDisplayEl.className = "pomodoro-timer-display pomodoro-timer-fade";
- if (state.pomodoroCurrentMode !== "idle") {
- this.pomodoroTimerDisplayEl.addClass(`mode-${state.pomodoroCurrentMode}`);
- } else {
- this.pomodoroTimerDisplayEl.addClass("mode-idle");
- }
- if (state.pomodoroIsRunning) {
- this.pomodoroTimerDisplayEl.addClass("timer-visible");
- } else {
- this.pomodoroTimerDisplayEl.removeClass("timer-visible");
- }
- }
- this.updateButtonVisibility();
- 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");
- if (this.pomodoroCalculationResultEl && ((_a = this.pomodoroQuickSettingsPanelEl) == null ? void 0 : _a.style.display) === "none") {
- this.pomodoroCalculationResultEl.style.display = "none";
- }
- this.updateCycleProgressDisplay();
- }
- /**
- * Updates the cycle progress display based on current state
- */
- updateCycleProgressDisplay() {
- if (!this.pomodoroCycleProgressEl)
- return;
- const cycleProgress = this.plugin.pomodoroService.getCycleProgress();
- if (cycleProgress) {
- const { current, total, workSessionsRemaining, totalWorkSessions, totalTimeMinutes } = cycleProgress;
- const completedSessions = totalWorkSessions - workSessionsRemaining;
- 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.addClass("cycle-active");
- } else {
- this.pomodoroCycleProgressEl.style.display = "none";
- }
- }
- /**
- * Format time in seconds to a readable string
- */
- formatTime(totalSeconds) {
- const hours = Math.floor(totalSeconds / 3600);
- const minutes = Math.floor(totalSeconds % 3600 / 60);
- if (hours > 0) {
- return `${hours}h ${minutes}m`;
- } else {
- return `${minutes}m`;
- }
- }
- /**
- * Calculate and display estimation for current notes
- */
- async calculateAndDisplayEstimation() {
- const notesForEstimate = this.plugin.reviewController.getTodayNotes();
- await this.plugin.pomodoroService.calculateEstimationFromNotes(notesForEstimate);
- }
-};
-
-// ui/sidebar/note-item-renderer.ts
-var import_obsidian13 = require("obsidian");
-
-// utils/dates.ts
-var DateUtils = class {
- /**
- * Get start of day timestamp for a given date
- *
- * @param date Date to get start of day for
- * @returns Timestamp for start of day
- */
- static startOfDay(date = /* @__PURE__ */ new Date()) {
- const newDate = new Date(date);
- newDate.setHours(0, 0, 0, 0);
- return newDate.getTime();
- }
- /**
- * Add days to a timestamp
- *
- * @param timestamp Base timestamp
- * @param days Number of days to add
- * @returns New timestamp
- */
- static addDays(timestamp, days) {
- return timestamp + days * 24 * 60 * 60 * 1e3;
- }
- /**
- * Format a timestamp as a readable date string
- *
- * @param timestamp Timestamp to format
- * @param format Format type ('short', 'medium', 'long', 'relative')
- * @param baseDateParam Optional base date for relative formatting
- * @returns Formatted date string
- */
- static formatDate(timestamp, format = "medium", baseDateParam) {
- const noteEventDate = new Date(timestamp);
- if (format === "relative") {
- const referenceDateForCalc = baseDateParam ? new Date(baseDateParam) : /* @__PURE__ */ new Date();
- const normalizedNoteEventDate = this.startOfDay(noteEventDate);
- const normalizedReferenceDate = this.startOfDay(referenceDateForCalc);
- const normalizedActualCurrentDate = this.startOfDay(/* @__PURE__ */ new Date());
- if (normalizedNoteEventDate < normalizedActualCurrentDate) {
- return "Due notes";
- }
- if (baseDateParam) {
- const normalizedBaseDate = this.startOfDay(new Date(baseDateParam));
- if (normalizedBaseDate === normalizedActualCurrentDate) {
- return "Today";
- } else if (normalizedBaseDate === this.startOfDay(new Date(this.addDays(normalizedActualCurrentDate, 1)))) {
- return "Tomorrow";
- } else {
- return new Date(normalizedBaseDate).toLocaleDateString(void 0, { month: "short", day: "numeric" });
- }
- } else {
- const diffInDays = Math.floor((normalizedNoteEventDate - normalizedActualCurrentDate) / (24 * 60 * 60 * 1e3));
- if (diffInDays === 0) {
- return "Today";
- } else if (diffInDays === 1) {
- return "Tomorrow";
- } else {
- return `In ${diffInDays} days`;
- }
- }
- } else if (format === "short") {
- return noteEventDate.toLocaleDateString();
- } else if (format === "long") {
- return noteEventDate.toLocaleDateString(void 0, {
- weekday: "long",
- year: "numeric",
- month: "long",
- day: "numeric"
- });
- } else {
- return noteEventDate.toLocaleDateString(void 0, {
- weekday: "short",
- month: "short",
- day: "numeric"
- });
- }
- }
- /**
- * Get the day difference between two timestamps
- *
- * @param timestamp1 First timestamp
- * @param timestamp2 Second timestamp
- * @returns Difference in days
- */
- static dayDifference(timestamp1, timestamp2) {
- const date1 = new Date(timestamp1);
- const date2 = new Date(timestamp2);
- date1.setHours(0, 0, 0, 0);
- date2.setHours(0, 0, 0, 0);
- const diffTime = Math.abs(date2.getTime() - date1.getTime());
- const diffDays = Math.floor(diffTime / (1e3 * 60 * 60 * 24));
- return diffDays;
- }
- /**
- * Get start of UTC day timestamp for a given date
- *
- * @param date Date to get start of UTC day for
- * @returns Timestamp for start of UTC day (00:00:00.000Z)
- */
- static startOfUTCDay(date = /* @__PURE__ */ new Date()) {
- const newDate = new Date(date.getTime());
- newDate.setUTCHours(0, 0, 0, 0);
- return newDate.getTime();
- }
- /**
- * Get end of UTC day timestamp for a given date
- *
- * @param date Date to get end of UTC day for
- * @returns Timestamp for end of UTC day (23:59:59.999Z)
- */
- static endOfUTCDay(date = /* @__PURE__ */ new Date()) {
- const newDate = new Date(date.getTime());
- newDate.setUTCHours(23, 59, 59, 999);
- return newDate.getTime();
- }
- /**
- * Get the day difference between two timestamps based on UTC days
- *
- * @param timestamp1 First timestamp
- * @param timestamp2 Second timestamp
- * @returns Difference in UTC days
- */
- static dayDifferenceUTC(timestamp1, timestamp2) {
- const date1UTCMidnight = this.startOfUTCDay(new Date(timestamp1));
- const date2UTCMidnight = this.startOfUTCDay(new Date(timestamp2));
- const diffTime = Math.abs(date2UTCMidnight - date1UTCMidnight);
- const diffDays = Math.floor(diffTime / (1e3 * 60 * 60 * 24));
- return diffDays;
- }
- /**
- * Check if two dates are the same day, ignoring time.
- * @param date1 The first date.
- * @param date2 The second date.
- * @returns True if both dates fall on the same day, false otherwise.
- */
- static isSameDay(date1, date2) {
- return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate();
- }
-};
-
-// ui/sidebar/note-item-renderer.ts
-var NoteItemRenderer = class {
- constructor(plugin) {
- this.plugin = plugin;
- }
- async _populateNoteItemDetails(noteEl, note, dateStr, selectedNotesArray) {
- noteEl.dataset.notePath = note.path;
- noteEl.removeClass("overdue-note");
- noteEl.removeAttribute("title");
- if (dateStr === "Due notes") {
- noteEl.addClass("overdue-note");
- const daysOverdue = Math.abs(Math.floor((note.nextReviewDate - DateUtils.startOfDay()) / (24 * 60 * 60 * 1e3)));
- const originalDueDate = new Date(note.nextReviewDate).toLocaleDateString();
- noteEl.setAttribute("title", `Originally due: ${originalDueDate} (${daysOverdue} ${daysOverdue === 1 ? "day" : "days"} overdue)`);
- }
- if (selectedNotesArray.includes(note.path)) {
- noteEl.addClass("selected");
- } else {
- noteEl.removeClass("selected");
- }
- const titleEl = noteEl.querySelector(".review-note-title");
- if (titleEl) {
- const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
- titleEl.setText(file instanceof import_obsidian13.TFile ? file.basename : note.path);
- }
- const estimatedTime = await this.plugin.reviewScheduleService.estimateReviewTime(note.path);
- const formattedTime = EstimationUtils.formatTime(estimatedTime);
- const phaseEl = noteEl.querySelector(".review-note-phase");
- const timeElOld = noteEl.querySelector(".review-note-time");
- if (timeElOld)
- timeElOld.remove();
- if (phaseEl) {
- phaseEl.empty();
- phaseEl.removeClass("review-phase-initial", "review-phase-graduated", "review-phase-spaced");
- if (note.scheduleCategory === "initial") {
- const totalInitialSteps = this.plugin.settings.initialScheduleCustomIntervals.length;
- 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.addClass("review-phase-initial");
- } else {
- phaseEl.setText(note.scheduleCategory === "graduated" ? "Graduated" : "Spaced");
- phaseEl.addClass(note.scheduleCategory === "graduated" ? "review-phase-graduated" : "review-phase-spaced");
- const timeElNew = noteEl.createDiv("review-note-time");
- noteEl.insertBefore(timeElNew, phaseEl.nextSibling);
- EstimationUtils.formatTimeWithColor(estimatedTime, timeElNew);
- }
- }
- const buttonsEl = noteEl.querySelector(".review-note-buttons");
- let dragHandleEl = buttonsEl == null ? void 0 : buttonsEl.querySelector(".review-note-drag-handle");
- if (dragHandleEl) {
- const isDraggable = dateStr === "Due notes" || dateStr === "Today";
- dragHandleEl.classList.toggle("is-disabled", !isDraggable);
- if (isDraggable && !dragHandleEl.hasAttribute("draggable")) {
- } else if (!isDraggable) {
- noteEl.removeAttribute("draggable");
- }
- }
- const advanceBtn = noteEl.querySelector(".review-note-advance");
- if (advanceBtn) {
- const todayStartTs = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- const noteReviewDayStartTs = DateUtils.startOfDay(new Date(note.nextReviewDate));
- const isEligibleForAdvance = noteReviewDayStartTs > todayStartTs;
- advanceBtn.disabled = !isEligibleForAdvance;
- advanceBtn.style.display = isEligibleForAdvance ? "" : "none";
- }
- }
- async updateNoteItem(noteEl, note, dateStr, selectedNotesArray) {
- await this._populateNoteItemDetails(noteEl, note, dateStr, selectedNotesArray);
- }
- async renderNoteItem(notesContainer, noteToRender, dateStr, parentContainerForBulkActions, selectedNotesArray, lastSelectedNotePathRef, onSelectionChange, onNoteAction) {
- if (!parentContainerForBulkActions) {
- parentContainerForBulkActions = document.body;
- }
- const noteEl = notesContainer.createDiv("review-note-item");
- const titleEl = noteEl.createDiv({ cls: ["review-note-title", "sf-pointer-cursor"] });
- noteEl.createDiv("review-note-phase");
- const buttonsEl = noteEl.createDiv("review-note-buttons");
- const actionBtnsEl = buttonsEl.createDiv("review-note-actions");
- const reviewBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-review" });
- (0, import_obsidian13.setIcon)(reviewBtn, "play");
- reviewBtn.title = "Review";
- const advanceBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-advance" });
- (0, import_obsidian13.setIcon)(advanceBtn, "arrow-left-circle");
- advanceBtn.title = "Advance";
- const postponeBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-postpone" });
- (0, import_obsidian13.setIcon)(postponeBtn, "arrow-right-circle");
- postponeBtn.title = "Postpone";
- const removeBtn = actionBtnsEl.createEl("button", { cls: "review-note-button review-note-remove" });
- (0, import_obsidian13.setIcon)(removeBtn, "trash-2");
- removeBtn.title = "Remove";
- const dragHandleEl = buttonsEl.createDiv("review-note-drag-handle");
- dragHandleEl.setAttribute("aria-label", "Drag to reorder");
- for (let i = 0; i < 3; i++) {
- dragHandleEl.createDiv("drag-handle-line");
- }
- titleEl.addEventListener("click", (e) => {
- e.stopPropagation();
- const path = noteEl.dataset.notePath;
- if (path)
- this.plugin.reviewController.openNoteWithoutReview(path);
- });
- reviewBtn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const path = noteEl.dataset.notePath;
- if (path) {
- await this.plugin.reviewController.reviewNote(path);
- await onNoteAction();
- }
- });
- advanceBtn.addEventListener("click", async (e) => {
- e.stopPropagation();
- if (advanceBtn.disabled)
- return;
- const path = noteEl.dataset.notePath;
- if (path) {
- await this.plugin.reviewController.advanceNote(path);
- await onNoteAction();
- }
- });
- postponeBtn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const path = noteEl.dataset.notePath;
- if (path) {
- try {
- await this.plugin.reviewController.postponeNote(path);
- await this.plugin.savePluginData();
- new import_obsidian13.Notice(`Note postponed`);
- await onNoteAction();
- } catch (error) {
- new import_obsidian13.Notice("Failed to postpone note.");
- await onNoteAction();
- }
- }
- });
- removeBtn.addEventListener("click", async (e) => {
- e.stopPropagation();
- const path = noteEl.dataset.notePath;
- if (path) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- const confirmed = confirm(`Remove "${file instanceof import_obsidian13.TFile ? file.basename : path}" from review schedule?`);
- if (!confirmed)
- return;
- try {
- await this.plugin.reviewScheduleService.removeFromReview(path);
- await this.plugin.savePluginData();
- new import_obsidian13.Notice(`Note removed from review schedule`);
- await onNoteAction();
- } catch (error) {
- new import_obsidian13.Notice("Failed to remove note from schedule.");
- await onNoteAction();
- }
- }
- });
- dragHandleEl.addEventListener("mousedown", (e) => {
- e.stopPropagation();
- if (!dragHandleEl.classList.contains("is-disabled")) {
- noteEl.setAttribute("draggable", "true");
- }
- });
- noteEl.addEventListener("dragstart", (e) => {
- var _a;
- const path = noteEl.dataset.notePath;
- if (path && noteEl.getAttribute("draggable") === "true") {
- (_a = e.dataTransfer) == null ? void 0 : _a.setData("text/plain", path);
- } else {
- e.preventDefault();
- }
- });
- noteEl.addEventListener("dragend", () => {
- noteEl.removeAttribute("draggable");
- });
- noteEl.addEventListener("click", (e) => {
- e.stopPropagation();
- const currentPath = noteEl.dataset.notePath;
- if (!currentPath)
- return;
- const allVisibleNoteElements = Array.from(parentContainerForBulkActions.querySelectorAll(".review-note-item[data-note-path]"));
- const allVisibleNotePaths = allVisibleNoteElements.map((el) => el.dataset.notePath).filter((p2) => p2);
- const currentIndex = allVisibleNotePaths.indexOf(currentPath);
- if (e.shiftKey && lastSelectedNotePathRef.current && lastSelectedNotePathRef.current !== currentPath) {
- const lastClickedIndexInVisible = allVisibleNotePaths.indexOf(lastSelectedNotePathRef.current);
- if (lastClickedIndexInVisible !== -1 && currentIndex !== -1) {
- const start = Math.min(lastClickedIndexInVisible, currentIndex);
- const end = Math.max(lastClickedIndexInVisible, currentIndex);
- const notesToSelectInRange = allVisibleNotePaths.slice(start, end + 1);
- if (e.ctrlKey || e.metaKey) {
- notesToSelectInRange.forEach((p2) => {
- if (p2 && !selectedNotesArray.includes(p2))
- selectedNotesArray.push(p2);
- });
- } else {
- selectedNotesArray.length = 0;
- selectedNotesArray.push(...notesToSelectInRange.filter((p2) => p2));
- }
- } else {
- selectedNotesArray.length = 0;
- selectedNotesArray.push(currentPath);
- }
- } else if (e.ctrlKey || e.metaKey) {
- const indexInSelection = selectedNotesArray.indexOf(currentPath);
- if (indexInSelection > -1) {
- selectedNotesArray.splice(indexInSelection, 1);
- } else {
- selectedNotesArray.push(currentPath);
- }
- } else {
- selectedNotesArray.length = 0;
- selectedNotesArray.push(currentPath);
- }
- lastSelectedNotePathRef.current = currentPath;
- onSelectionChange();
- });
- noteEl.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- e.stopPropagation();
- const path = noteEl.dataset.notePath;
- if (!path)
- return;
- const menu = new import_obsidian13.Menu();
- menu.addItem((item) => item.setTitle("Open note").setIcon("file-text").onClick(() => this.plugin.reviewController.openNoteWithoutReview(path)));
- menu.addItem((item) => item.setTitle("Review note").setIcon("play-circle").onClick(async () => {
- this.plugin.reviewController.reviewNote(path);
- await onNoteAction();
- }));
- menu.addItem((item) => item.setTitle("Postpone by 1 day").setIcon("skip-forward").onClick(async () => {
- await this.plugin.reviewController.postponeNote(path, 1);
- await onNoteAction();
- }));
- const schedule = this.plugin.reviewScheduleService.schedules[path];
- if (schedule) {
- const todayStart = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- const noteReviewDayStart = DateUtils.startOfDay(new Date(schedule.nextReviewDate));
- if (noteReviewDayStart > todayStart) {
- menu.addItem((item) => item.setTitle("Advance note").setIcon("arrow-left-circle").onClick(async () => {
- await this.plugin.reviewController.advanceNote(path);
- await onNoteAction();
- }));
- }
- }
- menu.addItem((item) => item.setTitle("Remove from review").setIcon("trash").onClick(async () => {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- const confirmed = confirm(`Remove "${file instanceof import_obsidian13.TFile ? file.basename : path}" from review schedule?`);
- if (confirmed) {
- await this.plugin.reviewScheduleService.removeFromReview(path);
- await this.plugin.savePluginData();
- new import_obsidian13.Notice("Note removed from review schedule.");
- await onNoteAction();
- }
- }));
- menu.showAtMouseEvent(e);
- });
- await this._populateNoteItemDetails(noteEl, noteToRender, dateStr, selectedNotesArray);
- return noteEl;
- }
-};
-
-// ui/sidebar/list-view-renderer.ts
-var import_obsidian14 = require("obsidian");
-var ListViewRenderer = class {
- // Callback to trigger full refresh if needed
- constructor(plugin, pomodoroUIManager, noteItemRenderer, stateAccessors) {
- this.plugin = plugin;
- this.pomodoroUIManager = pomodoroUIManager;
- this.noteItemRenderer = noteItemRenderer;
- this.getActiveListBaseDate = stateAccessors.getActiveListBaseDate;
- this.getSelectedNotes = stateAccessors.getSelectedNotes;
- this.setSelectedNotes = stateAccessors.setSelectedNotes;
- this.getExpandedUpcomingDayKey = stateAccessors.getExpandedUpcomingDayKey;
- this.setExpandedUpcomingDayKey = stateAccessors.setExpandedUpcomingDayKey;
- this.getLastSelectedNotePath = stateAccessors.getLastSelectedNotePath;
- this.setLastSelectedNotePath = stateAccessors.setLastSelectedNotePath;
- this.refreshSidebarView = stateAccessors.refreshSidebarView;
- }
- /**
- * Render the list view content into the provided container.
- * @param container Container element for list view content
- */
- async render(container) {
- const activeListBaseDate = this.getActiveListBaseDate();
- const selectedNotes = this.getSelectedNotes();
- const dueNotesForStats = this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true);
- const notesForPomodoro = this.plugin.reviewController.getTodayNotes();
- await this._ensureAndUpdateReviewButtonsSection(container, notesForPomodoro, selectedNotes);
- this._ensureAndUpdateAllCaughtUpMessage(container, dueNotesForStats, activeListBaseDate);
- let notesToGroup;
- let shouldIncludeFutureInGrouping = false;
- if (activeListBaseDate) {
- notesToGroup = Object.values(this.plugin.reviewScheduleService.schedules);
- shouldIncludeFutureInGrouping = true;
- } else {
- notesToGroup = dueNotesForStats;
- }
- const groupedNotes = await this.groupNotesByDate(notesToGroup, shouldIncludeFutureInGrouping);
- const sortedDateKeys = this.getSortedDateKeys(groupedNotes);
- await this._ensureAndUpdateDateSections(container, sortedDateKeys, groupedNotes);
- this._ensureAndUpdateActiveSessionSection(container);
- if (!activeListBaseDate) {
- await this._ensureAndUpdateUpcomingReviewsSection(container);
- } else {
- const existingUpcomingSection = container.querySelector(".review-upcoming-section");
- if (existingUpcomingSection)
- existingUpcomingSection.remove();
- }
- this.updateBulkActionButtonsVisibility(container);
- }
- // private async _ensureAndUpdateStatsSection(container: HTMLElement, dueNotesForStats: ReviewSchedule[]): Promise {
- // let statsEl = container.querySelector(".review-stats-list-view") as HTMLElement;
- // if (!statsEl) {
- // statsEl = container.createDiv("review-stats-list-view");
- // }
- // let statsCountEl = statsEl.querySelector(".review-stats-count") as HTMLElement;
- // if (!statsCountEl) {
- // statsCountEl = statsEl.createEl("div", { cls: "review-stats-count" });
- // }
- // const overdueNotes = dueNotesForStats.filter(note => note.nextReviewDate < DateUtils.startOfDay());
- // let totalTime = 0;
- // for (const note of dueNotesForStats) {
- // totalTime += await this.plugin.reviewScheduleService.estimateReviewTime(note.path);
- // }
- // statsCountEl.setText(`${dueNotesForStats.length} notes - ${EstimationUtils.formatTime(totalTime)}${overdueNotes.length > 0 ? ` (${overdueNotes.length} overdue)` : ''}`);
- // }
- async _ensureAndUpdateReviewButtonsSection(container, notesForDisplay, selectedNotes) {
- let pomodoroContainer = container.querySelector(".sidebar-pomodoro-section");
- if (!pomodoroContainer) {
- pomodoroContainer = container.createDiv("sidebar-pomodoro-section");
- const pomodoroSectionContainerEl = pomodoroContainer.createDiv("sidebar-pomodoro-button-container");
- }
- if (this.pomodoroUIManager && pomodoroContainer) {
- const pomodoroSectionContainerEl = pomodoroContainer.querySelector(".sidebar-pomodoro-button-container");
- if (pomodoroSectionContainerEl) {
- this.pomodoroUIManager.attachAndRender(pomodoroSectionContainerEl);
- if (this.plugin.settings.pomodoroEnabled) {
- pomodoroContainer.style.display = "";
- this.pomodoroUIManager.showPomodoroSection(true);
- this.pomodoroUIManager.updatePomodoroUI();
- this.pomodoroUIManager.calculateAndDisplayEstimation();
- } else {
- pomodoroContainer.style.display = "none";
- this.pomodoroUIManager.showPomodoroSection(false);
- }
- }
- }
- let reviewButtonsContainer = container.querySelector(".review-buttons-container");
- if (notesForDisplay.length > 0) {
- if (!reviewButtonsContainer) {
- reviewButtonsContainer = container.createDiv("review-buttons-container");
- 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 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" });
- reviewAllMCQBtn.addEventListener("click", () => {
- this.plugin.reviewController.reviewAllNotesWithMCQ(true);
- });
- }
- }
- reviewButtonsContainer.style.display = "";
- let bulkActionButtons = container.querySelector(".review-bulk-actions");
- 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);
- this.setSelectedNotes([]);
- await this.refreshSidebarView();
- });
- const advanceSelectedBtn = bulkActionButtons.createEl("button", { text: "Advance Selected", cls: "review-bulk-button review-bulk-advance" });
- advanceSelectedBtn.addEventListener("click", async () => {
- const pathsToAdvance = [...this.getSelectedNotes()];
- if (pathsToAdvance.length === 0) {
- new import_obsidian14.Notice("No notes selected to advance.");
- return;
- }
- await this.plugin.reviewController.advanceNotes(pathsToAdvance);
- this.setSelectedNotes([]);
- await this.refreshSidebarView();
- });
- const postponeSelectedBtn = bulkActionButtons.createEl("button", { text: "Postpone Selected", cls: "review-bulk-button review-bulk-postpone" });
- postponeSelectedBtn.addEventListener("click", async () => {
- const pathsToPostpone = [...this.getSelectedNotes()];
- if (pathsToPostpone.length === 0) {
- new import_obsidian14.Notice("No notes selected to postpone.");
- return;
- }
- this.setSelectedNotes([]);
- await this.plugin.reviewController.postponeNotes(pathsToPostpone);
- await this.refreshSidebarView();
- });
- const removeSelectedBtn = bulkActionButtons.createEl("button", { text: "Remove Selected", cls: "review-bulk-button review-bulk-remove" });
- removeSelectedBtn.addEventListener("click", async () => {
- const pathsToRemove = [...this.getSelectedNotes()];
- 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 import_obsidian14.Notice(`Removed ${pathsToRemove.length} selected notes.`);
- });
- }
- this.updateBulkActionButtonsVisibility(container);
- } else if (reviewButtonsContainer) {
- reviewButtonsContainer.style.display = "none";
- const bulkActionButtons = container.querySelector(".review-bulk-actions");
- if (bulkActionButtons)
- bulkActionButtons.style.display = "none";
- }
- }
- _ensureAndUpdateAllCaughtUpMessage(container, dueNotesForStats, activeListBaseDate) {
- let caughtUpEl = container.querySelector(".review-all-caught-up");
- if (dueNotesForStats.length === 0 && !activeListBaseDate) {
- if (!caughtUpEl) {
- caughtUpEl = container.createDiv("review-all-caught-up");
- const statsEl = container.querySelector(".review-stats-list-view");
- const buttonsContainer = container.querySelector(".review-buttons-container");
- const anchor = buttonsContainer || statsEl;
- if (anchor && anchor.nextSibling) {
- container.insertBefore(caughtUpEl, anchor.nextSibling);
- } else if (anchor) {
- container.appendChild(caughtUpEl);
- } else {
- container.prepend(caughtUpEl);
- }
- }
- caughtUpEl.setText("All caught up! No notes due for review.");
- caughtUpEl.style.display = "";
- } else if (caughtUpEl) {
- caughtUpEl.style.display = "none";
- }
- }
- async _ensureAndUpdateDateSections(container, sortedDateKeys, groupedNotes) {
- const existingSectionElements = Array.from(container.querySelectorAll(".review-date-section"));
- const dataKeysInDom = new Set(existingSectionElements.map((el) => el.dataset.dateKey).filter(Boolean));
- const dataKeysFromData = new Set(sortedDateKeys);
- let notesDisplayed = false;
- for (const sectionEl of existingSectionElements) {
- if (!dataKeysFromData.has(sectionEl.dataset.dateKey)) {
- sectionEl.remove();
- }
- }
- for (const dateStr of sortedDateKeys) {
- const notesForSection = groupedNotes[dateStr];
- if (!notesForSection || notesForSection.length === 0)
- continue;
- notesDisplayed = true;
- let dateSectionEl = container.querySelector(`.review-date-section[data-date-key="${dateStr}"]`);
- let notesContainerEl;
- if (!dateSectionEl) {
- dateSectionEl = container.createDiv("review-date-section");
- dateSectionEl.dataset.dateKey = dateStr;
- const headerRow = dateSectionEl.createDiv("review-date-header");
- const headerContainer2 = headerRow.createDiv("review-date-header-container");
- headerContainer2.createEl("h3");
- const todayStart = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- const sectionDateKeyIsFuture = !["Due notes", "Today"].includes(dateStr) && (notesForSection[0] && DateUtils.startOfDay(new Date(notesForSection[0].nextReviewDate)) > todayStart);
- if (sectionDateKeyIsFuture) {
- const advanceAllBtn = headerContainer2.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 () => {
- 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);
- });
- }
- const postponeAllBtn = headerContainer2.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 () => {
- 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);
- });
- headerRow.createSpan("review-date-time");
- notesContainerEl = dateSectionEl.createDiv("review-notes-container");
- } else {
- notesContainerEl = dateSectionEl.querySelector(".review-notes-container");
- if (!notesContainerEl) {
- notesContainerEl = dateSectionEl.createDiv("review-notes-container");
- }
- }
- dateSectionEl.removeClass("review-date-section-overdue");
- const actualTodayStart = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- const isDefaultTodayView = !this.getActiveListBaseDate();
- if (dateStr === "Due notes") {
- dateSectionEl.addClass("review-date-section-overdue");
- }
- const headerContainer = dateSectionEl.querySelector(".review-date-header-container");
- const dateHeading = headerContainer.querySelector("h3");
- const reviewTimeEl = dateSectionEl.querySelector(".review-date-time");
- let displayHeader = dateStr;
- const noteCountText = `${notesForSection.length} ${notesForSection.length === 1 ? "note" : "notes"}`;
- if (dateStr !== "Due notes" && notesForSection.length > 0) {
- const actualGroupSampleDate = new Date(notesForSection[0].nextReviewDate);
- const formattedActualDate = actualGroupSampleDate.toLocaleDateString(void 0, { month: "short", day: "numeric" });
- if (["Today", "Tomorrow"].includes(dateStr) || dateStr.startsWith("In ")) {
- displayHeader = `${dateStr} (${formattedActualDate})`;
- }
- displayHeader += ` - ${noteCountText}`;
- } else {
- displayHeader = `${dateStr} - ${noteCountText}`;
- }
- dateHeading.setText(displayHeader);
- let overdueBadge = dateHeading.querySelector(".review-overdue-badge");
- const todayActualStart = DateUtils.startOfDay();
- const shouldShowOverdueBadge = dateStr === "Due notes";
- if (shouldShowOverdueBadge) {
- const overdueNotesInThisSection = notesForSection;
- if (overdueNotesInThisSection.length > 0) {
- const daysDiff = overdueNotesInThisSection.map((note) => {
- const referenceDateForDiff = isDefaultTodayView ? todayActualStart : DateUtils.startOfDay(this.getActiveListBaseDate());
- return Math.floor((referenceDateForDiff - new Date(note.nextReviewDate).getTime()) / (24 * 60 * 60 * 1e3));
- });
- const maxDays = Math.max(0, ...daysDiff.filter((d) => d >= 0 && !isNaN(d)));
- if (maxDays > 0) {
- if (!overdueBadge) {
- overdueBadge = dateHeading.createSpan("review-overdue-badge sf-overdue-badge");
- }
- overdueBadge.setText(` (${maxDays} ${maxDays === 1 ? "day" : "days"} overdue)`);
- overdueBadge.style.display = "";
- } else if (overdueBadge) {
- overdueBadge.style.display = "none";
- }
- } else if (overdueBadge) {
- overdueBadge.style.display = "none";
- }
- } else if (overdueBadge) {
- overdueBadge.style.display = "none";
- }
- let sectionTime = 0;
- for (const note of notesForSection) {
- sectionTime += await this.plugin.reviewScheduleService.estimateReviewTime(note.path);
- }
- reviewTimeEl.setText(`(${EstimationUtils.formatTime(sectionTime)})`);
- await this._updateOrRenderNoteList(notesContainerEl, notesForSection, dateStr, container);
- }
- let noNotesForDateMsg = container.querySelector(".review-no-notes-for-date");
- const activeListBaseDate = this.getActiveListBaseDate();
- if (activeListBaseDate && !notesDisplayed) {
- if (!noNotesForDateMsg) {
- noNotesForDateMsg = container.createDiv("review-no-notes-for-date");
- }
- noNotesForDateMsg.setText(`No notes scheduled on or after ${activeListBaseDate.toLocaleDateString()}.`);
- noNotesForDateMsg.style.display = "";
- } else if (noNotesForDateMsg) {
- noNotesForDateMsg.style.display = "none";
- }
- }
- _ensureAndUpdateActiveSessionSection(container) {
- const activeSession = this.plugin.reviewSessionService.getActiveSession();
- let sessionSection = container.querySelector(".review-session-section");
- if (activeSession) {
- if (!sessionSection) {
- sessionSection = container.createDiv("review-session-section");
- sessionSection.createEl("h3", { text: "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" });
- continueBtn.addEventListener("click", () => {
- });
- const endBtn = sessionSection.createEl("button", { text: "End Session", cls: "review-session-end" });
- endBtn.addEventListener("click", () => {
- this.plugin.reviewSessionService.setActiveSession(null);
- this.refreshSidebarView();
- });
- }
- sessionSection.style.display = "";
- sessionSection.querySelector(".review-session-name").setText(activeSession.name);
- sessionSection.querySelector(".review-session-progress").setText(`Progress: ${activeSession.currentIndex}/${activeSession.hierarchy.traversalOrder.length}`);
- const progressBar = sessionSection.querySelector(".review-session-progress-bar");
- 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";
- }
- }
- async _ensureAndUpdateUpcomingReviewsSection(container) {
- const allSchedules = Object.values(this.plugin.reviewScheduleService.schedules);
- const upcomingGroupedNotes = await this.groupNotesByDate(allSchedules, true);
- const upcomingKeys = this.getSortedDateKeys(upcomingGroupedNotes).filter((key) => {
- if (key === "Due notes")
- return false;
- const actualTodayStart = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- if (key === DateUtils.formatDate(actualTodayStart, "relative", null))
- return false;
- if (key === DateUtils.formatDate(DateUtils.addDays(actualTodayStart, 1), "relative", null))
- return false;
- return true;
- });
- let upcomingSection = container.querySelector(".review-upcoming-section");
- if (upcomingKeys.length > 0) {
- if (!upcomingSection) {
- upcomingSection = container.createDiv("review-upcoming-section");
- upcomingSection.createEl("h3", { text: "Upcoming Reviews" });
- upcomingSection.createDiv("review-upcoming-list");
- }
- upcomingSection.style.display = "";
- const upcomingListEl = upcomingSection.querySelector(".review-upcoming-list");
- if (!upcomingListEl)
- return;
- const existingDayItemElements = Array.from(upcomingListEl.querySelectorAll(".review-upcoming-day"));
- const dayKeysInDom = new Set(existingDayItemElements.map((el) => el.dataset.dayKey).filter(Boolean));
- for (const dayItemEl of existingDayItemElements) {
- if (!upcomingKeys.includes(dayItemEl.dataset.dayKey)) {
- dayItemEl.remove();
- }
- }
- for (const dayKey of upcomingKeys) {
- const notesForDay = upcomingGroupedNotes[dayKey];
- if (!notesForDay || notesForDay.length === 0) {
- const staleEmptyDayItem = upcomingListEl.querySelector(`.review-upcoming-day[data-day-key="${dayKey}"]`);
- if (staleEmptyDayItem)
- staleEmptyDayItem.remove();
- continue;
- }
- let dayItemEl = upcomingListEl.querySelector(`.review-upcoming-day[data-day-key="${dayKey}"]`);
- if (!dayItemEl) {
- dayItemEl = upcomingListEl.createDiv("review-upcoming-day");
- dayItemEl.addClass("clickable");
- dayItemEl.dataset.dayKey = dayKey;
- const daySummary = dayItemEl.createDiv("review-upcoming-day-summary");
- daySummary.createEl("span", { cls: "review-upcoming-day-name" });
- dayItemEl.addEventListener("click", async () => {
- const currentDayKey = dayItemEl.dataset.dayKey;
- if (!currentDayKey)
- return;
- const expandedUpcomingDayKey = this.getExpandedUpcomingDayKey();
- const isCurrentlyExpanded = expandedUpcomingDayKey === currentDayKey;
- this.setExpandedUpcomingDayKey(isCurrentlyExpanded ? null : currentDayKey);
- await this.refreshSidebarView();
- });
- }
- const daySummaryNameEl = dayItemEl.querySelector(".review-upcoming-day-summary .review-upcoming-day-name");
- let upcomingDisplayHeader = dayKey;
- if (notesForDay.length > 0) {
- const sampleUpcomingDate = new Date(notesForDay[0].nextReviewDate);
- const formattedUpcomingDate = DateUtils.formatDate(sampleUpcomingDate.getTime(), "medium");
- if (["Today", "Tomorrow"].includes(dayKey) || dayKey.startsWith("In ")) {
- upcomingDisplayHeader = `${dayKey} (${formattedUpcomingDate})`;
- } else {
- upcomingDisplayHeader = formattedUpcomingDate;
- }
- }
- if (daySummaryNameEl)
- daySummaryNameEl.setText(`${upcomingDisplayHeader}: ${notesForDay.length} ${notesForDay.length === 1 ? "note" : "notes"}`);
- const isExpanded = this.getExpandedUpcomingDayKey() === dayKey;
- dayItemEl.classList.toggle("is-expanded", isExpanded);
- let notesContainerEl = dayItemEl.querySelector(".review-upcoming-notes-container");
- if (isExpanded) {
- if (!notesContainerEl) {
- notesContainerEl = dayItemEl.createDiv("review-upcoming-notes-container");
- }
- notesContainerEl.style.display = "";
- await this._updateOrRenderNoteList(notesContainerEl, notesForDay, dayKey, container);
- } else if (notesContainerEl) {
- notesContainerEl.style.display = "none";
- }
- }
- } else if (upcomingSection) {
- upcomingSection.style.display = "none";
- }
- }
- /**
- * Renders or updates a list of notes within a given container.
- */
- async _updateOrRenderNoteList(notesContainer, notes, dateStr, parentContainerForBulkActions) {
- const existingNoteElements = Array.from(notesContainer.querySelectorAll(".review-note-item[data-note-path]"));
- const existingNotesMap = new Map(existingNoteElements.map((el) => [el.dataset.notePath, el]));
- const notesInOrder = [];
- const lastSelectedNotePath = this.getLastSelectedNotePath();
- const lastSelectedNotePathRef = { current: lastSelectedNotePath };
- for (const note of notes) {
- let noteEl = existingNotesMap.get(note.path);
- if (noteEl) {
- await this.noteItemRenderer.updateNoteItem(
- noteEl,
- note,
- dateStr,
- this.getSelectedNotes()
- );
- existingNotesMap.delete(note.path);
- } else {
- noteEl = await this.noteItemRenderer.renderNoteItem(
- notesContainer,
- // Temporarily append here, will be reordered
- note,
- dateStr,
- parentContainerForBulkActions,
- this.getSelectedNotes(),
- lastSelectedNotePathRef,
- () => this.handleSelectionChange(parentContainerForBulkActions),
- this.handleNoteAction.bind(this)
- );
- }
- if (noteEl)
- notesInOrder.push(noteEl);
- }
- for (const staleNoteEl of existingNotesMap.values()) {
- staleNoteEl.remove();
- }
- notesContainer.empty();
- for (const noteEl of notesInOrder) {
- notesContainer.appendChild(noteEl);
- }
- this.setLastSelectedNotePath(lastSelectedNotePathRef.current);
- }
- /**
- * Callback function passed to NoteItemRenderer to handle UI updates after selection changes.
- */
- handleSelectionChange(container) {
- this.updateSelectionClasses(container);
- this.updateBulkActionButtonsVisibility(container);
- }
- /**
- * Callback function passed to NoteItemRenderer for actions (like postpone, remove)
- * that require a broader UI update (potentially a full refresh or targeted updates).
- */
- async handleNoteAction() {
- await this.refreshSidebarView();
- }
- /**
- * Updates the 'selected' class on note items based on the selectedNotes array.
- * (Called by handleSelectionChange)
- */
- updateSelectionClasses(container) {
- const selectedNotes = this.getSelectedNotes();
- const allNoteElements = container.querySelectorAll(".review-note-item[data-note-path]");
- allNoteElements.forEach((el) => {
- const path = el.dataset.notePath;
- if (path && selectedNotes.includes(path)) {
- el.classList.add("selected");
- } else {
- el.classList.remove("selected");
- }
- });
- }
- /**
- * Updates the visibility of the bulk action buttons based on selection count.
- * (Called by handleSelectionChange and render)
- */
- updateBulkActionButtonsVisibility(container) {
- const selectedNotesPaths = this.getSelectedNotes();
- const bulkActionsContainer = container.querySelector(".review-bulk-actions");
- if (bulkActionsContainer) {
- bulkActionsContainer.style.display = selectedNotesPaths.length > 1 ? "flex" : "none";
- const advanceSelectedBtn = bulkActionsContainer.querySelector(".review-bulk-advance");
- if (advanceSelectedBtn) {
- if (selectedNotesPaths.length > 1) {
- const todayStart = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- const hasEligibleFutureNote = selectedNotesPaths.some((path) => {
- const schedule = this.plugin.reviewScheduleService.schedules[path];
- return schedule && DateUtils.startOfDay(new Date(schedule.nextReviewDate)) > todayStart;
- });
- advanceSelectedBtn.disabled = !hasEligibleFutureNote;
- advanceSelectedBtn.style.display = "";
- } else {
- advanceSelectedBtn.disabled = true;
- }
- }
- }
- }
- /**
- * Updates the main header statistics display.
- */
- async updateHeaderStats(container) {
- const headerStatsEl = container.querySelector(".review-stats-list-view .review-stats-count");
- if (!headerStatsEl)
- return;
- const dueNotesForStats = this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true);
- const overdueNotes = dueNotesForStats.filter((note) => note.nextReviewDate < DateUtils.startOfDay());
- let totalTime = 0;
- for (const note of dueNotesForStats) {
- totalTime += await this.plugin.reviewScheduleService.estimateReviewTime(note.path);
- }
- const statsText = `${dueNotesForStats.length} notes - ${EstimationUtils.formatTime(totalTime)}${overdueNotes.length > 0 ? ` (${overdueNotes.length} overdue)` : ""}`;
- headerStatsEl.textContent = statsText;
- const reviewButtonsContainer = container.querySelector(".review-buttons-container");
- if (reviewButtonsContainer) {
- reviewButtonsContainer.style.display = dueNotesForStats.length > 0 ? "" : "none";
- }
- this.updateBulkActionButtonsVisibility(container);
- const allCaughtUpEl = container.querySelector(".review-all-caught-up");
- const notesInCurrentContext = this.plugin.reviewController.getTodayNotes();
- if (allCaughtUpEl) {
- allCaughtUpEl.style.display = notesInCurrentContext.length === 0 ? "" : "none";
- if (notesInCurrentContext.length === 0) {
- 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.");
- const anchor = buttonsContainer;
- if (anchor && anchor.nextSibling) {
- container.insertBefore(newCaughtUpEl, anchor.nextSibling);
- } else if (anchor) {
- container.appendChild(newCaughtUpEl);
- } else {
- container.prepend(newCaughtUpEl);
- }
- }
- }
- /**
- * Updates the count and estimated time for a specific date section header, or removes the section if empty.
- */
- async updateSectionCounts(sectionEl, container) {
- if (!sectionEl || !container || !sectionEl.parentElement)
- return;
- const notesInSection = Array.from(sectionEl.querySelectorAll(".review-note-item[data-note-path]"));
- const count = notesInSection.length;
- if (count === 0) {
- sectionEl.remove();
- } else {
- const headerTextEl = sectionEl.querySelector(".review-date-header-container h3");
- const timeEl = sectionEl.querySelector(".review-date-time");
- const dateStr = sectionEl.dataset.dateKey;
- if (headerTextEl && timeEl && dateStr) {
- let sectionTime = 0;
- for (const noteEl of notesInSection) {
- const path = noteEl.dataset.notePath;
- if (path) {
- sectionTime += await this.plugin.reviewScheduleService.estimateReviewTime(path);
- }
- }
- timeEl.setText(`(${EstimationUtils.formatTime(sectionTime)})`);
- let displayHeader = dateStr;
- const noteCountText = `${count} ${count === 1 ? "note" : "notes"}`;
- const firstNotePath = notesInSection[0].dataset.notePath;
- const schedule = firstNotePath ? this.plugin.reviewScheduleService.schedules[firstNotePath] : null;
- if (dateStr !== "Due notes" && schedule) {
- const actualGroupSampleDate = new Date(schedule.nextReviewDate);
- const formattedActualDate = actualGroupSampleDate.toLocaleDateString(void 0, { month: "short", day: "numeric" });
- if (["Today", "Tomorrow"].includes(dateStr) || dateStr.startsWith("In ")) {
- displayHeader = `${dateStr} (${formattedActualDate})`;
- }
- displayHeader += ` - ${noteCountText}`;
- } else {
- displayHeader = `${dateStr} - ${noteCountText}`;
- }
- headerTextEl.textContent = displayHeader;
- let overdueBadge = headerTextEl.querySelector(".review-overdue-badge");
- if (dateStr === "Due notes") {
- const daysDiff = await Promise.all(notesInSection.map(async (noteEl) => {
- const path = noteEl.dataset.notePath;
- const noteSchedule = path ? this.plugin.reviewScheduleService.schedules[path] : null;
- return noteSchedule ? Math.abs(Math.floor((noteSchedule.nextReviewDate - DateUtils.startOfDay()) / (24 * 60 * 60 * 1e3))) : 0;
- }));
- const maxDays = Math.max(0, ...daysDiff.filter((d) => !isNaN(d)));
- if (maxDays > 0) {
- const badgeText = ` (${maxDays} ${maxDays === 1 ? "day" : "days"} overdue)`;
- if (!overdueBadge) {
- overdueBadge = headerTextEl.createSpan("review-overdue-badge sf-overdue-badge");
- }
- overdueBadge.setText(badgeText);
- } else if (overdueBadge) {
- overdueBadge.remove();
- }
- } else if (overdueBadge) {
- overdueBadge.remove();
- }
- }
- }
- await this.updateHeaderStats(container);
- }
- /**
- * Updates counts for all date sections currently in the DOM.
- */
- async updateAllSectionCounts(container) {
- const sections = container.querySelectorAll(".review-date-section");
- for (const section of Array.from(sections)) {
- await this.updateSectionCounts(section, container);
- }
- await this.updateHeaderStats(container);
- }
- /**
- * Group notes by their review date, considering activeListBaseDate.
- */
- async groupNotesByDate(notes, includeFuture = false) {
- const grouped = {};
- const actualTodayStart = DateUtils.startOfDay(/* @__PURE__ */ new Date());
- const activeListBaseDate = this.getActiveListBaseDate();
- const refDateForFilteringStart = activeListBaseDate ? DateUtils.startOfDay(new Date(activeListBaseDate)) : actualTodayStart;
- for (const note of notes) {
- const noteDate = new Date(note.nextReviewDate);
- const noteDateStart = DateUtils.startOfDay(noteDate);
- if (activeListBaseDate) {
- if (noteDateStart !== refDateForFilteringStart) {
- continue;
- }
- }
- let dateStr = DateUtils.formatDate(note.nextReviewDate, "relative", activeListBaseDate);
- if (!grouped[dateStr]) {
- grouped[dateStr] = [];
- }
- grouped[dateStr].push(note);
- }
- return grouped;
- }
- /**
- * Get sorted date keys in the preferred display order: Due notes, Today, Tomorrow, future dates.
- */
- getSortedDateKeys(groupedNotes) {
- const keys = Object.keys(groupedNotes);
- const dateOrder = { "Due notes": 0, "Today": 1, "Tomorrow": 2 };
- return keys.sort((a, b2) => {
- const aIsSpecial = a in dateOrder;
- const bIsSpecial = b2 in dateOrder;
- if (aIsSpecial && bIsSpecial)
- return dateOrder[a] - dateOrder[b2];
- if (aIsSpecial)
- return -1;
- if (bIsSpecial)
- return 1;
- const numAMatch = a.match(/^In (\d+) days$/);
- const numBMatch = b2.match(/^In (\d+) days$/);
- if (numAMatch && numBMatch)
- return parseInt(numAMatch[1]) - parseInt(numBMatch[1]);
- if (numAMatch)
- return -1;
- if (numBMatch)
- return 1;
- return a.localeCompare(b2);
- });
- }
-};
-
-// ui/calendar-view.ts
-var import_obsidian15 = require("obsidian");
-var CalendarView = class {
- /**
- * Initialize calendar view
- *
- * @param containerEl Container element
- * @param plugin Reference to the main plugin
- */
- constructor(containerEl, plugin) {
- /**
- * Reviews grouped by date
- */
- this.reviewsByDate = /* @__PURE__ */ new Map();
- // Persistent UI elements
- this.calendarHeaderEl = null;
- this.monthTitleEl = null;
- this.calendarGridEl = null;
- this.containerEl = containerEl;
- this.plugin = plugin;
- this.currentDate = /* @__PURE__ */ new Date();
- }
- /**
- * Render the calendar view
- */
- async render() {
- this.ensureCalendarBaseStructure();
- this.updateCalendarHeader();
- await this.loadReviewsData();
- if (this.calendarGridEl) {
- this.renderCalendarGridContent(this.calendarGridEl);
- }
- }
- ensureCalendarBaseStructure() {
- var _a, _b;
- if (!this.containerEl)
- return;
- let calendarContainer = this.containerEl.querySelector(".calendar-container");
- if (!calendarContainer) {
- calendarContainer = this.containerEl.createDiv("calendar-container");
- }
- if (!this.calendarHeaderEl || !calendarContainer.contains(this.calendarHeaderEl)) {
- (_a = this.calendarHeaderEl) == null ? void 0 : _a.remove();
- this.calendarHeaderEl = calendarContainer.createDiv("calendar-header");
- const prevMonthBtn = this.calendarHeaderEl.createDiv("calendar-nav-btn");
- (0, import_obsidian15.setIcon)(prevMonthBtn, "chevron-left");
- prevMonthBtn.addEventListener("click", () => {
- this.currentDate.setMonth(this.currentDate.getMonth() - 1);
- this.render();
- });
- this.monthTitleEl = this.calendarHeaderEl.createDiv("calendar-month-title");
- const nextMonthBtn = this.calendarHeaderEl.createDiv("calendar-nav-btn");
- (0, import_obsidian15.setIcon)(nextMonthBtn, "chevron-right");
- nextMonthBtn.addEventListener("click", () => {
- this.currentDate.setMonth(this.currentDate.getMonth() + 1);
- this.render();
- });
- const todayBtn = this.calendarHeaderEl.createDiv("calendar-today-btn");
- todayBtn.setText("Today");
- todayBtn.addEventListener("click", () => {
- this.currentDate = /* @__PURE__ */ new Date();
- this.render();
- });
- }
- if (!this.calendarGridEl || !calendarContainer.contains(this.calendarGridEl)) {
- (_b = this.calendarGridEl) == null ? void 0 : _b.remove();
- this.calendarGridEl = calendarContainer.createDiv("calendar-grid");
- }
- }
- /**
- * Update calendar header (month title)
- */
- updateCalendarHeader() {
- if (this.monthTitleEl) {
- this.monthTitleEl.setText(
- this.currentDate.toLocaleString("default", {
- month: "long",
- year: "numeric"
- })
- );
- }
- }
- /**
- * Load and organize review data by date
- */
- async loadReviewsData() {
- this.reviewsByDate = /* @__PURE__ */ 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();
- if (!this.reviewsByDate.has(dateKey)) {
- this.reviewsByDate.set(dateKey, {
- timestamp: scheduleDueDayStart,
- // Store the start of day timestamp
- notes: [],
- totalTime: 0
- });
- }
- const dateReviews = this.reviewsByDate.get(dateKey);
- if (dateReviews) {
- dateReviews.notes.push(schedule);
- dateReviews.totalTime += await this.plugin.reviewScheduleService.estimateReviewTime(schedule.path);
- }
- }
- }
- /**
- * Render or update the calendar grid content
- *
- * @param gridEl The calendar grid element to populate
- */
- renderCalendarGridContent(gridEl) {
- if (!gridEl.querySelector(".calendar-weekday")) {
- const weekdays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
- weekdays.forEach((day) => {
- const dayHeader = gridEl.createDiv("calendar-weekday");
- dayHeader.setText(day);
- });
- }
- const { year, month, firstDay, daysInMonth } = this.getCalendarData();
- const totalCells = 42;
- let dayCells = Array.from(gridEl.querySelectorAll(".calendar-day"));
- if (dayCells.length < totalCells) {
- for (let i = dayCells.length; i < totalCells; i++) {
- dayCells.push(gridEl.createDiv("calendar-day"));
- }
- } else if (dayCells.length > totalCells) {
- for (let i = totalCells; i < dayCells.length; i++) {
- dayCells[i].remove();
- }
- dayCells = dayCells.slice(0, totalCells);
- }
- let dayOfMonth = 1;
- for (let i = 0; i < totalCells; i++) {
- const dayCell = dayCells[i];
- dayCell.empty();
- dayCell.className = "calendar-day";
- dayCell.removeAttribute("data-date-key");
- dayCell.onclick = null;
- if (i >= firstDay && dayOfMonth <= daysInMonth) {
- const currentDateObj = new Date(Date.UTC(year, month, dayOfMonth));
- const cellDayStart = DateUtils.startOfUTCDay(currentDateObj);
- const dateKey = cellDayStart.toString();
- dayCell.dataset.dateKey = dateKey;
- const dayNumber = dayCell.createDiv("calendar-day-number");
- dayNumber.setText(dayOfMonth.toString());
- if (this.isToday(year, month, dayOfMonth)) {
- dayCell.addClass("today");
- }
- const dateReviews = this.reviewsByDate.get(dateKey);
- 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 = /* @__PURE__ */ new Date();
- const isClickedDateToday = DateUtils.isSameDay(currentDateObj, today);
- 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 import_obsidian15.Notice("Switched to list view. Sidebar will update.");
- }
- });
- if (dateReviews.notes.length > 10)
- dayCell.addClass("heavy-load");
- else if (dateReviews.notes.length > 5)
- dayCell.addClass("medium-load");
- else
- dayCell.addClass("light-load");
- }
- dayOfMonth++;
- } else {
- dayCell.addClass("empty");
- }
- }
- }
- /**
- * Get calendar data for the current month
- *
- * @returns Calendar data object
- */
- getCalendarData() {
- const year = this.currentDate.getFullYear();
- const month = this.currentDate.getMonth();
- const firstDay = new Date(year, month, 1).getDay();
- const daysInMonth = new Date(year, month + 1, 0).getDate();
- return { year, month, firstDay, daysInMonth };
- }
- /**
- * Check if a date is today
- *
- * @param year Year
- * @param month Month
- * @param day Day
- * @returns True if the date is today
- */
- isToday(year, month, day) {
- const today = /* @__PURE__ */ new Date();
- return today.getFullYear() === year && today.getMonth() === month && today.getDate() === day;
- }
-};
-
-// ui/sidebar-view.ts
-var ReviewSidebarView = class extends import_obsidian16.ItemView {
- constructor(leaf, plugin) {
- super(leaf);
- this.activeListBaseDate = null;
- this.selectedNotes = [];
- this.lastSelectedNotePath = null;
- this.lastScrollPosition = 0;
- this.expandedUpcomingDayKey = null;
- // State for upcoming section
- this.resizeObserver = null;
- this.listViewRenderer = null;
- // Persistent UI elements
- this.mainContainer = null;
- this.persistentHeaderEl = null;
- this.listViewContentEl = null;
- this.calendarViewContentEl = null;
- this.plugin = plugin;
- this.noteItemRenderer = new NoteItemRenderer(this.plugin);
- }
- getViewType() {
- return "spaceforge-review-schedule";
- }
- getDisplayText() {
- return "Spaceforge Review";
- }
- getIcon() {
- return "calendar-clock";
- }
- async onOpen() {
- this.plugin.events.on("sidebar-update", this.refresh.bind(this));
- this.plugin.events.on("pomodoro-update", () => {
- if (this.pomodoroUIManager) {
- this.pomodoroUIManager.updatePomodoroUI();
- }
- });
- await this.refresh();
- }
- async onClose() {
- if (this.resizeObserver) {
- this.resizeObserver.disconnect();
- }
- this.plugin.events.off("sidebar-update", this.refresh.bind(this));
- }
- ensureBaseStructure() {
- const contentEl = this.containerEl;
- if (!contentEl)
- return;
- if (!this.mainContainer) {
- contentEl.empty();
- this.mainContainer = contentEl.createDiv("spaceforge-container");
- }
- if (!this.persistentHeaderEl) {
- this.persistentHeaderEl = this.mainContainer.createDiv("review-header");
- this.persistentHeaderEl.createEl("h2", { text: "Review Schedule" });
- const viewToggle = this.persistentHeaderEl.createDiv("review-view-toggle");
- const listViewBtn = viewToggle.createDiv("review-view-btn");
- listViewBtn.setText("List");
- listViewBtn.addEventListener("click", async () => {
- if (this.plugin.settings.sidebarViewType === "list")
- return;
- this.plugin.settings.sidebarViewType = "list";
- this.activeListBaseDate = null;
- await this.plugin.savePluginData();
- await this.refresh();
- });
- const calendarViewBtn = viewToggle.createDiv("review-view-btn");
- calendarViewBtn.setText("Calendar");
- calendarViewBtn.addEventListener("click", async () => {
- if (this.plugin.settings.sidebarViewType === "calendar")
- return;
- this.plugin.settings.sidebarViewType = "calendar";
- await this.plugin.savePluginData();
- await this.refresh();
- });
- }
- this.updateViewToggleButtonsState();
- if (!this.listViewContentEl) {
- this.listViewContentEl = this.mainContainer.createDiv("list-view-content");
- }
- if (!this.pomodoroUIManager) {
- this.pomodoroUIManager = new PomodoroUIManager(this.plugin);
- }
- if (!this.listViewRenderer) {
- this.listViewRenderer = new ListViewRenderer(this.plugin, this.pomodoroUIManager, this.noteItemRenderer, {
- getActiveListBaseDate: () => this.activeListBaseDate,
- getSelectedNotes: () => this.selectedNotes,
- setSelectedNotes: (notes) => {
- this.selectedNotes = notes;
- },
- getExpandedUpcomingDayKey: () => this.expandedUpcomingDayKey,
- setExpandedUpcomingDayKey: (key) => {
- this.expandedUpcomingDayKey = key;
- },
- getLastSelectedNotePath: () => this.lastSelectedNotePath,
- setLastSelectedNotePath: (path) => {
- this.lastSelectedNotePath = path;
- },
- refreshSidebarView: this.refresh.bind(this)
- });
- }
- if (!this.calendarViewContentEl) {
- this.calendarViewContentEl = this.mainContainer.createDiv("calendar-view-content");
- }
- if (!this.calendarView && this.calendarViewContentEl) {
- this.calendarView = new CalendarView(this.calendarViewContentEl, this.plugin);
- }
- }
- updateViewToggleButtonsState() {
- if (!this.persistentHeaderEl)
- return;
- const viewToggle = this.persistentHeaderEl.querySelector(".review-view-toggle");
- if (!viewToggle)
- return;
- const listViewBtn = viewToggle.children[0];
- const calendarViewBtn = viewToggle.children[1];
- if (listViewBtn)
- listViewBtn.classList.toggle("active", this.plugin.settings.sidebarViewType === "list");
- if (calendarViewBtn)
- calendarViewBtn.classList.toggle("active", this.plugin.settings.sidebarViewType === "calendar");
- }
- async refresh() {
- const previousActiveListBaseDateEpoch = this.activeListBaseDate ? DateUtils.startOfUTCDay(this.activeListBaseDate) : null;
- let newTargetDate = this.activeListBaseDate;
- if (this.plugin.clickedDateFromCalendar) {
- newTargetDate = this.plugin.clickedDateFromCalendar;
- this.plugin.clickedDateFromCalendar = null;
- if (this.plugin.settings.sidebarViewType !== "list") {
- this.plugin.settings.sidebarViewType = "list";
- await this.plugin.savePluginData();
- }
- }
- const newTargetDateEpoch = newTargetDate ? DateUtils.startOfUTCDay(newTargetDate) : null;
- let reviewDateChanged = false;
- if (newTargetDateEpoch !== previousActiveListBaseDateEpoch) {
- this.activeListBaseDate = newTargetDate;
- reviewDateChanged = true;
- }
- const currentControllerOverrideEpoch = this.plugin.reviewController.getCurrentReviewDateOverride();
- const targetControllerOverrideValue = this.activeListBaseDate ? DateUtils.startOfUTCDay(this.activeListBaseDate) : null;
- if (targetControllerOverrideValue !== currentControllerOverrideEpoch) {
- await this.plugin.reviewController.setReviewDateOverride(targetControllerOverrideValue);
- if (!reviewDateChanged) {
- reviewDateChanged = true;
- }
- }
- this.ensureBaseStructure();
- let storedScrollPosition = this.lastScrollPosition;
- if (this.mainContainer) {
- storedScrollPosition = this.mainContainer.scrollTop;
- }
- this.updateViewToggleButtonsState();
- await this.showCorrectViewPane();
- if (this.mainContainer) {
- requestAnimationFrame(() => {
- if (this.mainContainer)
- this.mainContainer.scrollTop = storedScrollPosition;
- this.lastScrollPosition = storedScrollPosition;
- });
- }
- }
- async showCorrectViewPane() {
- if (this.plugin.settings.sidebarViewType === "calendar") {
- if (this.listViewContentEl)
- this.listViewContentEl.hide();
- if (this.calendarViewContentEl) {
- this.calendarViewContentEl.show();
- await this.renderCalendarViewContent(this.calendarViewContentEl);
- }
- } else {
- if (this.calendarViewContentEl)
- this.calendarViewContentEl.hide();
- if (this.listViewContentEl) {
- this.listViewContentEl.show();
- await this.renderListViewContent(this.listViewContentEl);
- }
- }
- }
- /**
- * Render the list view content by delegating to ListViewRenderer.
- */
- async renderListViewContent(container) {
- if (this.listViewRenderer) {
- await this.listViewRenderer.render(container);
- } else {
- container.setText("Error: Could not render list view. Renderer not ready.");
- }
- }
- /**
- * Render the calendar view content
- */
- async renderCalendarViewContent(container) {
- if (this.calendarView) {
- await this.calendarView.render();
- } else {
- container.setText("Error: Could not render calendar view. CalendarView not ready.");
- return;
- }
- const resizeObserver = new ResizeObserver((entries) => {
- for (const entry of entries) {
- if (entry.target === this.containerEl) {
- this.updateCalendarContainerClass();
- }
- }
- });
- if (this.containerEl) {
- resizeObserver.observe(this.containerEl);
- this.resizeObserver = resizeObserver;
- this.updateCalendarContainerClass();
- }
- }
- updateCalendarContainerClass() {
- if (!this.containerEl || !this.calendarViewContentEl)
- return;
- const calendarContainer = this.calendarViewContentEl.querySelector(".calendar-container");
- if (calendarContainer) {
- const sidebarWidth = this.containerEl.clientWidth;
- const collapsedThreshold = 300;
- if (sidebarWidth < collapsedThreshold) {
- calendarContainer.classList.add("is-collapsed");
- } else {
- calendarContainer.classList.remove("is-collapsed");
- }
- }
- }
- // --- 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, note) {
- if (!this.listViewRenderer) {
- return;
- }
- const notes = await this.listViewRenderer.groupNotesByDate(
- this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true),
- false
- );
- const dateNotes = notes[dateStr];
- if (!dateNotes)
- return;
- const index = dateNotes.findIndex((n) => n.path === note.path);
- if (index <= 0)
- return;
- const path1 = dateNotes[index].path;
- const path2 = dateNotes[index - 1].path;
- await this.plugin.reviewController.swapNotes(path1, path2);
- await this.refresh();
- new import_obsidian16.Notice(`Moved note up`);
- }
- /**
- * Move a note down in the list
- * (Existing Method - Consider moving to controller)
- */
- async moveNoteDown(dateStr, note) {
- if (!this.listViewRenderer) {
- return;
- }
- const notes = await this.listViewRenderer.groupNotesByDate(
- this.plugin.reviewScheduleService.getDueNotesWithCustomOrder(Date.now(), true),
- false
- );
- const dateNotes = notes[dateStr];
- if (!dateNotes)
- return;
- const index = dateNotes.findIndex((n) => n.path === note.path);
- if (index < 0 || index >= dateNotes.length - 1)
- return;
- const path1 = dateNotes[index].path;
- const path2 = dateNotes[index + 1].path;
- await this.plugin.reviewController.swapNotes(path1, path2);
- await this.refresh();
- new import_obsidian16.Notice(`Moved note down`);
- }
- /**
- * Group notes by their folder
- * (Existing Method - Consider moving to utility/service)
- */
- async groupNotesByFolder(notes) {
- var _a;
- const grouped = {};
- for (const note of notes) {
- const file = this.plugin.app.vault.getAbstractFileByPath(note.path);
- const folderPath = ((_a = file == null ? void 0 : file.parent) == null ? void 0 : _a.path) || "/";
- if (!grouped[folderPath]) {
- grouped[folderPath] = [];
- }
- grouped[folderPath].push(note);
- }
- return grouped;
- }
- // --- State Management for Obsidian View Lifecycle ---
- getViewState() {
- if (this.mainContainer) {
- 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,
- sidebarViewType: this.plugin.settings.sidebarViewType
- };
- }
- async setViewState(state) {
- var _a, _b, _c;
- if (!state)
- return;
- this.activeListBaseDate = state.activeListBaseDateISO ? new Date(state.activeListBaseDateISO) : null;
- this.expandedUpcomingDayKey = (_a = state.expandedUpcomingDayKey) != null ? _a : null;
- this.selectedNotes = (_b = state.selectedNotes) != null ? _b : [];
- this.lastScrollPosition = (_c = state.lastScrollPosition) != null ? _c : 0;
- if (state.sidebarViewType) {
- this.plugin.settings.sidebarViewType = state.sidebarViewType;
- }
- await this.refresh();
- }
-};
-
-// ui/settings-tab.ts
-var import_obsidian17 = require("obsidian");
-
-// models/settings.ts
-var DEFAULT_SETTINGS = {
- showNavigationNotifications: true,
- baseEase: 250,
- // 2.5 in SM-2 format (recommended default from the original algorithm)
- loadBalance: false,
- // Disable load balancing by default for pure SM-2 compliance
- maximumInterval: 365,
- // Cap at 1 year (extension to original SM-2)
- useCustomDataPath: false,
- customDataPath: "",
- // autoNextNote property removed, now always true by default
- notifyBeforeDue: 120,
- includeSubfolders: true,
- readingSpeed: 100,
- // Default WPM set to 100
- sidebarViewType: "calendar",
- useInitialSchedule: true,
- initialScheduleCustomIntervals: [0, 3, 7, 14, 30],
- // MCQ settings
- enableMCQ: true,
- mcqApiProvider: "ollama" /* Ollama */,
- // Default API provider set to Ollama
- openRouterApiKey: "",
- openRouterModel: "openai/gpt-4.1-mini",
- openaiApiKey: "",
- openaiModel: "gpt-3.5-turbo",
- ollamaApiUrl: "",
- ollamaModel: "",
- geminiApiKey: "",
- geminiModel: "gemini-pro",
- claudeApiKey: "",
- claudeModel: "claude-3-sonnet-20240229",
- togetherApiKey: "",
- togetherModel: "meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8",
- mcqPromptType: "detailed",
- mcqQuestionsPerNote: 4,
- mcqChoicesPerQuestion: 5,
- mcqQuestionAmountMode: "fixed" /* Fixed */,
- // Default to fixed number
- mcqWordsPerQuestion: 100,
- // Default words per question if mode is switched
- mcqTimeDeductionAmount: 0.5,
- mcqTimeDeductionSeconds: 90,
- mcqDifficulty: "advanced" /* Advanced */,
- mcqDeductFullMarkOnFirstFailure: true,
- mcqBasicSystemPrompt: "You are a tutor who creates clear, straightforward multiple-choice questions to test basic understanding of the given content. Focus on key concepts and important facts. Make questions simple and direct, with one clearly correct answer. Always mark the correct answer with [CORRECT] at the end of the line.",
- mcqAdvancedSystemPrompt: "You are an expert tutor who creates challenging but fair multiple-choice questions to test deep understanding of the given content. Generate questions that assess comprehension, application, and analysis, not just memorization. Make incorrect choices plausible to encourage critical thinking. Always mark the correct answer with [CORRECT] at the end of the line.",
- // Pomodoro Timer Defaults
- pomodoroEnabled: true,
- pomodoroSoundEnabled: true,
- pomodoroWorkDuration: 25,
- pomodoroShortBreakDuration: 5,
- pomodoroLongBreakDuration: 15,
- pomodoroSessionsUntilLongBreak: 4,
- // MCQ Question Regeneration Settings
- enableQuestionRegenerationOnRating: false,
- minSm2RatingForQuestionRegeneration: 4,
- // SM-2: 0 (Blackout) to 5 (Perfect Recall) - Defaulting to 4 (Correct with Hesitation)
- minFsrsRatingForQuestionRegeneration: 3,
- // FSRS: 1 (Again) to 4 (Easy) - Defaulting to 3 (Good)
- // Algorithm Defaults
- defaultSchedulingAlgorithm: "fsrs",
- fsrsParameters: {
- request_retention: 0.9,
- maximum_interval: 36500,
- enable_fuzz: true,
- // Default FSRS weights from FSRS-4.5-Anki
- w: [
- 0.4,
- 0.6,
- 2.4,
- 5.8,
- 4.93,
- 0.94,
- 0.86,
- 0.01,
- 1.49,
- 0.14,
- 0.94,
- 2.18,
- 0.05,
- 0.34,
- 1.26,
- 0.29,
- 2.61
- ],
- learning_steps: [1, 10],
- // 1 minute, 10 minutes
- enable_short_term: false
- // Default for enable_short_term
- },
- // Navigation Command Defaults
- enableNavigationCommands: false,
- navigationCommand: {
- modifiers: [],
- key: null
- },
- navigationCommandDelay: 500
-};
-
-// models/plugin-data.ts
-var DEFAULT_PLUGIN_STATE_DATA = {
- schedules: {},
- history: [],
- reviewSessions: { sessions: {}, activeSessionId: null },
- mcqSets: {},
- mcqSessions: {},
- customNoteOrder: [],
- lastLinkAnalysisTimestamp: null,
- version: "0.0.0",
- // This will be updated from plugin.manifest.version on save
- // Pomodoro Timer State Defaults
- pomodoroCurrentMode: "idle",
- pomodoroTimeLeftInSeconds: 25 * 60,
- // Default to work duration
- pomodoroSessionsCompletedInCycle: 0,
- pomodoroIsRunning: false,
- pomodoroEndTimeMs: null,
- // Pomodoro Estimation and Cycle Tracking Defaults
- pomodoroEstimatedTotalCycles: null,
- pomodoroEstimatedWorkSessions: null,
- pomodoroIsEstimationActive: false,
- pomodoroUserHasModifiedSettings: false,
- // User Override Time Settings Defaults
- pomodoroUserOverrideHours: 0,
- pomodoroUserOverrideMinutes: 0,
- pomodoroUserAddToEstimation: false
-};
-var DEFAULT_APP_DATA = {
- settings: DEFAULT_SETTINGS,
- pluginState: DEFAULT_PLUGIN_STATE_DATA
-};
-
-// ui/settings-tab.ts
-var SpaceforgeSettingTab = class extends import_obsidian17.PluginSettingTab {
- /**
- * Initialize settings tab
- *
- * @param app Obsidian app
- * @param plugin Reference to the main plugin
- */
- constructor(app, plugin) {
- super(app, plugin);
- this.plugin = plugin;
- }
- /**
- * Display settings
- */
- display() {
- const { containerEl } = this;
- containerEl.empty();
- const createCollapsible = (title, iconName, defaultOpen = true) => {
- const sectionContainer = containerEl.createEl("div", { cls: "sf-settings-section" });
- const header = sectionContainer.createEl("div", { cls: "sf-settings-section-header" });
- if (iconName) {
- const iconEl = header.createEl("span", { cls: "sf-settings-icon" });
- (0, import_obsidian17.setIcon)(iconEl, iconName);
- }
- header.createEl("h3", { text: title });
- const collapseIndicator = header.createEl("span", {
- cls: "sf-settings-collapse-indicator",
- text: defaultOpen ? "\u25BE" : "\u25B8"
- });
- const contentContainer = sectionContainer.createEl("div", {
- cls: "sf-settings-section-content"
- });
- if (!defaultOpen) {
- contentContainer.style.display = "none";
- }
- header.addEventListener("click", () => {
- const isVisible = contentContainer.style.display !== "none";
- contentContainer.style.display = isVisible ? "none" : "block";
- collapseIndicator.textContent = isVisible ? "\u25B8" : "\u25BE";
- });
- return contentContainer;
- };
- const createActionButtons = () => {
- const actionsContainer = containerEl.createEl("div", { cls: "sf-settings-actions" });
- const exportBtn = actionsContainer.createEl("button", { text: "Export all data" });
- exportBtn.addEventListener("click", () => {
- var _a, _b, _c, _d, _e, _f, _g, _h;
- const pluginStateToExport = {
- schedules: this.plugin.reviewScheduleService.schedules,
- history: this.plugin.reviewHistoryService.history,
- reviewSessions: this.plugin.reviewSessionService.reviewSessions,
- mcqSets: this.plugin.mcqService.mcqSets,
- mcqSessions: this.plugin.mcqService.mcqSessions,
- customNoteOrder: this.plugin.reviewScheduleService.customNoteOrder,
- lastLinkAnalysisTimestamp: this.plugin.reviewScheduleService.lastLinkAnalysisTimestamp,
- version: this.plugin.manifest.version,
- pomodoroCurrentMode: this.plugin.pluginState.pomodoroCurrentMode,
- pomodoroTimeLeftInSeconds: this.plugin.pluginState.pomodoroTimeLeftInSeconds,
- pomodoroSessionsCompletedInCycle: this.plugin.pluginState.pomodoroSessionsCompletedInCycle,
- pomodoroIsRunning: this.plugin.pluginState.pomodoroIsRunning,
- pomodoroEndTimeMs: (_a = this.plugin.pluginState.pomodoroEndTimeMs) != null ? _a : null,
- // Add the missing field
- pomodoroEstimatedTotalCycles: (_b = this.plugin.pluginState.pomodoroEstimatedTotalCycles) != null ? _b : null,
- pomodoroEstimatedWorkSessions: (_c = this.plugin.pluginState.pomodoroEstimatedWorkSessions) != null ? _c : null,
- pomodoroIsEstimationActive: (_d = this.plugin.pluginState.pomodoroIsEstimationActive) != null ? _d : false,
- pomodoroUserHasModifiedSettings: (_e = this.plugin.pluginState.pomodoroUserHasModifiedSettings) != null ? _e : false,
- pomodoroUserOverrideHours: (_f = this.plugin.pluginState.pomodoroUserOverrideHours) != null ? _f : 0,
- pomodoroUserOverrideMinutes: (_g = this.plugin.pluginState.pomodoroUserOverrideMinutes) != null ? _g : 0,
- pomodoroUserAddToEstimation: (_h = this.plugin.pluginState.pomodoroUserAddToEstimation) != null ? _h : false
- };
- const dataToExport = {
- settings: this.plugin.settings,
- pluginState: pluginStateToExport
- };
- const dataJson = JSON.stringify(dataToExport, null, 2);
- const blob = new Blob([dataJson], { type: "application/json" });
- const a = document.createElement("a");
- a.href = URL.createObjectURL(blob);
- a.download = "spaceforge-data.json";
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- new import_obsidian17.Notice("All plugin data exported successfully");
- });
- const importBtn = actionsContainer.createEl("button", { text: "Import all data" });
- importBtn.addEventListener("click", () => {
- const input = document.createElement("input");
- input.type = "file";
- input.accept = "application/json";
- input.onchange = async (e) => {
- var _a, _b, _c;
- const file = (_a = e.target.files) == null ? void 0 : _a[0];
- if (file) {
- try {
- const text = await file.text();
- const importedFullData = JSON.parse(text);
- if (!importedFullData || typeof importedFullData.settings !== "object" || typeof importedFullData.pluginState !== "object") {
- throw new Error("Invalid data file format. Expected settings and pluginState properties.");
- }
- this.plugin.settings = { ...DEFAULT_SETTINGS, ...importedFullData.settings };
- this.plugin.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA, ...importedFullData.pluginState };
- this.plugin.reviewScheduleService.schedules = this.plugin.pluginState.schedules || {};
- this.plugin.reviewHistoryService.history = this.plugin.pluginState.history || [];
- this.plugin.reviewSessionService.reviewSessions = this.plugin.pluginState.reviewSessions || { sessions: {}, activeSessionId: null };
- 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 = typeof this.plugin.pluginState.lastLinkAnalysisTimestamp === "number" ? this.plugin.pluginState.lastLinkAnalysisTimestamp : null;
- (_b = this.plugin.pomodoroService) == null ? void 0 : _b.onSettingsChanged();
- (_c = this.plugin.pomodoroService) == null ? void 0 : _c.reinitializeTimerFromState();
- this.plugin.initializeMCQComponents();
- await this.plugin.savePluginData();
- this.display();
- new import_obsidian17.Notice("All plugin data imported successfully. Plugin may require a reload for all changes to take effect.");
- } catch (error) {
- new import_obsidian17.Notice(`Failed to import data: ${error.message}`);
- }
- }
- };
- input.click();
- });
- const resetBtn = actionsContainer.createEl("button", { text: "Reset to defaults" });
- resetBtn.addEventListener("click", async () => {
- var _a, _b, _c;
- const confirmed = confirm("Are you sure you want to reset all plugin data (settings and state) to defaults? This cannot be undone.");
- if (confirmed) {
- this.plugin.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
- this.plugin.pluginState = JSON.parse(JSON.stringify(DEFAULT_PLUGIN_STATE_DATA));
- 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 = (_a = this.plugin.pluginState.lastLinkAnalysisTimestamp) != null ? _a : null;
- (_b = this.plugin.pomodoroService) == null ? void 0 : _b.onSettingsChanged();
- (_c = this.plugin.pomodoroService) == null ? void 0 : _c.reinitializeTimerFromState();
- this.plugin.initializeMCQComponents();
- await this.plugin.savePluginData();
- this.display();
- new import_obsidian17.Notice("All plugin data reset to defaults.");
- }
- });
- const clearScheduleBtn = actionsContainer.createEl("button", {
- text: "Clear all schedule data",
- cls: "sf-button-danger"
- // Optional: Add a class for dangerous actions
- });
- clearScheduleBtn.addEventListener("click", async () => {
- var _a;
- 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 {
- 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];
- 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;
- if (this.plugin.reviewController) {
- await this.plugin.reviewController.updateTodayNotes();
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- await this.plugin.savePluginData();
- new import_obsidian17.Notice("All schedule data cleared successfully.");
- this.display();
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- } catch (error) {
- new import_obsidian17.Notice("Failed to clear schedule data. Check console for details.");
- }
- }
- });
- return actionsContainer;
- };
- const spacedRepSection = createCollapsible("Spaced repetition", "calendar-clock", true);
- new import_obsidian17.Setting(spacedRepSection).setName("Algorithm configuration").setHeading();
- const algoSelectionSetting = new import_obsidian17.Setting(spacedRepSection).setName("Default scheduling algorithm").setDesc("Choose the default algorithm for newly created notes.").addDropdown((dropdown) => dropdown.addOption("sm2", "SM-2").addOption("fsrs", "FSRS").setValue(this.plugin.settings.defaultSchedulingAlgorithm).onChange(async (value) => {
- this.plugin.settings.defaultSchedulingAlgorithm = value;
- await this.plugin.savePluginData();
- this.display();
- }));
- const aboutAlgoContainer = spacedRepSection.createEl("div", { cls: "sf-info-box sf-algo-about-box" });
- this.renderAboutAlgorithmSection(aboutAlgoContainer, this.plugin.settings.defaultSchedulingAlgorithm);
- const sm2ParamsContainer = spacedRepSection.createEl("details", { cls: "sf-settings-collapsible-subsection" });
- const sm2Summary = sm2ParamsContainer.createEl("summary");
- sm2Summary.setText("SM-2 parameters");
- sm2ParamsContainer.open = false;
- new import_obsidian17.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).").addSlider((slider) => slider.setLimits(130, 500, 10).setValue(this.plugin.settings.baseEase).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.baseEase = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.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.").addToggle((toggle) => toggle.setValue(this.plugin.settings.useInitialSchedule).onChange(async (value) => {
- this.plugin.settings.useInitialSchedule = value;
- await this.plugin.savePluginData();
- this.display();
- }));
- if (this.plugin.settings.useInitialSchedule) {
- new import_obsidian17.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.").addText((text) => text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(", ")).onChange(async (value) => {
- const intervals = value.split(",").map((s) => parseInt(s.trim())).filter((n) => !isNaN(n) && n >= 0);
- if (intervals.length > 0 && intervals[0] === 0) {
- this.plugin.settings.initialScheduleCustomIntervals = intervals;
- await this.plugin.savePluginData();
- } else {
- new import_obsidian17.Notice("Custom initial SM-2 intervals must start with 0 and be valid numbers.", 5e3);
- text.setValue(this.plugin.settings.initialScheduleCustomIntervals.join(", "));
- }
- }));
- }
- new import_obsidian17.Setting(sm2ParamsContainer).setName("SM-2: Maximum interval (days)").setDesc("Longest possible interval between SM-2 reviews.").addText((text) => text.setValue(this.plugin.settings.maximumInterval.toString()).onChange(async (value) => {
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.maximumInterval = numValue;
- await this.plugin.savePluginData();
- }
- }));
- new import_obsidian17.Setting(sm2ParamsContainer).setName("SM-2: Load balancing").setDesc("Add slight randomness to SM-2 intervals to prevent reviews clumping on the same day.").addToggle((toggle) => toggle.setValue(this.plugin.settings.loadBalance).onChange(async (value) => {
- this.plugin.settings.loadBalance = value;
- await this.plugin.savePluginData();
- }));
- const fsrsParamsContainer = spacedRepSection.createEl("details", { cls: "sf-settings-collapsible-subsection" });
- const fsrsSummary = fsrsParamsContainer.createEl("summary");
- fsrsSummary.setText("FSRS parameters");
- fsrsParamsContainer.open = false;
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Request retention").setDesc("Desired recall probability (0.7-0.99, default: 0.9). Higher values mean more frequent reviews.").addText((text) => {
- var _a, _b, _c;
- return text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.request_retention) == null ? void 0 : _b.toString()) != null ? _c : DEFAULT_SETTINGS.fsrsParameters.request_retention.toString()).onChange(async (value) => {
- const numValue = parseFloat(value);
- if (!isNaN(numValue) && numValue >= 0.7 && numValue <= 0.99) {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, request_retention: numValue };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- } else {
- new import_obsidian17.Notice("FSRS Request Retention must be between 0.7 and 0.99.");
- }
- });
- });
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Maximum interval (days)").setDesc("Longest possible interval FSRS will schedule.").addText((text) => {
- var _a, _b, _c;
- return text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.maximum_interval) == null ? void 0 : _b.toString()) != null ? _c : DEFAULT_SETTINGS.fsrsParameters.maximum_interval.toString()).onChange(async (value) => {
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, maximum_interval: numValue };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- } else {
- new import_obsidian17.Notice("FSRS Maximum Interval must be a positive number.");
- }
- });
- });
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Learning steps (minutes)").setDesc("Comma-separated initial learning intervals in minutes (e.g., 1,10 for 1m, 10m).").addText((text) => {
- var _a, _b, _c;
- return text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.learning_steps) == null ? void 0 : _b.join(",")) != null ? _c : 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) {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, learning_steps: steps };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- } else if (value.trim() === "") {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, learning_steps: [] };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- } else {
- new import_obsidian17.Notice("FSRS Learning Steps must be valid comma-separated numbers > 0, or empty.");
- }
- });
- });
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Enable fuzz").setDesc("Add slight randomness to FSRS intervals (recommended).").addToggle((toggle) => {
- var _a, _b;
- return toggle.setValue((_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.enable_fuzz) != null ? _b : DEFAULT_SETTINGS.fsrsParameters.enable_fuzz).onChange(async (value) => {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, enable_fuzz: value };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- });
- });
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Enable short term scheduling").setDesc("Use FSRS short-term memory model (affects initial learning steps).").addToggle((toggle) => {
- var _a, _b;
- return toggle.setValue((_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.enable_short_term) != null ? _b : DEFAULT_SETTINGS.fsrsParameters.enable_short_term).onChange(async (value) => {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, enable_short_term: value };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- });
- });
- new import_obsidian17.Setting(fsrsParamsContainer).setName("Weights (w)").setDesc("FSRS algorithm parameters (17 numbers). Edit with caution. Default weights are generally good.").addTextArea((text) => {
- var _a, _b, _c;
- text.inputEl.rows = 3;
- text.inputEl.addClass("sf-full-width-textarea");
- text.setValue((_c = (_b = (_a = this.plugin.settings.fsrsParameters) == null ? void 0 : _a.w) == null ? void 0 : _b.join(",")) != null ? _c : 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))) {
- this.plugin.settings.fsrsParameters = { ...this.plugin.settings.fsrsParameters, w: weights };
- await this.plugin.savePluginData();
- this.plugin.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- } else {
- new import_obsidian17.Notice("FSRS Weights must be a comma-separated list of 17 valid numbers.");
- }
- });
- });
- const conversionContainer = spacedRepSection.createEl("details", { cls: "sf-settings-collapsible-subsection" });
- const conversionSummary = conversionContainer.createEl("summary");
- conversionSummary.setText("Card conversion utilities");
- conversionContainer.open = false;
- new import_obsidian17.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.").addButton((button) => button.setButtonText("Convert SM-2 to FSRS").setCta().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 import_obsidian17.Notice("Converting SM-2 cards to FSRS... This may take a moment.");
- await this.plugin.reviewScheduleService.convertAllSm2ToFsrs();
- await this.plugin.savePluginData();
- new import_obsidian17.Notice("All SM-2 cards have been converted to FSRS.");
- this.display();
- }
- }));
- new import_obsidian17.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.").addButton((button) => button.setButtonText("Convert FSRS to SM-2").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 import_obsidian17.Notice("Converting FSRS cards to SM-2... This may take a moment.");
- await this.plugin.reviewScheduleService.convertAllFsrsToSm2();
- await this.plugin.savePluginData();
- new import_obsidian17.Notice("All FSRS cards have been converted to SM-2.");
- this.display();
- }
- }));
- const interfaceSection = createCollapsible("Interface & behavior", "settings", false);
- new import_obsidian17.Setting(interfaceSection).setName("Display").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(interfaceSection).setName("Default view type").setDesc("Choose between list or calendar for the review sidebar").addDropdown((dropdown) => dropdown.addOption("list", "List view").addOption("calendar", "Calendar view").setValue(this.plugin.settings.sidebarViewType).onChange(async (value) => {
- var _a;
- this.plugin.settings.sidebarViewType = value;
- await this.plugin.savePluginData();
- (_a = this.plugin.getSidebarView()) == null ? void 0 : _a.refresh();
- }));
- new import_obsidian17.Setting(interfaceSection).setName("Show navigation notifications").setDesc("Display notifications when moving between notes").addToggle((toggle) => toggle.setValue(this.plugin.settings.showNavigationNotifications).onChange(async (value) => {
- this.plugin.settings.showNavigationNotifications = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(interfaceSection).setName("Review behavior").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(interfaceSection).setName("Include subfolders").setDesc("When adding a folder to review, include all notes in subfolders").addToggle((toggle) => toggle.setValue(this.plugin.settings.includeSubfolders).onChange(async (value) => {
- this.plugin.settings.includeSubfolders = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(interfaceSection).setName("Notification time").setDesc("Minutes before due time to notify about upcoming reviews (0 to disable)").addText((text) => text.setValue(this.plugin.settings.notifyBeforeDue.toString()).onChange(async (value) => {
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue >= 0) {
- this.plugin.settings.notifyBeforeDue = numValue;
- await this.plugin.savePluginData();
- }
- }));
- new import_obsidian17.Setting(interfaceSection).setName("Reading speed (WPM)").setDesc("Words per minute for estimating review time").addSlider((slider) => slider.setLimits(100, 500, 10).setValue(this.plugin.settings.readingSpeed).setDynamicTooltip().onChange(async (value) => {
- 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"
- });
- new import_obsidian17.Setting(interfaceSection).setName("Navigation command").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(interfaceSection).setName("Enable navigation command").setDesc("Execute a command with a slight delay after navigating to the next or previous note.").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableNavigationCommands).onChange(async (value) => {
- this.plugin.settings.enableNavigationCommands = value;
- await this.plugin.savePluginData();
- this.display();
- }));
- if (this.plugin.settings.enableNavigationCommands) {
- new import_obsidian17.Setting(interfaceSection).setName("Command to execute").setDesc("Click the button and press the desired hotkey.").addButton((button) => {
- const command = this.plugin.settings.navigationCommand;
- const hotkeyText = command.key ? [...command.modifiers, command.key].join(" + ") : "Click to set";
- button.setButtonText(hotkeyText).onClick(() => {
- button.setButtonText("...");
- const keydownHandler = (e) => {
- e.preventDefault();
- e.stopPropagation();
- const modifiers = [];
- if (e.ctrlKey)
- modifiers.push("Ctrl");
- if (e.metaKey)
- modifiers.push("Meta");
- if (e.altKey)
- modifiers.push("Alt");
- if (e.shiftKey)
- modifiers.push("Shift");
- let key = e.key;
- if (key === " ")
- key = "Space";
- if (!["Control", "Shift", "Alt", "Meta"].includes(key)) {
- this.plugin.settings.navigationCommand = {
- modifiers,
- key
- };
- this.plugin.savePluginData();
- document.removeEventListener("keydown", keydownHandler, { capture: true });
- this.display();
- }
- };
- document.addEventListener("keydown", keydownHandler, { capture: true });
- });
- });
- new import_obsidian17.Setting(interfaceSection).setName("Command execution delay (ms)").setDesc("How long to wait before executing the command after navigation.").addSlider((slider) => slider.setLimits(0, 2e3, 100).setValue(this.plugin.settings.navigationCommandDelay).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.navigationCommandDelay = value;
- await this.plugin.savePluginData();
- }));
- }
- const mcqSection = createCollapsible("Multiple choice questions", "newspaper", false);
- new import_obsidian17.Setting(mcqSection).setName("Enable MCQ feature").setDesc("Use AI-generated multiple-choice questions to test your knowledge").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableMCQ).onChange(async (value) => {
- this.plugin.settings.enableMCQ = value;
- await this.plugin.savePluginData();
- this.plugin.initializeMCQComponents();
- try {
- const apiSettings = {
- openRouterApiKey: this.plugin.settings.openRouterApiKey,
- enableMCQ: value,
- openRouterModel: this.plugin.settings.openRouterModel
- };
- window.localStorage.setItem("spaceforge-api-settings", JSON.stringify(apiSettings));
- } catch (e) {
- }
- this.display();
- }));
- if (this.plugin.settings.enableMCQ) {
- new import_obsidian17.Setting(mcqSection).setName("API configuration").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(mcqSection).setName("API provider").setDesc("Select the API provider for generating MCQs.").addDropdown((dropdown) => {
- dropdown.addOption("openrouter" /* OpenRouter */, "OpenRouter").addOption("openai" /* OpenAI */, "OpenAI").addOption("ollama" /* Ollama */, "Ollama").addOption("gemini" /* Gemini */, "Gemini").addOption("claude" /* Claude */, "Claude").addOption("together" /* Together */, "Together AI").setValue(this.plugin.settings.mcqApiProvider).onChange(async (value) => {
- this.plugin.settings.mcqApiProvider = value;
- await this.plugin.savePluginData();
- this.plugin.initializeMCQComponents();
- this.display();
- });
- });
- const provider = this.plugin.settings.mcqApiProvider;
- if (provider === "openrouter" /* OpenRouter */) {
- new import_obsidian17.Setting(mcqSection).setName("OpenRouter configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- const apiKeyContainer = mcqSection.createEl("div", { cls: "sf-setting-highlight" });
- new import_obsidian17.Setting(apiKeyContainer).setName("OpenRouter API key").setDesc("Required for generating MCQs via OpenRouter.").addText((text) => text.setPlaceholder("Enter your OpenRouter API key").setValue(this.plugin.settings.openRouterApiKey).onChange(async (value) => {
- this.plugin.settings.openRouterApiKey = value;
- await this.plugin.savePluginData();
- }));
- apiKeyContainer.createEl("div").setText("Get your API key at https://openrouter.ai/keys");
- new import_obsidian17.Setting(mcqSection).setName("OpenRouter model").setDesc("Model identifier from OpenRouter (e.g., openai/gpt-4.1-mini)").addText((text) => text.setPlaceholder("Enter OpenRouter model identifier").setValue(this.plugin.settings.openRouterModel).onChange(async (value) => {
- this.plugin.settings.openRouterModel = value;
- await this.plugin.savePluginData();
- }));
- } else if (provider === "openai" /* OpenAI */) {
- new import_obsidian17.Setting(mcqSection).setName("OpenAI configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("OpenAI API key").setDesc("Your OpenAI API key.").addText((text) => text.setPlaceholder("Enter your OpenAI API key (sk-...)").setValue(this.plugin.settings.openaiApiKey).onChange(async (value) => {
- this.plugin.settings.openaiApiKey = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("OpenAI model").setDesc("Model name (e.g., gpt-3.5-turbo, gpt-4)").addText((text) => text.setPlaceholder("Enter OpenAI model name").setValue(this.plugin.settings.openaiModel).onChange(async (value) => {
- this.plugin.settings.openaiModel = value;
- await this.plugin.savePluginData();
- }));
- } else if (provider === "ollama" /* Ollama */) {
- new import_obsidian17.Setting(mcqSection).setName("Ollama configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Ollama API URL").setDesc("URL of your running Ollama instance (e.g., http://localhost:11434)").addText((text) => text.setPlaceholder("http://localhost:11434").setValue(this.plugin.settings.ollamaApiUrl).onChange(async (value) => {
- this.plugin.settings.ollamaApiUrl = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("Ollama model").setDesc("Name of the Ollama model to use (e.g., llama3, mistral)").addText((text) => text.setPlaceholder("Enter Ollama model name").setValue(this.plugin.settings.ollamaModel).onChange(async (value) => {
- this.plugin.settings.ollamaModel = value;
- await this.plugin.savePluginData();
- }));
- } else if (provider === "gemini" /* Gemini */) {
- new import_obsidian17.Setting(mcqSection).setName("Gemini configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Gemini API key").setDesc("Your Google AI Gemini API key.").addText((text) => text.setPlaceholder("Enter your Gemini API key").setValue(this.plugin.settings.geminiApiKey).onChange(async (value) => {
- this.plugin.settings.geminiApiKey = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("Gemini model").setDesc("Model name (e.g., gemini-pro)").addText((text) => text.setPlaceholder("Enter Gemini model name").setValue(this.plugin.settings.geminiModel).onChange(async (value) => {
- this.plugin.settings.geminiModel = value;
- await this.plugin.savePluginData();
- }));
- } else if (provider === "claude" /* Claude */) {
- new import_obsidian17.Setting(mcqSection).setName("Claude configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Claude API key").setDesc("Your Anthropic Claude API key.").addText((text) => text.setPlaceholder("Enter your Claude API key").setValue(this.plugin.settings.claudeApiKey).onChange(async (value) => {
- this.plugin.settings.claudeApiKey = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("Claude model").setDesc("Model name (e.g., claude-3-opus-20240229, claude-3-sonnet-20240229)").addText((text) => text.setPlaceholder("Enter Claude model name").setValue(this.plugin.settings.claudeModel).onChange(async (value) => {
- this.plugin.settings.claudeModel = value;
- await this.plugin.savePluginData();
- }));
- } else if (provider === "together" /* Together */) {
- new import_obsidian17.Setting(mcqSection).setName("Together AI configuration").setHeading().setClass("sf-settings-subsection-provider-header");
- new import_obsidian17.Setting(mcqSection).setName("Together AI API key").setDesc("Your Together AI API key.").addText((text) => text.setPlaceholder("Enter your Together AI API key").setValue(this.plugin.settings.togetherApiKey).onChange(async (value) => {
- this.plugin.settings.togetherApiKey = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).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").setValue(this.plugin.settings.togetherModel).onChange(async (value) => {
- this.plugin.settings.togetherModel = value;
- await this.plugin.savePluginData();
- }));
- }
- new import_obsidian17.Setting(mcqSection).setName("Question generation (common)").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(mcqSection).setName("Question amount mode").setDesc("How to determine the number of questions per note.").addDropdown((dropdown) => dropdown.addOption("fixed" /* Fixed */, "Fixed Number").addOption("wordsPerQuestion" /* WordsPerQuestion */, "Per X Words").setValue(this.plugin.settings.mcqQuestionAmountMode).onChange(async (value) => {
- this.plugin.settings.mcqQuestionAmountMode = value;
- await this.plugin.savePluginData();
- this.display();
- }));
- if (this.plugin.settings.mcqQuestionAmountMode === "fixed" /* Fixed */) {
- new import_obsidian17.Setting(mcqSection).setName("Questions per note (Fixed)").setDesc("Number of questions to generate for each note.").addSlider((slider) => slider.setLimits(1, 10, 1).setValue(this.plugin.settings.mcqQuestionsPerNote).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.mcqQuestionsPerNote = value;
- await this.plugin.savePluginData();
- }));
- } else if (this.plugin.settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- new import_obsidian17.Setting(mcqSection).setName("Words per question target").setDesc("Generate approximately 1 question for every X words in the note.").addText((text) => text.setPlaceholder("100").setValue(this.plugin.settings.mcqWordsPerQuestion.toString()).onChange(async (value) => {
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.mcqWordsPerQuestion = numValue;
- await this.plugin.savePluginData();
- } else {
- new import_obsidian17.Notice("Words per question must be a positive number.");
- text.setValue(this.plugin.settings.mcqWordsPerQuestion.toString());
- }
- }));
- }
- new import_obsidian17.Setting(mcqSection).setName("Choices per question").setDesc("Number of answer choices for each question").addSlider((slider) => slider.setLimits(2, 6, 1).setValue(this.plugin.settings.mcqChoicesPerQuestion).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.mcqChoicesPerQuestion = value;
- await this.plugin.savePluginData();
- }));
- const mcqFormattingGrid = mcqSection.createEl("div", { cls: "sf-setting-grid" });
- const promptTypeContainer = mcqFormattingGrid.createEl("div");
- new import_obsidian17.Setting(promptTypeContainer).setName("Prompt type").setDesc("Format for MCQ generation").addDropdown((dropdown) => dropdown.addOption("basic", "Basic").addOption("detailed", "Detailed").setValue(this.plugin.settings.mcqPromptType).onChange(async (value) => {
- this.plugin.settings.mcqPromptType = value;
- await this.plugin.savePluginData();
- }));
- const difficultyContainer = mcqFormattingGrid.createEl("div");
- new import_obsidian17.Setting(difficultyContainer).setName("MCQ difficulty").setDesc("Complexity level").addDropdown((dropdown) => dropdown.addOption("basic" /* Basic */, "Basic recall").addOption("advanced" /* Advanced */, "Advanced understanding").setValue(this.plugin.settings.mcqDifficulty).onChange(async (value) => {
- this.plugin.settings.mcqDifficulty = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("Scoring").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(mcqSection).setName("Time deduction amount").setDesc("Score penalty for slow answers (0-1)").addSlider((slider) => slider.setLimits(0, 1, 0.1).setValue(this.plugin.settings.mcqTimeDeductionAmount).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.mcqTimeDeductionAmount = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("Time deduction threshold").setDesc("Apply penalty after this many seconds").addSlider((slider) => slider.setLimits(10, 120, 5).setValue(this.plugin.settings.mcqTimeDeductionSeconds).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.mcqTimeDeductionSeconds = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.Setting(mcqSection).setName("Deduct full mark on first failure").setDesc("If enabled, the score for a question will be 0 if the first attempt is incorrect.").addToggle((toggle) => toggle.setValue(this.plugin.settings.mcqDeductFullMarkOnFirstFailure).onChange(async (value) => {
- this.plugin.settings.mcqDeductFullMarkOnFirstFailure = value;
- await this.plugin.savePluginData();
- }));
- const systemPromptsContainer = mcqSection.createEl("details", { cls: "sf-system-prompts-container" });
- systemPromptsContainer.createEl("summary", { text: "System prompts (advanced)", cls: "sf-settings-subsection" });
- systemPromptsContainer.createEl("div", { text: "Basic difficulty prompt", cls: "sf-prompt-label" });
- const basicTextarea = systemPromptsContainer.createEl("textarea", {
- attr: {
- placeholder: "Enter system prompt for basic difficulty",
- rows: "6"
- },
- cls: "prompt-textarea"
- });
- basicTextarea.value = this.plugin.settings.mcqBasicSystemPrompt;
- basicTextarea.addEventListener("change", async () => {
- this.plugin.settings.mcqBasicSystemPrompt = basicTextarea.value;
- await this.plugin.savePluginData();
- });
- systemPromptsContainer.createEl("div", { text: "Advanced difficulty prompt", cls: "sf-prompt-label" });
- const advancedTextarea = systemPromptsContainer.createEl("textarea", {
- attr: {
- placeholder: "Enter system prompt for advanced difficulty",
- rows: "6"
- },
- cls: "prompt-textarea"
- });
- advancedTextarea.value = this.plugin.settings.mcqAdvancedSystemPrompt;
- advancedTextarea.addEventListener("change", async () => {
- this.plugin.settings.mcqAdvancedSystemPrompt = advancedTextarea.value;
- await this.plugin.savePluginData();
- });
- new import_obsidian17.Setting(mcqSection).setName("Advanced question behavior").setHeading().setClass("sf-settings-subsection-header");
- const regenerationSetting = new import_obsidian17.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.setValue(this.plugin.settings.enableQuestionRegenerationOnRating).onChange(async (value) => {
- this.plugin.settings.enableQuestionRegenerationOnRating = value;
- await this.plugin.savePluginData();
- this.display();
- }));
- if (this.plugin.settings.enableQuestionRegenerationOnRating) {
- new import_obsidian17.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)").addSlider((slider) => slider.setLimits(0, 5, 1).setValue(this.plugin.settings.minSm2RatingForQuestionRegeneration).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.minSm2RatingForQuestionRegeneration = value;
- await this.plugin.savePluginData();
- }));
- new import_obsidian17.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)").addSlider((slider) => slider.setLimits(1, 4, 1).setValue(this.plugin.settings.minFsrsRatingForQuestionRegeneration).setDynamicTooltip().onChange(async (value) => {
- this.plugin.settings.minFsrsRatingForQuestionRegeneration = value;
- await this.plugin.savePluginData();
- }));
- }
- } else {
- 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."
- });
- }
- const pomodoroSection = createCollapsible("Pomodoro timer", "timer", false);
- new import_obsidian17.Setting(pomodoroSection).setName("Enable pomodoro timer").setDesc("Show the Pomodoro timer in the sidebar.").addToggle((toggle) => toggle.setValue(this.plugin.settings.pomodoroEnabled).onChange(async (value) => {
- var _a, _b;
- this.plugin.settings.pomodoroEnabled = value;
- await this.plugin.savePluginData();
- (_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
- (_b = this.plugin.getSidebarView()) == null ? void 0 : _b.refresh();
- this.display();
- }));
- if (this.plugin.settings.pomodoroEnabled) {
- new import_obsidian17.Setting(pomodoroSection).setName("Timer durations (minutes)").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(pomodoroSection).setName("Work duration").setDesc("Length of a work session.").addText((text) => text.setPlaceholder("25").setValue(this.plugin.settings.pomodoroWorkDuration.toString()).onChange(async (value) => {
- var _a;
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.pomodoroWorkDuration = numValue;
- await this.plugin.savePluginData();
- (_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
- }
- }));
- new import_obsidian17.Setting(pomodoroSection).setName("Short break duration").setDesc("Length of a short break.").addText((text) => text.setPlaceholder("5").setValue(this.plugin.settings.pomodoroShortBreakDuration.toString()).onChange(async (value) => {
- var _a;
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.pomodoroShortBreakDuration = numValue;
- await this.plugin.savePluginData();
- (_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
- }
- }));
- new import_obsidian17.Setting(pomodoroSection).setName("Long break duration").setDesc("Length of a long break.").addText((text) => text.setPlaceholder("15").setValue(this.plugin.settings.pomodoroLongBreakDuration.toString()).onChange(async (value) => {
- var _a;
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.pomodoroLongBreakDuration = numValue;
- await this.plugin.savePluginData();
- (_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
- }
- }));
- new import_obsidian17.Setting(pomodoroSection).setName("Sessions until long break").setDesc("Number of work sessions before a long break starts.").addText((text) => text.setPlaceholder("4").setValue(this.plugin.settings.pomodoroSessionsUntilLongBreak.toString()).onChange(async (value) => {
- var _a;
- const numValue = parseInt(value);
- if (!isNaN(numValue) && numValue > 0) {
- this.plugin.settings.pomodoroSessionsUntilLongBreak = numValue;
- await this.plugin.savePluginData();
- (_a = this.plugin.pomodoroService) == null ? void 0 : _a.onSettingsChanged();
- }
- }));
- new import_obsidian17.Setting(pomodoroSection).setName("Notifications").setHeading().setClass("sf-settings-subsection-header");
- new import_obsidian17.Setting(pomodoroSection).setName("Enable sound notifications").setDesc("Play a sound at the end of each work/break session.").addToggle((toggle) => toggle.setValue(this.plugin.settings.pomodoroSoundEnabled).onChange(async (value) => {
- this.plugin.settings.pomodoroSoundEnabled = value;
- 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."
- });
- }
- createActionButtons();
- }
- renderAboutAlgorithmSection(container, algorithm) {
- container.empty();
- if (algorithm === "sm2") {
- container.createEl("h4", { text: "About the Modified SM-2 Algorithm" });
- 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."
- });
- 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: "Both cases reset the repetition count to 1 and update the ease factor." });
- const ratingsTable = container.createEl("table", { cls: "sf-ratings-table" });
- 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" });
- const row1 = tbody.insertRow();
- row1.createEl("td", { text: "0-2" });
- row1.createEl("td", { text: "Incorrect / struggled" });
- row1.createEl("td", { text: "Resets, shortest interval" });
- const row2 = tbody.insertRow();
- row2.createEl("td", { text: "3" });
- row2.createEl("td", { text: "Correct with difficulty" });
- row2.createEl("td", { text: "Small increase" });
- const row3 = tbody.insertRow();
- row3.createEl("td", { text: "4" });
- row3.createEl("td", { text: "Correct with hesitation" });
- row3.createEl("td", { text: "Moderate increase" });
- const row4 = tbody.insertRow();
- row4.createEl("td", { text: "5" });
- 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."
- });
- container.createEl("p", {
- text: "Key concepts in FSRS:"
- });
- 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)." });
- const ratingsTable = container.createEl("table", { cls: "sf-ratings-table" });
- const thead = ratingsTable.createTHead();
- const tbody = ratingsTable.createTBody();
- const headerRow = thead.insertRow();
- headerRow.createEl("th", { text: "Rating (1-4)" });
- headerRow.createEl("th", { text: "Description" });
- headerRow.createEl("th", { text: "Effect on Stability/Difficulty" });
- const row1 = tbody.insertRow();
- row1.createEl("td", { text: "1 (Again)" });
- row1.createEl("td", { text: "Forgot the card" });
- row1.createEl("td", { text: "Decreases stability, may increase difficulty" });
- const row2 = tbody.insertRow();
- row2.createEl("td", { text: "2 (Hard)" });
- 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: "Recalled correctly" });
- row3.createEl("td", { text: "Standard increase in stability" });
- const row4 = tbody.insertRow();
- row4.createEl("td", { text: "4 (Easy)" });
- row4.createEl("td", { text: "Recalled easily" });
- row4.createEl("td", { text: "Largest increase in stability" });
- }
- }
-};
-
-// api/openrouter-service.ts
-var import_obsidian18 = require("obsidian");
-var OpenRouterService = class {
- /**
- * Initialize OpenRouter service
- *
- * @param plugin Reference to the main plugin
- */
- constructor(plugin) {
- this.plugin = plugin;
- }
- /**
- * Generate MCQs for a note
- *
- * @param notePath Path to the note
- * @param noteContent Content of the note
- * @param settings Current plugin settings
- * @returns Generated MCQ set or null if failed
- */
- async generateMCQs(notePath, noteContent, settings) {
- if (!settings.openRouterApiKey) {
- new import_obsidian18.Notice("OpenRouter API key is not set. Please add it in the settings.");
- return null;
- }
- try {
- new import_obsidian18.Notice("Generating MCQs using OpenRouter...");
- let numQuestionsToGenerate;
- if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
- numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion));
- } else {
- numQuestionsToGenerate = settings.mcqQuestionsPerNote;
- }
- const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate);
- const response = await this.makeApiRequest(prompt, settings);
- const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
- if (questions.length === 0) {
- new import_obsidian18.Notice("Failed to generate valid MCQs from OpenRouter. Please try again.");
- return null;
- }
- const mcqSet = {
- notePath,
- questions,
- generatedAt: Date.now()
- };
- return mcqSet;
- } catch (error) {
- new import_obsidian18.Notice("Failed to generate MCQs with OpenRouter. Please check console for details.");
- return null;
- }
- }
- /**
- * Generate prompt for the AI
- *
- * @param noteContent Content of the note
- * @param settings Current plugin settings
- * @param numQuestionsToGenerate The target number of questions to ask for
- * @returns Prompt for the AI
- */
- generatePrompt(noteContent, settings, numQuestionsToGenerate) {
- const questionCount = numQuestionsToGenerate;
- const choiceCount = settings.mcqChoicesPerQuestion;
- const promptType = settings.mcqPromptType;
- const difficulty = settings.mcqDifficulty;
- 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 {
- 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.
-
-For example:
-1. What is the capital of France?
- A) London
- B) Berlin
- C) Paris [CORRECT]
- D) Madrid
- E) Rome`;
- }
- if (difficulty === "basic") {
- basePrompt += `
-
-Create straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`;
- } else {
- basePrompt += `
-
-Create challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`;
- }
- return `${basePrompt}
-
-Note Content:
-${noteContent}`;
- }
- /**
- * Make API request to OpenRouter
- *
- * @param prompt Prompt for the AI
- * @param settings Current plugin settings
- * @returns AI response text
- */
- async makeApiRequest(prompt, settings) {
- const apiKey = settings.openRouterApiKey;
- const model = settings.openRouterModel;
- const difficulty = settings.mcqDifficulty;
- try {
- const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
- const response = await (0, import_obsidian18.requestUrl)({
- url: "https://openrouter.ai/api/v1/chat/completions",
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${apiKey}`,
- "HTTP-Referer": "https://obsidian.md",
- // Required by OpenRouter
- "X-Title": "Spaceforge Plugin for Obsidian"
- // Identifying the app
- },
- body: JSON.stringify({
- model,
- messages: [
- {
- role: "system",
- content: systemPrompt
- },
- {
- role: "user",
- content: prompt
- }
- ]
- })
- });
- if (response.status !== 200) {
- throw new Error(`API request failed (${response.status}): ${response.text}`);
- }
- const data = response.json;
- if (!data.choices || !data.choices.length || !data.choices[0].message) {
- throw new Error("Invalid API response format from OpenRouter - missing choices");
- }
- return data.choices[0].message.content;
- } catch (error) {
- new import_obsidian18.Notice(`OpenRouter API error: ${error.message}`);
- throw error;
- }
- }
- /**
- * Parse the AI response to extract MCQs
- *
- * @param response AI response text
- * @param settings Current plugin settings
- * @param numQuestionsToGenerate The target number of questions expected
- * @returns Array of parsed MCQ questions
- */
- parseResponse(response, settings, numQuestionsToGenerate) {
- const questions = [];
- try {
- let questionBlocks = [];
- questionBlocks = response.split(/\d+\.\s+/).filter((block) => block.trim().length > 0);
- if (questionBlocks.length === 0) {
- const lines = response.split("\n");
- let currentQuestion = "";
- for (const line of lines) {
- if (/^\d+\./.test(line.trim())) {
- if (currentQuestion) {
- questionBlocks.push(currentQuestion);
- }
- currentQuestion = line.replace(/^\d+\.\s*/, "") + "\n";
- } else if (currentQuestion) {
- currentQuestion += line + "\n";
- }
- }
- if (currentQuestion) {
- questionBlocks.push(currentQuestion);
- }
- }
- for (const block of questionBlocks) {
- const lines = block.split("\n").filter((line) => line.trim().length > 0);
- if (lines.length < 2) {
- continue;
- }
- let questionText = lines[0].trim();
- questionText = questionText.replace(//g, "").replace(/<\/think>/g, "");
- const choices = [];
- let correctAnswerIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- const line = lines[i].trim();
- const isCorrect = line.includes("[CORRECT]");
- 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 (correctAnswerIndex === -1) {
- for (let i = 0; i < choices.length; i++) {
- if (lines[i + 1] && (lines[i + 1].toLowerCase().includes("correct") || lines[i + 1].includes("\u2713") || lines[i + 1].includes("\u2714\uFE0F"))) {
- correctAnswerIndex = i;
- break;
- }
- }
- }
- if (correctAnswerIndex === -1 && choices.length > 0) {
- correctAnswerIndex = 0;
- }
- if (questionText && choices.length >= 2) {
- questions.push({
- question: questionText,
- choices,
- correctAnswerIndex
- });
- } else {
- }
- }
- return questions.slice(0, numQuestionsToGenerate);
- } catch (error) {
- new import_obsidian18.Notice("Error parsing MCQ response from OpenRouter. Please try again.");
- return [];
- }
- }
-};
-
-// api/openai-service.ts
-var import_obsidian19 = require("obsidian");
-var OpenAIService = class {
- constructor(plugin) {
- this.plugin = plugin;
- }
- async generateMCQs(notePath, noteContent, settings) {
- if (!settings.openaiApiKey) {
- new import_obsidian19.Notice("OpenAI API key is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- if (!settings.openaiModel) {
- new import_obsidian19.Notice("OpenAI Model is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- try {
- new import_obsidian19.Notice("Generating MCQs using OpenAI...");
- let numQuestionsToGenerate;
- if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
- numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion));
- } else {
- numQuestionsToGenerate = settings.mcqQuestionsPerNote;
- }
- const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate);
- const response = await this.makeApiRequest(prompt, settings);
- const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
- if (questions.length === 0) {
- new import_obsidian19.Notice("Failed to generate valid MCQs from OpenAI. Please try again.");
- return null;
- }
- return {
- notePath,
- questions,
- generatedAt: Date.now()
- };
- } catch (error) {
- new import_obsidian19.Notice("Failed to generate MCQs with OpenAI. Please check console for details.");
- return null;
- }
- }
- generatePrompt(noteContent, settings, numQuestionsToGenerate) {
- const questionCount = numQuestionsToGenerate;
- const choiceCount = settings.mcqChoicesPerQuestion;
- const promptType = settings.mcqPromptType;
- const difficulty = settings.mcqDifficulty;
- 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 {
- 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.
-
-For example:
-1. What is the capital of France?
- A) London
- B) Berlin
- C) Paris [CORRECT]
- D) Madrid
- E) Rome`;
- }
- if (difficulty === "basic") {
- basePrompt += `
-
-Create straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`;
- } else {
- basePrompt += `
-
-Create challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`;
- }
- return `${basePrompt}
-
-Note Content:
-${noteContent}`;
- }
- async makeApiRequest(prompt, settings) {
- var _a;
- const apiKey = settings.openaiApiKey;
- const model = settings.openaiModel;
- const difficulty = settings.mcqDifficulty;
- const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
- try {
- const response = await (0, import_obsidian19.requestUrl)({
- url: "https://api.openai.com/v1/chat/completions",
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${apiKey}`
- },
- body: JSON.stringify({
- model,
- messages: [
- { role: "system", content: systemPrompt },
- { role: "user", content: prompt }
- ]
- })
- });
- if (response.status !== 200) {
- const errorData = response.json || { message: response.text };
- throw new Error(`API request failed (${response.status}): ${((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || "Unknown error"}`);
- }
- const data = response.json;
- if (!data.choices || !data.choices.length || !data.choices[0].message || !data.choices[0].message.content) {
- throw new Error("Invalid API response format from OpenAI - missing content");
- }
- return data.choices[0].message.content;
- } catch (error) {
- new import_obsidian19.Notice(`OpenAI API error: ${error.message}`);
- throw error;
- }
- }
- parseResponse(response, settings, numQuestionsToGenerate) {
- const questions = [];
- try {
- let questionBlocks = response.split(/\d+\.\s+/).filter((block) => block.trim().length > 0);
- if (questionBlocks.length === 0) {
- const lines = response.split("\n");
- let currentQuestion = "";
- for (const line of lines) {
- if (/^\d+\./.test(line.trim())) {
- if (currentQuestion)
- questionBlocks.push(currentQuestion);
- currentQuestion = line.replace(/^\d+\.\s*/, "") + "\n";
- } else if (currentQuestion) {
- currentQuestion += line + "\n";
- }
- }
- if (currentQuestion)
- questionBlocks.push(currentQuestion);
- }
- for (const block of questionBlocks) {
- const lines = block.split("\n").filter((line) => line.trim().length > 0);
- if (lines.length < 2)
- continue;
- let questionText = lines[0].trim();
- questionText = questionText.replace(//g, "").replace(/<\/think>/g, "");
- const choices = [];
- let correctAnswerIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- const line = lines[i].trim();
- const isCorrect = line.includes("[CORRECT]");
- 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 (correctAnswerIndex === -1) {
- for (let i = 0; i < choices.length; i++) {
- if (lines[i + 1] && (lines[i + 1].toLowerCase().includes("correct") || lines[i + 1].includes("\u2713") || lines[i + 1].includes("\u2714\uFE0F"))) {
- correctAnswerIndex = i;
- break;
- }
- }
- }
- if (correctAnswerIndex === -1 && choices.length > 0)
- correctAnswerIndex = 0;
- if (questionText && choices.length >= 2) {
- questions.push({ question: questionText, choices, correctAnswerIndex });
- }
- }
- return questions.slice(0, numQuestionsToGenerate);
- } catch (error) {
- new import_obsidian19.Notice("Error parsing MCQ response from OpenAI. Please try again.");
- return [];
- }
- }
-};
-
-// api/ollama-service.ts
-var import_obsidian20 = require("obsidian");
-var OllamaService = class {
- constructor(plugin) {
- this.plugin = plugin;
- }
- async generateMCQs(notePath, noteContent, settings) {
- if (!settings.ollamaApiUrl) {
- new import_obsidian20.Notice("Ollama API URL is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- if (!settings.ollamaModel) {
- new import_obsidian20.Notice("Ollama Model is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- try {
- new import_obsidian20.Notice("Generating MCQs using Ollama...");
- let numQuestionsToGenerate;
- if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
- numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion));
- } else {
- numQuestionsToGenerate = settings.mcqQuestionsPerNote;
- }
- const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate);
- const response = await this.makeApiRequest(prompt, settings);
- const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
- if (questions.length === 0) {
- new import_obsidian20.Notice("Failed to generate valid MCQs from Ollama. Please try again.");
- return null;
- }
- return {
- notePath,
- questions,
- generatedAt: Date.now()
- };
- } catch (error) {
- new import_obsidian20.Notice("Failed to generate MCQs with Ollama. Please check console for details.");
- return null;
- }
- }
- generatePrompt(noteContent, settings, numQuestionsToGenerate) {
- const questionCount = numQuestionsToGenerate;
- const choiceCount = settings.mcqChoicesPerQuestion;
- const promptType = settings.mcqPromptType;
- const difficulty = settings.mcqDifficulty;
- 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 {
- 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.
-
-For example:
-1. What is the capital of France?
- A) London
- B) Berlin
- C) Paris [CORRECT]
- D) Madrid
- E) Rome`;
- }
- if (difficulty === "basic") {
- basePrompt += `
-
-Create straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`;
- } else {
- basePrompt += `
-
-Create challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`;
- }
- return `${basePrompt}
-
-Note Content:
-${noteContent}`;
- }
- async makeApiRequest(prompt, settings) {
- 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 : settings.mcqAdvancedSystemPrompt;
- try {
- const response = await (0, import_obsidian20.requestUrl)({
- url: `${apiUrl}/api/chat`,
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- model,
- messages: [
- { role: "system", content: systemPrompt },
- { role: "user", content: prompt }
- ],
- stream: false
- // Ensure we get the full response, not a stream
- })
- });
- if (response.status !== 200) {
- const errorText = response.text;
- throw new Error(`API request failed (${response.status}): ${errorText}`);
- }
- const data = response.json;
- if (!data.message || !data.message.content) {
- throw new Error("Invalid API response format from Ollama - missing message content");
- }
- return data.message.content;
- } catch (error) {
- new import_obsidian20.Notice(`Ollama API error: ${error.message}`);
- throw error;
- }
- }
- parseResponse(response, settings, numQuestionsToGenerate) {
- const questions = [];
- try {
- let questionBlocks = response.split(/\d+\.\s+/).filter((block) => block.trim().length > 0);
- if (questionBlocks.length === 0) {
- const lines = response.split("\n");
- let currentQuestion = "";
- for (const line of lines) {
- if (/^\d+\./.test(line.trim())) {
- if (currentQuestion)
- questionBlocks.push(currentQuestion);
- currentQuestion = line.replace(/^\d+\.\s*/, "") + "\n";
- } else if (currentQuestion) {
- currentQuestion += line + "\n";
- }
- }
- if (currentQuestion)
- questionBlocks.push(currentQuestion);
- }
- for (const block of questionBlocks) {
- const lines = block.split("\n").filter((line) => line.trim().length > 0);
- if (lines.length < 2)
- continue;
- let questionText = lines[0].trim();
- questionText = questionText.replace(//g, "").replace(/<\/think>/g, "");
- const choices = [];
- let correctAnswerIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- const line = lines[i].trim();
- const isCorrect = line.includes("[CORRECT]");
- 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 (correctAnswerIndex === -1) {
- for (let i = 0; i < choices.length; i++) {
- if (lines[i + 1] && (lines[i + 1].toLowerCase().includes("correct") || lines[i + 1].includes("\u2713") || lines[i + 1].includes("\u2714\uFE0F"))) {
- correctAnswerIndex = i;
- break;
- }
- }
- }
- if (correctAnswerIndex === -1 && choices.length > 0)
- correctAnswerIndex = 0;
- if (questionText && choices.length >= 2) {
- questions.push({ question: questionText, choices, correctAnswerIndex });
- }
- }
- return questions.slice(0, numQuestionsToGenerate);
- } catch (error) {
- new import_obsidian20.Notice("Error parsing MCQ response from Ollama. Please try again.");
- return [];
- }
- }
-};
-
-// api/gemini-service.ts
-var import_obsidian21 = require("obsidian");
-var GeminiService = class {
- constructor(plugin) {
- this.plugin = plugin;
- }
- async generateMCQs(notePath, noteContent, settings) {
- if (!settings.geminiApiKey) {
- new import_obsidian21.Notice("Gemini API key is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- if (!settings.geminiModel) {
- new import_obsidian21.Notice("Gemini Model is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- try {
- new import_obsidian21.Notice("Generating MCQs using Gemini...");
- let numQuestionsToGenerate;
- if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
- numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion));
- } else {
- numQuestionsToGenerate = settings.mcqQuestionsPerNote;
- }
- const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate);
- const response = await this.makeApiRequest(prompt, settings);
- const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
- if (questions.length === 0) {
- new import_obsidian21.Notice("Failed to generate valid MCQs from Gemini. Please try again.");
- return null;
- }
- return {
- notePath,
- questions,
- generatedAt: Date.now()
- };
- } catch (error) {
- new import_obsidian21.Notice("Failed to generate MCQs with Gemini. Please check console for details.");
- return null;
- }
- }
- generatePrompt(noteContent, settings, numQuestionsToGenerate) {
- const questionCount = numQuestionsToGenerate;
- const choiceCount = settings.mcqChoicesPerQuestion;
- const promptType = settings.mcqPromptType;
- const difficulty = settings.mcqDifficulty;
- let basePrompt = "";
- const systemInstruction = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
- if (promptType === "basic") {
- basePrompt = `${systemInstruction}
-
-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 {
- basePrompt = `${systemInstruction}
-
-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.
-
-For example:
-1. What is the capital of France?
- A) London
- B) Berlin
- C) Paris [CORRECT]
- D) Madrid
- E) Rome`;
- }
- return `${basePrompt}
-
-Note Content:
-${noteContent}`;
- }
- async makeApiRequest(prompt, settings) {
- var _a;
- const apiKey = settings.geminiApiKey;
- const model = settings.geminiModel;
- const apiUrl = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`;
- try {
- const response = await (0, import_obsidian21.requestUrl)({
- url: apiUrl,
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- // Gemini API expects contents as an array of parts
- contents: [{ parts: [{ text: prompt }] }]
- // Optional: Add generationConfig if needed (e.g., temperature, maxOutputTokens)
- // generationConfig: {
- // temperature: 0.7,
- // maxOutputTokens: 1024,
- // }
- })
- });
- if (response.status !== 200) {
- const errorData = response.json;
- const errorMessage = ((_a = errorData == null ? void 0 : errorData.error) == null ? void 0 : _a.message) || (errorData == null ? void 0 : errorData.message) || "Unknown error";
- throw new Error(`API request failed (${response.status}): ${errorMessage}`);
- }
- const data = response.json;
- if (!data.candidates || !data.candidates.length || !data.candidates[0].content || !data.candidates[0].content.parts || !data.candidates[0].content.parts.length || !data.candidates[0].content.parts[0].text) {
- throw new Error("Invalid API response format from Gemini - missing content");
- }
- return data.candidates[0].content.parts[0].text;
- } catch (error) {
- new import_obsidian21.Notice(`Gemini API error: ${error.message}`);
- throw error;
- }
- }
- parseResponse(response, settings, numQuestionsToGenerate) {
- const questions = [];
- try {
- let questionBlocks = response.split(/\d+\.\s+/).filter((block) => block.trim().length > 0);
- if (questionBlocks.length === 0) {
- const lines = response.split("\n");
- let currentQuestion = "";
- for (const line of lines) {
- if (/^\d+\./.test(line.trim())) {
- if (currentQuestion)
- questionBlocks.push(currentQuestion);
- currentQuestion = line.replace(/^\d+\.\s*/, "") + "\n";
- } else if (currentQuestion) {
- currentQuestion += line + "\n";
- }
- }
- if (currentQuestion)
- questionBlocks.push(currentQuestion);
- }
- for (const block of questionBlocks) {
- const lines = block.split("\n").filter((line) => line.trim().length > 0);
- if (lines.length < 2)
- continue;
- let questionText = lines[0].trim();
- questionText = questionText.replace(//g, "").replace(/<\/think>/g, "");
- const choices = [];
- let correctAnswerIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- const line = lines[i].trim();
- const isCorrect = line.includes("[CORRECT]");
- 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 (correctAnswerIndex === -1) {
- for (let i = 0; i < choices.length; i++) {
- if (lines[i + 1] && (lines[i + 1].toLowerCase().includes("correct") || lines[i + 1].includes("\u2713") || lines[i + 1].includes("\u2714\uFE0F"))) {
- correctAnswerIndex = i;
- break;
- }
- }
- }
- if (correctAnswerIndex === -1 && choices.length > 0)
- correctAnswerIndex = 0;
- if (questionText && choices.length >= 2) {
- questions.push({ question: questionText, choices, correctAnswerIndex });
- }
- }
- return questions.slice(0, numQuestionsToGenerate);
- } catch (error) {
- new import_obsidian21.Notice("Error parsing MCQ response from Gemini. Please try again.");
- return [];
- }
- }
-};
-
-// api/claude-service.ts
-var import_obsidian22 = require("obsidian");
-var ClaudeService = class {
- constructor(plugin) {
- this.plugin = plugin;
- }
- async generateMCQs(notePath, noteContent, settings) {
- if (!settings.claudeApiKey) {
- new import_obsidian22.Notice("Claude API key is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- if (!settings.claudeModel) {
- new import_obsidian22.Notice("Claude Model is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- try {
- new import_obsidian22.Notice("Generating MCQs using Claude...");
- let numQuestionsToGenerate;
- if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
- numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion));
- } else {
- numQuestionsToGenerate = settings.mcqQuestionsPerNote;
- }
- const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate);
- const response = await this.makeApiRequest(prompt, settings);
- const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
- if (questions.length === 0) {
- new import_obsidian22.Notice("Failed to generate valid MCQs from Claude. Please try again.");
- return null;
- }
- return {
- notePath,
- questions,
- generatedAt: Date.now()
- };
- } catch (error) {
- new import_obsidian22.Notice("Failed to generate MCQs with Claude. Please check console for details.");
- return null;
- }
- }
- generatePrompt(noteContent, settings, numQuestionsToGenerate) {
- const questionCount = numQuestionsToGenerate;
- const choiceCount = settings.mcqChoicesPerQuestion;
- const promptType = settings.mcqPromptType;
- const difficulty = settings.mcqDifficulty;
- 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 {
- 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.
-
-For example:
-1. What is the capital of France?
- A) London
- B) Berlin
- C) Paris [CORRECT]
- D) Madrid
- E) Rome`;
- }
- if (difficulty === "basic") {
- basePrompt += `
-
-Create straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`;
- } else {
- basePrompt += `
-
-Create challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`;
- }
- return `${basePrompt}
-
-Note Content:
-${noteContent}`;
- }
- async makeApiRequest(prompt, settings) {
- var _a;
- const apiKey = settings.claudeApiKey;
- const model = settings.claudeModel;
- const difficulty = settings.mcqDifficulty;
- const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
- try {
- const response = await (0, import_obsidian22.requestUrl)({
- url: "https://api.anthropic.com/v1/messages",
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "x-api-key": apiKey,
- "anthropic-version": "2023-06-01"
- },
- body: JSON.stringify({
- model,
- max_tokens: 2048,
- // Adjust as needed
- system: systemPrompt,
- messages: [
- { role: "user", content: prompt }
- ]
- })
- });
- if (response.status !== 200) {
- const errorData = response.json || { message: response.text };
- throw new Error(`API request failed (${response.status}): ${((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || "Unknown error"}`);
- }
- const data = response.json;
- if (!data.content || !data.content.length || !data.content[0].text) {
- throw new Error("Invalid API response format from Claude - missing content");
- }
- return data.content[0].text;
- } catch (error) {
- new import_obsidian22.Notice(`Claude API error: ${error.message}`);
- throw error;
- }
- }
- parseResponse(response, settings, numQuestionsToGenerate) {
- const questions = [];
- try {
- let questionBlocks = response.split(/\n\d+\.\s+/).filter((block) => block.trim().length > 0);
- if (questionBlocks.length > 0 && !/^\d+\.\s+/.test(response.trimStart())) {
- if (!/^\d+\.\s+/.test(questionBlocks[0])) {
- if (response.trimStart().length > 0 && questionBlocks.length === 1 && !response.includes("\n1.")) {
- questionBlocks = response.split(/\n(?=\d+\.\s)/);
- if (questionBlocks.length === 1 && !/^\d+\.\s/.test(questionBlocks[0])) {
- const potentialBlocks = response.split(/\n\n+/);
- if (potentialBlocks.some((pb) => /^\d+\.\s/.test(pb.trimStart()))) {
- questionBlocks = potentialBlocks;
- } else {
- }
- }
- }
- }
- }
- if (questionBlocks.length === 0 || questionBlocks.length < settings.mcqQuestionsPerNote / 2 && response.includes("1.")) {
- const lines = response.split("\n");
- let currentQuestionBlock = "";
- const tempBlocks = [];
- for (const line of lines) {
- if (/^\d+\.\s+/.test(line.trim())) {
- if (currentQuestionBlock.trim().length > 0) {
- tempBlocks.push(currentQuestionBlock.trim());
- }
- currentQuestionBlock = line + "\n";
- } else if (currentQuestionBlock.length > 0) {
- currentQuestionBlock += line + "\n";
- } else if (tempBlocks.length === 0 && line.trim().length > 0) {
- currentQuestionBlock = line + "\n";
- }
- }
- if (currentQuestionBlock.trim().length > 0) {
- tempBlocks.push(currentQuestionBlock.trim());
- }
- if (tempBlocks.length > 0)
- questionBlocks = tempBlocks;
- }
- for (const block of questionBlocks) {
- const lines = block.split("\n").filter((line) => line.trim().length > 0);
- if (lines.length < 2)
- continue;
- let questionText = lines[0].replace(/^\d+\.\s*/, "").trim();
- questionText = questionText.replace(//g, "").replace(/<\/think>/g, "");
- const choices = [];
- let correctAnswerIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- 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();
- if (cleanedLine.length > 0) {
- choices.push(cleanedLine);
- if (isCorrect)
- correctAnswerIndex = choices.length - 1;
- }
- }
- if (correctAnswerIndex === -1 && choices.length > 0) {
- for (let i = 0; i < choices.length; i++) {
- if (choices[i].toLowerCase().includes("(correct answer)") || choices[i].toLowerCase().includes(" - correct")) {
- choices[i] = choices[i].replace(/\(correct answer\)/gi, "").replace(/ - correct/gi, "").trim();
- correctAnswerIndex = i;
- break;
- }
- }
- if (correctAnswerIndex === -1)
- correctAnswerIndex = 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 });
- }
- }
- return questions.slice(0, numQuestionsToGenerate);
- } catch (error) {
- new import_obsidian22.Notice("Error parsing MCQ response from Claude. Please try again.");
- return [];
- }
- }
-};
-
-// api/together-service.ts
-var import_obsidian23 = require("obsidian");
-var TogetherService = class {
- constructor(plugin) {
- this.plugin = plugin;
- }
- async generateMCQs(notePath, noteContent, settings) {
- if (!settings.togetherApiKey) {
- new import_obsidian23.Notice("Together AI API key is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- if (!settings.togetherModel) {
- new import_obsidian23.Notice("Together AI Model is not set. Please add it in the Spaceforge settings.");
- return null;
- }
- try {
- new import_obsidian23.Notice("Generating MCQs using Together AI...");
- let numQuestionsToGenerate;
- if (settings.mcqQuestionAmountMode === "wordsPerQuestion" /* WordsPerQuestion */) {
- const wordCount = noteContent.split(/\s+/).filter(Boolean).length;
- numQuestionsToGenerate = Math.max(1, Math.ceil(wordCount / settings.mcqWordsPerQuestion));
- } else {
- numQuestionsToGenerate = settings.mcqQuestionsPerNote;
- }
- const prompt = this.generatePrompt(noteContent, settings, numQuestionsToGenerate);
- const response = await this.makeApiRequest(prompt, settings);
- const questions = this.parseResponse(response, settings, numQuestionsToGenerate);
- if (questions.length === 0) {
- new import_obsidian23.Notice("Failed to generate valid MCQs from Together AI. Please try again.");
- return null;
- }
- return {
- notePath,
- questions,
- generatedAt: Date.now()
- };
- } catch (error) {
- new import_obsidian23.Notice("Failed to generate MCQs with Together AI. Please check console for details.");
- return null;
- }
- }
- generatePrompt(noteContent, settings, numQuestionsToGenerate) {
- const questionCount = numQuestionsToGenerate;
- const choiceCount = settings.mcqChoicesPerQuestion;
- const promptType = settings.mcqPromptType;
- const difficulty = settings.mcqDifficulty;
- 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 {
- 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.
-
-For example:
-1. What is the capital of France?
- A) London
- B) Berlin
- C) Paris [CORRECT]
- D) Madrid
- E) Rome`;
- }
- if (difficulty === "basic") {
- basePrompt += `
-
-Create straightforward questions that focus on key facts and basic concepts. Make the questions clear and direct, suitable for beginners or initial review.`;
- } else {
- basePrompt += `
-
-Create challenging questions that test deeper understanding and application of concepts. Make the incorrect choices plausible to encourage critical thinking.`;
- }
- return `${basePrompt}
-
-Note Content:
-${noteContent}`;
- }
- async makeApiRequest(prompt, settings) {
- var _a;
- const apiKey = settings.togetherApiKey;
- const model = settings.togetherModel;
- const difficulty = settings.mcqDifficulty;
- const systemPrompt = difficulty === "basic" ? settings.mcqBasicSystemPrompt : settings.mcqAdvancedSystemPrompt;
- try {
- const response = await (0, import_obsidian23.requestUrl)({
- url: "https://api.together.xyz/v1/chat/completions",
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- "Authorization": `Bearer ${apiKey}`
- },
- body: JSON.stringify({
- model,
- max_tokens: 2048,
- // Adjust as needed
- messages: [
- { role: "system", content: systemPrompt },
- { role: "user", content: prompt }
- ]
- })
- });
- if (response.status !== 200) {
- const errorData = response.json || { message: response.text };
- throw new Error(`API request failed (${response.status}): ${((_a = errorData.error) == null ? void 0 : _a.message) || errorData.message || "Unknown error"}`);
- }
- const data = response.json;
- if (!data.choices || !data.choices.length || !data.choices[0].message || !data.choices[0].message.content) {
- throw new Error("Invalid API response format from Together AI - missing content");
- }
- return data.choices[0].message.content;
- } catch (error) {
- new import_obsidian23.Notice(`Together AI API error: ${error.message}`);
- throw error;
- }
- }
- parseResponse(response, settings, numQuestionsToGenerate) {
- const questions = [];
- try {
- let questionBlocks = response.split(/\n\d+\.\s+/).filter((block) => block.trim().length > 0);
- if (questionBlocks.length > 0 && !/^\d+\.\s+/.test(response.trimStart())) {
- if (!/^\d+\.\s+/.test(questionBlocks[0])) {
- if (response.trimStart().length > 0 && questionBlocks.length === 1 && !response.includes("\n1.")) {
- questionBlocks = response.split(/\n(?=\d+\.\s)/);
- if (questionBlocks.length === 1 && !/^\d+\.\s/.test(questionBlocks[0])) {
- const potentialBlocks = response.split(/\n\n+/);
- if (potentialBlocks.some((pb) => /^\d+\.\s/.test(pb.trimStart()))) {
- questionBlocks = potentialBlocks;
- }
- }
- }
- }
- }
- if (questionBlocks.length === 0 || questionBlocks.length < settings.mcqQuestionsPerNote / 2 && response.includes("1.")) {
- const lines = response.split("\n");
- let currentQuestionBlock = "";
- const tempBlocks = [];
- for (const line of lines) {
- if (/^\d+\.\s+/.test(line.trim())) {
- if (currentQuestionBlock.trim().length > 0) {
- tempBlocks.push(currentQuestionBlock.trim());
- }
- currentQuestionBlock = line + "\n";
- } else if (currentQuestionBlock.length > 0) {
- currentQuestionBlock += line + "\n";
- } else if (tempBlocks.length === 0 && line.trim().length > 0) {
- currentQuestionBlock = line + "\n";
- }
- }
- if (currentQuestionBlock.trim().length > 0) {
- tempBlocks.push(currentQuestionBlock.trim());
- }
- if (tempBlocks.length > 0)
- questionBlocks = tempBlocks;
- }
- for (const block of questionBlocks) {
- const lines = block.split("\n").filter((line) => line.trim().length > 0);
- if (lines.length < 2)
- continue;
- let questionText = lines[0].replace(/^\d+\.\s*/, "").trim();
- questionText = questionText.replace(//g, "").replace(/<\/think>/g, "");
- const choices = [];
- let correctAnswerIndex = -1;
- for (let i = 1; i < lines.length; i++) {
- 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();
- if (cleanedLine.length > 0) {
- choices.push(cleanedLine);
- if (isCorrect)
- correctAnswerIndex = choices.length - 1;
- }
- }
- if (correctAnswerIndex === -1 && choices.length > 0) {
- for (let i = 0; i < choices.length; i++) {
- if (choices[i].toLowerCase().includes("(correct answer)") || choices[i].toLowerCase().includes(" - correct")) {
- choices[i] = choices[i].replace(/\(correct answer\)/gi, "").replace(/ - correct/gi, "").trim();
- correctAnswerIndex = i;
- break;
- }
- }
- if (correctAnswerIndex === -1)
- correctAnswerIndex = 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 });
- }
- }
- return questions.slice(0, numQuestionsToGenerate);
- } catch (error) {
- new import_obsidian23.Notice("Error parsing MCQ response from Together AI. Please try again.");
- return [];
- }
- }
-};
-
-// services/review-schedule-service.ts
-var import_obsidian24 = require("obsidian");
-
-// services/fsrs-schedule-service.ts
-var FsrsScheduleService = class {
- constructor(settings) {
- this.pluginSettings = settings;
- this.fsrsInstance = new W(this.mapSettingsToFsrsParams(settings.fsrsParameters));
- }
- mapSettingsToFsrsParams(params) {
- var _a, _b, _c, _d;
- const defaultWeights = [0.4, 0.6, 2.4, 5.8, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05, 0.34, 1.26, 0.29, 2.61];
- const mappedParams = {
- request_retention: (_a = params.request_retention) != null ? _a : 0.9,
- maximum_interval: (_b = params.maximum_interval) != null ? _b : 36500,
- w: params.w && params.w.length > 0 ? params.w : defaultWeights,
- enable_fuzz: (_c = params.enable_fuzz) != null ? _c : true,
- learning_steps: params.learning_steps && params.learning_steps.length > 0 ? params.learning_steps : [1, 10],
- enable_short_term: (_d = params.enable_short_term) != null ? _d : false
- // Added enable_short_term
- };
- return mappedParams;
- }
- updateFSRSInstance(settings) {
- this.pluginSettings = settings;
- this.fsrsInstance = new W(this.mapSettingsToFsrsParams(settings.fsrsParameters));
- }
- createNewFsrsCardData(creationDate = /* @__PURE__ */ new Date()) {
- const emptyCard = v(creationDate);
- return {
- stability: emptyCard.stability,
- difficulty: emptyCard.difficulty,
- elapsed_days: emptyCard.elapsed_days,
- scheduled_days: emptyCard.scheduled_days,
- reps: emptyCard.reps,
- lapses: emptyCard.lapses,
- state: emptyCard.state,
- // Cast to number; FsrsState is an enum
- last_review: emptyCard.last_review ? emptyCard.last_review.getTime() : void 0
- };
- }
- mapReviewScheduleFsrsDataToFsrsLibCard(fsrsData, now) {
- return {
- ...fsrsData,
- due: now,
- // This will be the review date for the repeat() call
- state: fsrsData.state,
- last_review: fsrsData.last_review ? new Date(fsrsData.last_review) : void 0
- };
- }
- mapFsrsLibRatingToTsFsrsRating(rating) {
- switch (rating) {
- case 1 /* Again */:
- return l.Again;
- case 2 /* Hard */:
- return l.Hard;
- case 3 /* Good */:
- return l.Good;
- case 4 /* Easy */:
- return l.Easy;
- default:
- throw new Error(`Unknown FsrsRating: ${rating}`);
- }
- }
- recordReview(currentFsrsData, rating, reviewDateTime) {
- const fsrsLibCardToReview = this.mapReviewScheduleFsrsDataToFsrsLibCard(currentFsrsData, reviewDateTime);
- const tsFsrsRating = this.mapFsrsLibRatingToTsFsrsRating(rating);
- const schedulingResult = this.fsrsInstance.repeat(fsrsLibCardToReview, reviewDateTime);
- const validRatingKey = tsFsrsRating;
- const resultCard = schedulingResult[validRatingKey].card;
- const resultLog = schedulingResult[validRatingKey].log;
- const updatedData = {
- stability: resultCard.stability,
- difficulty: resultCard.difficulty,
- elapsed_days: resultCard.elapsed_days,
- scheduled_days: resultCard.scheduled_days,
- reps: resultCard.reps,
- lapses: resultCard.lapses,
- state: resultCard.state,
- last_review: resultCard.last_review ? resultCard.last_review.getTime() : void 0
- };
- return {
- updatedData,
- nextReviewDate: resultCard.due.getTime(),
- log: resultLog
- };
- }
- skipReview(currentFsrsData, reviewDateTime) {
- return this.recordReview(currentFsrsData, 1 /* Again */, reviewDateTime);
- }
-};
-
-// services/review-schedule-service.ts
-var ReviewScheduleService = class {
- /**
- * Initialize Review Schedule Service
- *
- * @param plugin Reference to the main plugin
- * @param schedules Initial schedules data
- * @param customNoteOrder Initial custom note order data
- * @param lastLinkAnalysisTimestamp Initial last link analysis timestamp
- * @param history Reference to the history array in DataStorage
- */
- constructor(plugin, schedules, customNoteOrder, lastLinkAnalysisTimestamp, history) {
- // Added FsrsScheduleService instance
- /**
- * Note schedules indexed by path
- */
- this.schedules = {};
- /**
- * Custom order for notes (user-defined ordering)
- */
- this.customNoteOrder = [];
- /**
- * Timestamp of the last time link analysis was performed for ordering
- */
- this.lastLinkAnalysisTimestamp = null;
- this.plugin = plugin;
- this.schedules = schedules;
- this.customNoteOrder = customNoteOrder;
- this.lastLinkAnalysisTimestamp = lastLinkAnalysisTimestamp;
- this.history = history;
- this.fsrsService = new FsrsScheduleService(this.plugin.settings);
- }
- updateAlgorithmServicesForSettingsChange() {
- this.fsrsService.updateFSRSInstance(this.plugin.settings);
- }
- /**
- * Schedule a note for review
- *
- * @param path Path to the note file
- * @param daysFromNow Days until first review (default: 0, same day)
- */
- async scheduleNoteForReview(path, daysFromNow = 0) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!file || !(file instanceof import_obsidian24.TFile) || file.extension !== "md") {
- new import_obsidian24.Notice("Only markdown files can be added to the review schedule");
- return;
- }
- const now = Date.now();
- const todayUTCStart = DateUtils.startOfUTCDay(new Date(now));
- const defaultAlgorithm = this.plugin.settings.defaultSchedulingAlgorithm;
- let newSchedule;
- if (defaultAlgorithm === "fsrs") {
- const fsrsData = this.fsrsService.createNewFsrsCardData(new Date(now));
- newSchedule = {
- path,
- lastReviewDate: null,
- // Will be UTC midnight when set
- nextReviewDate: now,
- // FSRS cards are due immediately (exact timestamp)
- reviewCount: 0,
- schedulingAlgorithm: "fsrs",
- fsrsData,
- // SM-2 fields can be undefined or default
- ease: this.plugin.settings.baseEase,
- // Keep a base for potential conversion
- interval: 0,
- consecutive: 0,
- repetitionCount: 0,
- scheduleCategory: void 0
- // Not applicable to FSRS
- };
- } else {
- newSchedule = {
- path,
- lastReviewDate: null,
- // Will be UTC midnight when set
- nextReviewDate: DateUtils.addDays(todayUTCStart, daysFromNow),
- ease: this.plugin.settings.baseEase,
- interval: daysFromNow,
- consecutive: 0,
- reviewCount: 0,
- repetitionCount: 0,
- scheduleCategory: this.plugin.settings.useInitialSchedule ? "initial" : "spaced",
- schedulingAlgorithm: "sm2",
- fsrsData: void 0
- };
- if (newSchedule.scheduleCategory === "initial") {
- const initialIntervals = this.plugin.settings.initialScheduleCustomIntervals;
- if (initialIntervals && initialIntervals.length > 0) {
- newSchedule.interval = daysFromNow > 0 ? daysFromNow : initialIntervals[0];
- }
- if (daysFromNow === 0) {
- newSchedule.nextReviewDate = DateUtils.addDays(todayUTCStart, newSchedule.interval);
- }
- }
- }
- this.schedules[path] = newSchedule;
- if (!this.customNoteOrder.includes(path)) {
- this.customNoteOrder.push(path);
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- new import_obsidian24.Notice(`Note added to review schedule`);
- }
- /**
- * Check if a note is due for review on or before the specified date
- *
- * @param schedule The review schedule for the note
- * @param effectiveReviewDate The date to check against
- * @returns true if the note is due, false otherwise
- */
- isNoteDue(schedule, effectiveReviewDate) {
- const reviewDateObj = new Date(effectiveReviewDate);
- const effectiveUTCDayEnd = DateUtils.endOfUTCDay(reviewDateObj);
- return schedule.nextReviewDate <= effectiveUTCDayEnd;
- }
- /**
- * Record a review for a note
- *
- * @param path Path to the note file
- * @param response User's response during review (can be SM-2 or FSRS rating)
- * @param isSkipped Whether this review was explicitly skipped (default: false)
- * @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, response, isSkipped = false, currentReviewDate) {
- var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
- const schedule = this.schedules[path];
- if (!schedule)
- return false;
- const effectiveReviewDate = currentReviewDate || Date.now();
- const isDue = this.isNoteDue(schedule, effectiveReviewDate);
- if (!isDue) {
- return false;
- }
- const reviewDateObj = new Date(effectiveReviewDate);
- const effectiveUTCDayStart = DateUtils.startOfUTCDay(reviewDateObj);
- let historyResponseValue;
- if (Object.values(FsrsRating).includes(response) && typeof response === "number") {
- switch (response) {
- case 1 /* Again */:
- historyResponseValue = 1 /* IncorrectResponse */;
- break;
- case 2 /* Hard */:
- historyResponseValue = 2 /* IncorrectButFamiliar */;
- break;
- case 3 /* Good */:
- historyResponseValue = 3 /* CorrectWithDifficulty */;
- break;
- case 4 /* Easy */:
- historyResponseValue = 4 /* CorrectWithHesitation */;
- break;
- default:
- historyResponseValue = 3 /* CorrectWithDifficulty */;
- }
- } else {
- historyResponseValue = toSM2Quality(response);
- }
- if (!isSkipped) {
- schedule.reviewCount = (schedule.reviewCount || 0) + 1;
- }
- schedule.lastReviewDate = effectiveUTCDayStart;
- this.history.push({
- path,
- timestamp: effectiveReviewDate,
- response: historyResponseValue,
- interval: (_c = (_b = schedule.interval) != null ? _b : (_a = schedule.fsrsData) == null ? void 0 : _a.scheduled_days) != null ? _c : 0,
- ease: (_e = schedule.ease) != null ? _e : ((_d = schedule.fsrsData) == null ? void 0 : _d.difficulty) ? Math.round(schedule.fsrsData.difficulty * 10) : this.plugin.settings.baseEase,
- isSkipped
- });
- if (this.history.length > 1e3)
- this.history.splice(0, this.history.length - 1e3);
- if (schedule.schedulingAlgorithm === "fsrs") {
- if (!schedule.fsrsData) {
- schedule.fsrsData = this.fsrsService.createNewFsrsCardData(reviewDateObj);
- }
- let actualFsrsRating;
- if (response === 5 /* Perfect */) {
- actualFsrsRating = 4 /* Easy */;
- } else if (response === 4 /* Good */) {
- actualFsrsRating = 3 /* Good */;
- } else if (response === 3 /* Fair */) {
- actualFsrsRating = 2 /* Hard */;
- } else if (response === 1 /* Hard */) {
- actualFsrsRating = 1 /* Again */;
- } else if (Object.values(FsrsRating).includes(response)) {
- actualFsrsRating = response;
- } else {
- const quality = toSM2Quality(response);
- if (quality >= 4)
- actualFsrsRating = 4 /* Easy */;
- else if (quality === 3)
- actualFsrsRating = 3 /* Good */;
- else if (quality === 2)
- actualFsrsRating = 2 /* Hard */;
- else
- actualFsrsRating = 1 /* Again */;
- }
- 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;
- schedule.interval = updatedData.scheduled_days;
- schedule.ease = Math.round(updatedData.difficulty * 10);
- } else {
- let qualityRating = toSM2Quality(response);
- schedule.ease = (_f = schedule.ease) != null ? _f : this.plugin.settings.baseEase;
- schedule.interval = (_g = schedule.interval) != null ? _g : 0;
- schedule.repetitionCount = (_h = schedule.repetitionCount) != null ? _h : 0;
- schedule.consecutive = (_i = schedule.consecutive) != null ? _i : 0;
- schedule.scheduleCategory = (_j = schedule.scheduleCategory) != null ? _j : this.plugin.settings.useInitialSchedule ? "initial" : "spaced";
- if (schedule.scheduleCategory === "initial") {
- const initialIntervals = this.plugin.settings.initialScheduleCustomIntervals || [];
- if (schedule.repetitionCount < initialIntervals.length) {
- schedule.interval = initialIntervals[schedule.repetitionCount];
- } else {
- schedule.scheduleCategory = "graduated";
- const daysLateForGraduation = 0;
- const { interval, ease, repetitionCount: newRepCount } = this.calculateSM2Schedule(
- schedule.interval,
- // previous interval (last of initial steps)
- schedule.ease,
- qualityRating,
- 0,
- // Reset repetition count for SM-2 calculation after graduation
- daysLateForGraduation,
- isSkipped
- );
- schedule.interval = interval;
- schedule.ease = ease;
- schedule.repetitionCount = newRepCount;
- }
- const q2 = qualityRating;
- let newEase = schedule.ease / 100;
- newEase = newEase + (0.1 - (5 - q2) * (0.08 + (5 - q2) * 0.02));
- newEase = Math.max(1.3, newEase);
- schedule.ease = Math.round(newEase * 100);
- if (qualityRating >= 3 /* CorrectWithDifficulty */) {
- schedule.consecutive += 1;
- if (qualityRating >= 3) {
- schedule.repetitionCount = (schedule.repetitionCount || 0) + 1;
- } else {
- schedule.repetitionCount = 0;
- }
- } else {
- schedule.consecutive = 0;
- schedule.repetitionCount = 0;
- }
- } else {
- const daysLate = schedule.nextReviewDate < effectiveUTCDayStart ? (
- // Compare with UTC day start
- DateUtils.dayDifferenceUTC(schedule.nextReviewDate, effectiveUTCDayStart)
- ) : 0;
- const { interval, ease, repetitionCount } = this.calculateSM2Schedule(
- schedule.interval,
- schedule.ease,
- qualityRating,
- schedule.repetitionCount || 0,
- daysLate,
- isSkipped
- );
- schedule.interval = interval;
- schedule.ease = ease;
- schedule.repetitionCount = repetitionCount;
- if (qualityRating >= 3 /* CorrectWithDifficulty */) {
- schedule.consecutive += 1;
- } else {
- schedule.consecutive = 0;
- }
- }
- schedule.nextReviewDate = DateUtils.addDays(effectiveUTCDayStart, schedule.interval);
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- return true;
- }
- /**
- * Calculate new schedule parameters based on review response using the SM-2 algorithm
- * (This method is likely redundant now that recordReview uses calculateSM2Schedule directly,
- * but keeping for potential external use or backward compatibility if needed)
- *
- * @param currentInterval Current interval in days
- * @param currentEase Current ease factor
- * @param response User's response during review
- * @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
- */
- calculateNewSchedule(currentInterval, currentEase, response, repetitionCount = 0, daysLate = 0, isSkipped = false) {
- let qualityRating = toSM2Quality(response);
- if (isSkipped || daysLate > 0) {
- const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0;
- let ease2 = currentEase / 100;
- ease2 = ease2 + (0.1 - (5 - q_eff) * (0.08 + (5 - q_eff) * 0.02));
- ease2 = Math.max(1.3, ease2);
- return {
- interval: 1,
- // Force next review to be tomorrow
- ease: Math.round(ease2 * 100),
- // Convert back to internal format
- repetitionCount: 1
- // Reset repetition count to 1
- };
- }
- let ease = currentEase / 100;
- let newRepetitionCount = repetitionCount;
- let interval;
- ease = ease + (0.1 - (5 - qualityRating) * (0.08 + (5 - qualityRating) * 0.02));
- ease = Math.max(1.3, ease);
- if (qualityRating < 3) {
- newRepetitionCount = 0;
- interval = 1;
- } else {
- newRepetitionCount += 1;
- if (newRepetitionCount === 1) {
- interval = 1;
- } else if (newRepetitionCount === 2) {
- interval = 6;
- } else {
- interval = Math.round(currentInterval * ease);
- }
- }
- 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;
- }
- interval = Math.max(1, interval);
- interval = Math.min(interval, this.plugin.settings.maximumInterval);
- return {
- interval: Math.round(interval),
- // SM-2 uses whole days
- ease: Math.round(ease * 100),
- repetitionCount: newRepetitionCount
- };
- }
- /**
- * 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
- */
- calculateSM2Schedule(currentInterval, currentEase, qualityRating, repetitionCount = 0, daysLate = 0, isSkipped = false) {
- if (isSkipped || daysLate > 0) {
- const q_eff = isSkipped ? Math.max(0, qualityRating - 1) : 0;
- let ease2 = currentEase / 100;
- ease2 = ease2 + (0.1 - (5 - q_eff) * (0.08 + (5 - q_eff) * 0.02));
- ease2 = Math.max(1.3, ease2);
- const result = {
- interval: 1,
- // Force next review to be tomorrow
- ease: Math.round(ease2 * 100),
- // Convert back to internal format
- repetitionCount: 1
- // Reset repetition count to 1
- };
- return result;
- }
- let ease = currentEase / 100;
- let newRepetitionCount = repetitionCount;
- let interval;
- ease = ease + (0.1 - (5 - qualityRating) * (0.08 + (5 - qualityRating) * 0.02));
- ease = Math.max(1.3, ease);
- if (qualityRating < 3) {
- newRepetitionCount = 0;
- interval = 1;
- } else {
- newRepetitionCount += 1;
- if (newRepetitionCount === 1) {
- interval = 1;
- } else if (newRepetitionCount === 2) {
- interval = 6;
- } else {
- interval = Math.round(currentInterval * ease);
- }
- }
- 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;
- }
- interval = Math.max(1, interval);
- interval = Math.min(interval, this.plugin.settings.maximumInterval);
- return {
- interval: Math.round(interval),
- // SM-2 uses whole days
- ease: Math.round(ease * 100),
- repetitionCount: newRepetitionCount
- };
- }
- /**
- * Get notes due for review
- *
- * @param date Optional target date (default: now)
- * @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 = Date.now(), matchExactDate = false) {
- const targetDate = new Date(date);
- const targetUTCDayStart = DateUtils.startOfUTCDay(targetDate);
- const targetUTCDayEnd = DateUtils.endOfUTCDay(targetDate);
- return Object.values(this.schedules).filter((schedule) => {
- if (matchExactDate) {
- return schedule.nextReviewDate >= targetUTCDayStart && schedule.nextReviewDate <= targetUTCDayEnd;
- } else {
- return schedule.nextReviewDate <= targetUTCDayEnd;
- }
- }).sort((a, b2) => a.nextReviewDate - b2.nextReviewDate);
- }
- /**
- * Get upcoming reviews within a specified timeframe
- *
- * @param days Number of days to look ahead
- * @returns Array of upcoming review schedules sorted by due date
- */
- getUpcomingReviews(days = 7) {
- const now = Date.now();
- const futureDate = DateUtils.addDays(now, days);
- return Object.values(this.schedules).filter(
- (schedule) => schedule.nextReviewDate > now && schedule.nextReviewDate <= futureDate
- ).sort((a, b2) => a.nextReviewDate - b2.nextReviewDate);
- }
- /**
- * Skip a note's review and reschedule for tomorrow with penalized quality
- *
- * This implements the "Postpone to Tomorrow" functionality from the modified SM-2 algorithm.
- * It applies a one-step quality penalty (reduce by 1 but not below 0) and forces the next
- * review to be tomorrow, regardless of what the normal interval would be. This keeps items
- * in rotation rather than letting them disappear into an ever-growing backlog.
- *
- * @param path Path to the note file
- * @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, response = 3 /* CorrectWithDifficulty */, currentReviewDate) {
- const schedule = this.schedules[path];
- if (!schedule)
- return;
- const effectiveReviewDate = currentReviewDate || Date.now();
- const reviewDateObj = new Date(effectiveReviewDate);
- if (schedule.schedulingAlgorithm === "fsrs") {
- if (!schedule.fsrsData) {
- schedule.fsrsData = this.fsrsService.createNewFsrsCardData(reviewDateObj);
- }
- const { updatedData, nextReviewDate: newNextReviewDateFsrs, log } = this.fsrsService.skipReview(
- schedule.fsrsData,
- reviewDateObj
- // Pass exact moment for FSRS skip
- );
- schedule.fsrsData = updatedData;
- schedule.nextReviewDate = newNextReviewDateFsrs;
- schedule.lastReviewDate = DateUtils.startOfUTCDay(reviewDateObj);
- this.history.push({
- // Log FSRS skip
- path,
- timestamp: effectiveReviewDate,
- response: 1 /* IncorrectResponse */,
- // Approx. for log
- interval: schedule.fsrsData.scheduled_days,
- ease: Math.round(schedule.fsrsData.difficulty * 10),
- isSkipped: true
- });
- } else {
- let qualityRating = toSM2Quality(response);
- qualityRating = Math.max(0, qualityRating - 1);
- this.history.push({
- path,
- timestamp: effectiveReviewDate,
- response: qualityRating,
- interval: schedule.interval || 0,
- ease: schedule.ease || 0,
- isSkipped: true
- });
- const effectiveUTCDayStart = DateUtils.startOfUTCDay(reviewDateObj);
- schedule.lastReviewDate = effectiveUTCDayStart;
- if (schedule.scheduleCategory === "initial") {
- schedule.interval = 1;
- schedule.nextReviewDate = DateUtils.addDays(effectiveUTCDayStart, 1);
- } else {
- const { interval, ease, repetitionCount } = this.calculateSM2Schedule(
- schedule.interval || 0,
- schedule.ease || this.plugin.settings.baseEase,
- qualityRating,
- schedule.repetitionCount || 0,
- 0,
- true
- // daysLate = 0 for a skip, isSkipped = true
- );
- schedule.interval = interval;
- schedule.ease = ease;
- schedule.repetitionCount = repetitionCount;
- schedule.nextReviewDate = DateUtils.addDays(effectiveUTCDayStart, interval);
- }
- schedule.consecutive = 0;
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
- /**
- * Postpone a note's review
- *
- * @param path Path to the note file
- * @param days Number of days to postpone (default: 1)
- */
- async postponeNote(path, days = 1) {
- const schedule = this.schedules[path];
- if (!schedule)
- return;
- schedule.nextReviewDate = DateUtils.addDays(schedule.nextReviewDate, days);
- if (this.plugin.events) {
- setTimeout(() => {
- this.plugin.events.emit("sidebar-update");
- }, 50);
- }
- new import_obsidian24.Notice(`Review postponed for ${days} day${days !== 1 ? "s" : ""}`);
- }
- /**
- * Advance a note's review by one day, if eligible.
- *
- * @param path Path to the note file
- * @returns True if the note was advanced, false otherwise.
- */
- async advanceNote(path) {
- const schedule = this.schedules[path];
- if (!schedule) {
- return false;
- }
- const todayUTCMidnight = DateUtils.startOfUTCDay(/* @__PURE__ */ new Date());
- const noteReviewUTCDayStart = DateUtils.startOfUTCDay(new Date(schedule.nextReviewDate));
- if (noteReviewUTCDayStart <= todayUTCMidnight) {
- return false;
- }
- const newPotentialNextReviewTimestamp = DateUtils.addDays(schedule.nextReviewDate, -1);
- if (schedule.schedulingAlgorithm === "sm2") {
- schedule.nextReviewDate = Math.max(todayUTCMidnight, DateUtils.startOfUTCDay(new Date(newPotentialNextReviewTimestamp)));
- } else {
- schedule.nextReviewDate = Math.max(todayUTCMidnight, newPotentialNextReviewTimestamp);
- }
- if (this.plugin.events) {
- setTimeout(() => {
- this.plugin.events.emit("sidebar-update");
- }, 50);
- }
- return true;
- }
- /**
- * Remove a note from the review schedule
- *
- * @param path Path to the note file
- */
- async removeFromReview(path) {
- if (this.schedules[path]) {
- delete this.schedules[path];
- this.customNoteOrder = this.customNoteOrder.filter((p2) => p2 !== path);
- new import_obsidian24.Notice("Note removed from review schedule");
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
- }
- /**
- * Clear all review schedules
- */
- async clearAllSchedules() {
- this.schedules = {};
- this.customNoteOrder = [];
- new import_obsidian24.Notice("All review schedules have been cleared");
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- if (this.plugin.reviewController) {
- await this.plugin.reviewController.updateTodayNotes();
- }
- }
- /**
- * Estimate review time for a note
- *
- * @param path Path to the note file
- * @returns Estimated review time in seconds
- */
- async estimateReviewTime(path) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!(file instanceof import_obsidian24.TFile))
- return 60;
- try {
- const content = await this.plugin.app.vault.read(file);
- return EstimationUtils.estimateReviewTime(file, content);
- } catch (error) {
- return 60;
- }
- }
- /**
- * Schedule multiple notes for review in a specific order
- *
- * @param paths Array of note paths in the order they should be processed
- * @param daysFromNow Days until first review (default: 0, same day)
- * @returns Number of notes scheduled
- */
- async scheduleNotesInOrder(paths, daysFromNow = 0) {
- let count = 0;
- for (const path of paths) {
- const file = this.plugin.app.vault.getAbstractFileByPath(path);
- if (!file || !(file instanceof import_obsidian24.TFile) || file.extension !== "md" || this.schedules[path]) {
- continue;
- }
- const now = Date.now();
- const todayUTCStart = DateUtils.startOfUTCDay(new Date(now));
- const defaultAlgorithm = this.plugin.settings.defaultSchedulingAlgorithm;
- let newSchedule;
- if (defaultAlgorithm === "fsrs") {
- const fsrsData = this.fsrsService.createNewFsrsCardData(new Date(now));
- newSchedule = {
- path,
- lastReviewDate: null,
- nextReviewDate: now,
- reviewCount: 0,
- // FSRS due now
- schedulingAlgorithm: "fsrs",
- fsrsData,
- ease: this.plugin.settings.baseEase,
- interval: 0,
- consecutive: 0,
- repetitionCount: 0,
- scheduleCategory: void 0
- };
- } else {
- newSchedule = {
- path,
- lastReviewDate: null,
- nextReviewDate: DateUtils.addDays(todayUTCStart, daysFromNow),
- // UTC midnight
- ease: this.plugin.settings.baseEase,
- interval: daysFromNow,
- consecutive: 0,
- reviewCount: 0,
- repetitionCount: 0,
- scheduleCategory: this.plugin.settings.useInitialSchedule ? "initial" : "spaced",
- schedulingAlgorithm: "sm2",
- fsrsData: void 0
- };
- if (newSchedule.scheduleCategory === "initial") {
- const initialIntervals = this.plugin.settings.initialScheduleCustomIntervals;
- if (initialIntervals && initialIntervals.length > 0) {
- newSchedule.interval = daysFromNow > 0 ? daysFromNow : initialIntervals[0];
- }
- if (daysFromNow === 0) {
- newSchedule.nextReviewDate = DateUtils.addDays(todayUTCStart, newSchedule.interval);
- }
- }
- }
- this.schedules[path] = newSchedule;
- if (!this.customNoteOrder.includes(path)) {
- this.customNoteOrder.push(path);
- }
- count++;
- }
- if (count > 0) {
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
- return count;
- }
- /**
- * Update the custom note order - used to maintain user-defined ordering
- *
- * @param order Array of note paths in desired order
- */
- async updateCustomNoteOrder(order) {
- const uniqueValidPaths = Array.from(new Set(order)).filter((path) => this.schedules[path] !== void 0);
- this.customNoteOrder = uniqueValidPaths;
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
- /**
- * Get due notes ordered by custom order if available
- *
- * @param date Optional target date (default: now)
- * @param useCustomOrder Whether to apply custom ordering (default: true)
- * @param matchExactDate Passed to getDueNotes to filter by exact date if true.
- * @returns Array of due note schedules sorted appropriately
- */
- getDueNotesWithCustomOrder(date = Date.now(), useCustomOrder = true, matchExactDate = false) {
- const dueNotes = this.getDueNotes(date, matchExactDate);
- if (!useCustomOrder || this.customNoteOrder.length === 0) {
- return dueNotes;
- }
- const notesByPath = {};
- dueNotes.forEach((note) => {
- notesByPath[note.path] = note;
- });
- const notesInOrder = [];
- const orderedPaths = /* @__PURE__ */ new Set();
- for (const path of this.customNoteOrder) {
- if (notesByPath[path]) {
- notesInOrder.push(notesByPath[path]);
- orderedPaths.add(path);
- }
- }
- for (const note of dueNotes) {
- if (!orderedPaths.has(note.path)) {
- notesInOrder.push(note);
- }
- }
- return notesInOrder;
- }
- /**
- * Handles the renaming of a note file.
- * Updates the schedule and custom order if the note was scheduled.
- *
- * @param oldPath The original path of the note.
- * @param newPath The new path of the note.
- */
- handleNoteRename(oldPath, newPath) {
- if (this.schedules[oldPath]) {
- const schedule = this.schedules[oldPath];
- delete this.schedules[oldPath];
- schedule.path = newPath;
- this.schedules[newPath] = schedule;
- const oldPathIndex = this.customNoteOrder.indexOf(oldPath);
- if (oldPathIndex > -1) {
- this.customNoteOrder[oldPathIndex] = newPath;
- } else {
- if (!this.customNoteOrder.includes(newPath)) {
- this.customNoteOrder.push(newPath);
- }
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
- }
- // Helper method for backward compatibility (moved from DataStorage)
- getRepetitionCount(interval) {
- if (interval <= 1)
- return 0;
- if (interval <= 6)
- return 1;
- return 2;
- }
- async convertAllSm2ToFsrs() {
- let convertedCount = 0;
- for (const path in this.schedules) {
- if (Object.prototype.hasOwnProperty.call(this.schedules, path)) {
- const schedule = this.schedules[path];
- if (schedule.schedulingAlgorithm === "sm2") {
- schedule.schedulingAlgorithm = "fsrs";
- const baseDate = schedule.lastReviewDate ? new Date(schedule.lastReviewDate) : /* @__PURE__ */ new Date();
- schedule.fsrsData = this.fsrsService.createNewFsrsCardData(baseDate);
- schedule.nextReviewDate = baseDate.getTime();
- schedule.ease = this.plugin.settings.baseEase;
- schedule.interval = 0;
- schedule.repetitionCount = 0;
- schedule.consecutive = 0;
- schedule.scheduleCategory = void 0;
- convertedCount++;
- }
- }
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
- async convertAllFsrsToSm2() {
- let convertedCount = 0;
- for (const path in this.schedules) {
- if (Object.prototype.hasOwnProperty.call(this.schedules, path)) {
- const schedule = this.schedules[path];
- if (schedule.schedulingAlgorithm === "fsrs") {
- schedule.schedulingAlgorithm = "sm2";
- schedule.ease = this.plugin.settings.baseEase;
- schedule.interval = 0;
- schedule.repetitionCount = 0;
- schedule.consecutive = 0;
- schedule.scheduleCategory = this.plugin.settings.useInitialSchedule ? "initial" : "spaced";
- const now = Date.now();
- const todayUTCStart = DateUtils.startOfUTCDay(new Date(now));
- let nextReview = DateUtils.addDays(todayUTCStart, 0);
- if (schedule.scheduleCategory === "initial" && this.plugin.settings.initialScheduleCustomIntervals.length > 0) {
- schedule.interval = this.plugin.settings.initialScheduleCustomIntervals[0];
- nextReview = DateUtils.addDays(todayUTCStart, schedule.interval);
- }
- schedule.nextReviewDate = nextReview;
- schedule.fsrsData = void 0;
- convertedCount++;
- }
- }
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- }
-};
-
-// services/review-history-service.ts
-var ReviewHistoryService = class {
- /**
- * Initialize Review History Service
- *
- * @param history Reference to the history array in DataStorage
- */
- constructor(history) {
- this.history = history;
- }
- /**
- * Get review history for a specific note
- *
- * @param path Path to the note file
- * @returns Array of review history items for the note
- */
- getNoteHistory(path) {
- return this.history.filter((item) => item.path === path).sort((a, b2) => b2.timestamp - a.timestamp);
- }
- /**
- * Add a history item (used by ReviewScheduleService)
- * This method is here to centralize history management, even if called from another service.
- *
- * @param item The history item to add
- */
- addHistoryItem(item) {
- this.history.push(item);
- if (this.history.length > 1e3) {
- this.history = this.history.slice(-1e3);
- }
- }
-};
-
-// services/review-session-service.ts
-var import_obsidian25 = require("obsidian");
-
-// models/review-session.ts
-function generateSessionId(prefix = "session") {
- return `${prefix}_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
-}
-function getNextFileInSession(session) {
- if (!session.hierarchy.traversalOrder.length) {
- return null;
- }
- if (session.currentIndex >= session.hierarchy.traversalOrder.length) {
- return null;
- }
- return session.hierarchy.traversalOrder[session.currentIndex];
-}
-function advanceSession(session) {
- return {
- ...session,
- currentIndex: session.currentIndex + 1,
- updatedAt: Date.now()
- };
-}
-function isSessionComplete(session) {
- return session.currentIndex >= session.hierarchy.traversalOrder.length;
-}
-
-// services/review-session-service.ts
-var ReviewSessionService = class {
- /**
- * Initialize Review Session Service
- *
- * @param plugin Reference to the main plugin
- * @param reviewSessions Reference to the reviewSessions object in DataStorage
- */
- constructor(plugin, reviewSessions) {
- this.plugin = plugin;
- this.reviewSessions = reviewSessions;
- }
- /**
- * Create a new review session for a folder
- *
- * @param folderPath Path to the folder
- * @param name Name for the session
- * @returns Created review session or null if failed
- */
- async createReviewSession(folderPath, name) {
- const folder = this.plugin.app.vault.getAbstractFileByPath(folderPath);
- if (!folder || !(folder instanceof import_obsidian25.TFolder)) {
- new import_obsidian25.Notice("Invalid folder for review session");
- return null;
- }
- try {
- const includeSubfolders = this.plugin.settings.includeSubfolders;
- const hierarchy = await LinkAnalyzer.analyzeFolder(
- this.plugin.app.vault,
- folder,
- includeSubfolders
- );
- const id = generateSessionId(folder.name);
- const session = {
- id,
- name: name || folder.name,
- path: folderPath,
- hierarchy,
- currentIndex: 0,
- createdAt: Date.now(),
- updatedAt: Date.now(),
- isActive: false
- };
- this.reviewSessions.sessions[id] = session;
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- return session;
- } catch (error) {
- new import_obsidian25.Notice("Failed to create review session");
- return null;
- }
- }
- /**
- * Set the active review session
- *
- * @param sessionId ID of the session to activate
- * @returns Whether the session was activated
- */
- async setActiveSession(sessionId) {
- if (sessionId === null) {
- this.reviewSessions.activeSessionId = null;
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- return true;
- }
- const session = this.reviewSessions.sessions[sessionId];
- if (!session) {
- return false;
- }
- this.reviewSessions.activeSessionId = sessionId;
- session.isActive = true;
- session.updatedAt = Date.now();
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- return true;
- }
- /**
- * Get the active review session
- *
- * @returns Active review session or null if none
- */
- getActiveSession() {
- const id = this.reviewSessions.activeSessionId;
- if (!id) {
- return null;
- }
- return this.reviewSessions.sessions[id] || null;
- }
- /**
- * Get the next file to review in the active session
- *
- * @returns Path to the next file or null if done
- */
- getNextSessionFile() {
- const session = this.getActiveSession();
- if (!session) {
- return null;
- }
- return getNextFileInSession(session);
- }
- /**
- * Advance to the next file in the active session
- *
- * @returns Whether there are more files to review
- */
- async advanceActiveSession() {
- const session = this.getActiveSession();
- if (!session) {
- return false;
- }
- const updatedSession = advanceSession(session);
- this.reviewSessions.sessions[session.id] = updatedSession;
- if (isSessionComplete(updatedSession)) {
- updatedSession.isActive = false;
- this.reviewSessions.activeSessionId = null;
- new import_obsidian25.Notice(`Completed review session: ${updatedSession.name}`);
- }
- if (this.plugin.events) {
- this.plugin.events.emit("sidebar-update");
- }
- return !isSessionComplete(updatedSession);
- }
- /**
- * Schedule all files in a session for review
- * (This method depends on ReviewScheduleService, will need to pass it in or access via plugin)
- *
- * @param sessionId ID of the session
- * @returns Number of files scheduled
- */
- async scheduleSessionForReview(sessionId) {
- const session = this.reviewSessions.sessions[sessionId];
- if (!session) {
- return 0;
- }
- if (!this.plugin.reviewScheduleService) {
- return 0;
- }
- return await this.plugin.reviewScheduleService.scheduleNotesInOrder(session.hierarchy.traversalOrder);
- }
-};
-
-// services/mcq-service.ts
-var MCQService = class {
- /**
- * Initialize MCQ Service
- *
- * @param mcqSets Reference to the mcqSets object in DataStorage
- * @param mcqSessions Reference to the mcqSessions object in DataStorage
- */
- constructor(mcqSets, mcqSessions) {
- this.mcqSets = mcqSets;
- this.mcqSessions = mcqSessions;
- }
- /**
- * Save an MCQ set
- *
- * @param mcqSet MCQ set to save
- * @returns The ID of the saved MCQ set
- */
- saveMCQSet(mcqSet) {
- const id = `${mcqSet.notePath}_${mcqSet.generatedAt}`;
- this.mcqSets[id] = mcqSet;
- return id;
- }
- /**
- * Get the latest MCQ set for a note
- *
- * @param notePath Path to the note
- * @returns MCQ set or null if none exists
- */
- getMCQSetForNote(notePath) {
- try {
- if (!notePath) {
- return null;
- }
- if (!this.mcqSets) {
- this.mcqSets = {};
- return null;
- }
- const sets = Object.values(this.mcqSets).filter((set) => set && set.notePath === notePath && set.questions && set.questions.length > 0).sort((a, b2) => b2.generatedAt - a.generatedAt);
- if (sets.length > 0) {
- return sets[0];
- }
- return null;
- } catch (error) {
- return null;
- }
- }
- /**
- * Save an MCQ session
- *
- * @param session MCQ session to save
- */
- saveMCQSession(session) {
- try {
- if (!session || !session.notePath || !session.mcqSetId) {
- return;
- }
- if (!this.mcqSessions) {
- this.mcqSessions = {};
- }
- if (!this.mcqSessions[session.notePath]) {
- this.mcqSessions[session.notePath] = [];
- }
- const existingIndex = this.mcqSessions[session.notePath].findIndex(
- (s) => s && s.mcqSetId === session.mcqSetId && s.startedAt === session.startedAt
- );
- if (existingIndex >= 0) {
- this.mcqSessions[session.notePath][existingIndex] = session;
- } else {
- this.mcqSessions[session.notePath].push(session);
- }
- if (this.mcqSessions[session.notePath].length > 10) {
- this.mcqSessions[session.notePath].sort((a, b2) => b2.startedAt - a.startedAt);
- this.mcqSessions[session.notePath] = this.mcqSessions[session.notePath].slice(0, 10);
- }
- } catch (error) {
- }
- }
- /**
- * Get all MCQ sessions for a note
- *
- * @param notePath Path to the note
- * @returns Array of MCQ sessions
- */
- getMCQSessionsForNote(notePath) {
- return this.mcqSessions[notePath] || [];
- }
- /**
- * Get the latest MCQ session for a note
- *
- * @param notePath Path to the note
- * @returns Latest MCQ session or null
- */
- getLatestMCQSessionForNote(notePath) {
- const sessions = this.getMCQSessionsForNote(notePath).sort((a, b2) => b2.startedAt - a.startedAt);
- return sessions.length > 0 ? sessions[0] : null;
- }
- /**
- * Flags an MCQ set for regeneration.
- * This is typically called when a note's review rating meets certain criteria.
- *
- * @param notePath Path to the note whose MCQ set should be flagged.
- */
- flagMCQSetForRegeneration(notePath) {
- const mcqSet = this.getMCQSetForNote(notePath);
- if (mcqSet) {
- mcqSet.needsQuestionRegeneration = true;
- this.saveMCQSet(mcqSet);
- } else {
- }
- }
-};
-
-// services/pomodoro-service.ts
-var PomodoroService = class {
- constructor(plugin) {
- this.timerInterval = null;
- this.plugin = plugin;
- if (this.plugin.settings.pomodoroEnabled && this.plugin.pluginState.pomodoroIsRunning) {
- this.recalculateTimeLeftFromEndTime();
- this.startTimerInterval();
- }
- }
- get settings() {
- return this.plugin.settings;
- }
- get state() {
- return this.plugin.pluginState;
- }
- // --- Public API ---
- start() {
- if (this.state.pomodoroIsRunning)
- return;
- this.state.pomodoroIsRunning = true;
- if (this.state.pomodoroCurrentMode === "idle") {
- this.switchToMode("work");
- } else if (this.state.pomodoroTimeLeftInSeconds <= 0) {
- this.handleTimerEnd();
- }
- this.state.pomodoroEndTimeMs = Date.now() + this.state.pomodoroTimeLeftInSeconds * 1e3;
- this.startTimerInterval();
- this.notifyUpdate();
- this.plugin.savePluginData();
- }
- stop() {
- if (!this.state.pomodoroIsRunning)
- return;
- this.stopTimerInterval();
- this.state.pomodoroIsRunning = false;
- if (this.state.pomodoroEndTimeMs) {
- const remainingMs = this.state.pomodoroEndTimeMs - Date.now();
- this.state.pomodoroTimeLeftInSeconds = Math.max(0, Math.round(remainingMs / 1e3));
- }
- this.state.pomodoroEndTimeMs = null;
- this.notifyUpdate();
- this.plugin.savePluginData();
- }
- resetCurrentSession() {
- this.stopTimerInterval();
- this.state.pomodoroIsRunning = false;
- this.state.pomodoroEndTimeMs = null;
- this.resetTimeForMode(this.state.pomodoroCurrentMode === "idle" ? "work" : this.state.pomodoroCurrentMode);
- this.notifyUpdate();
- this.plugin.savePluginData();
- }
- skipSession() {
- this.stopTimerInterval();
- this.state.pomodoroIsRunning = false;
- this.handleTimerEnd(true);
- if (this.state.pomodoroIsRunning) {
- this.startTimerInterval();
- }
- this.notifyUpdate();
- this.plugin.savePluginData();
- }
- getFormattedTimeLeft() {
- const minutes = Math.floor(this.state.pomodoroTimeLeftInSeconds / 60);
- const seconds = this.state.pomodoroTimeLeftInSeconds % 60;
- return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
- }
- updateDurations(work, short, long, sessions) {
- const currentWork = this.settings.pomodoroWorkDuration;
- 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 (userChangedSettings) {
- this.state.pomodoroUserHasModifiedSettings = true;
- this.state.pomodoroIsEstimationActive = false;
- this.state.pomodoroEstimatedTotalCycles = null;
- this.state.pomodoroEstimatedWorkSessions = null;
- }
- const activeModesForDurationUpdate = ["work", "shortBreak", "longBreak"];
- if (!this.state.pomodoroIsRunning && activeModesForDurationUpdate.includes(this.state.pomodoroCurrentMode)) {
- this.resetTimeForMode(this.state.pomodoroCurrentMode);
- }
- this.plugin.savePluginData();
- this.notifyUpdate();
- }
- /**
- * Calculate and set estimation based on reading time
- */
- async calculateEstimationFromNotes(notes) {
- const userOverrideHours = this.state.pomodoroUserOverrideHours || 0;
- const userOverrideMinutes = this.state.pomodoroUserOverrideMinutes || 0;
- const userOverrideTimeInMinutes = userOverrideHours * 60 + userOverrideMinutes;
- const addToEstimation = this.state.pomodoroUserAddToEstimation || false;
- let totalReadingTimeInSeconds = 0;
- let totalReadingTimeInMinutes = 0;
- if (notes.length > 0) {
- for (const note of notes) {
- totalReadingTimeInSeconds += await this.plugin.reviewScheduleService.estimateReviewTime(note.path);
- }
- totalReadingTimeInMinutes = totalReadingTimeInSeconds / 60;
- }
- if (userOverrideTimeInMinutes > 0) {
- if (addToEstimation) {
- totalReadingTimeInMinutes += userOverrideTimeInMinutes;
- } else {
- totalReadingTimeInMinutes = userOverrideTimeInMinutes;
- totalReadingTimeInSeconds = userOverrideTimeInMinutes * 60;
- }
- }
- if (notes.length === 0 && userOverrideTimeInMinutes === 0) {
- this.state.pomodoroIsEstimationActive = false;
- this.state.pomodoroEstimatedTotalCycles = null;
- this.state.pomodoroEstimatedWorkSessions = null;
- this.notifyUpdate();
- return null;
- }
- const settings = this.settings;
- const workDuration = settings.pomodoroWorkDuration;
- const shortBreakDuration = settings.pomodoroShortBreakDuration;
- const longBreakDuration = settings.pomodoroLongBreakDuration;
- const sessionsUntilLongBreak = settings.pomodoroSessionsUntilLongBreak;
- if (totalReadingTimeInMinutes === 0 || workDuration === 0) {
- this.state.pomodoroIsEstimationActive = false;
- this.state.pomodoroEstimatedTotalCycles = null;
- this.state.pomodoroEstimatedWorkSessions = null;
- this.notifyUpdate();
- return null;
- }
- let pomodorosNeeded = 0;
- let sessionsCompletedInCycle = 0;
- let remainingReadingTimeMinutes = totalReadingTimeInMinutes;
- let totalBreakTimeInMinutes = 0;
- while (remainingReadingTimeMinutes > 0) {
- pomodorosNeeded++;
- remainingReadingTimeMinutes -= workDuration;
- sessionsCompletedInCycle++;
- if (remainingReadingTimeMinutes <= 0)
- break;
- if (sessionsCompletedInCycle >= sessionsUntilLongBreak) {
- totalBreakTimeInMinutes += longBreakDuration;
- sessionsCompletedInCycle = 0;
- } else {
- totalBreakTimeInMinutes += shortBreakDuration;
- }
- }
- this.state.pomodoroEstimatedTotalCycles = Math.ceil(pomodorosNeeded / sessionsUntilLongBreak);
- this.state.pomodoroEstimatedWorkSessions = pomodorosNeeded;
- this.state.pomodoroIsEstimationActive = true;
- this.plugin.savePluginData();
- this.notifyUpdate();
- return {
- totalReadingTimeInSeconds,
- totalReadingTimeInMinutes,
- pomodorosNeeded,
- totalTimeWithBreaksMinutes: pomodorosNeeded * workDuration + totalBreakTimeInMinutes
- };
- }
- /**
- * Get current cycle progress information
- */
- getCycleProgress() {
- if (!this.state.pomodoroIsEstimationActive || !this.state.pomodoroEstimatedWorkSessions) {
- return null;
- }
- const currentCycle = Math.floor(this.state.pomodoroSessionsCompletedInCycle / this.settings.pomodoroSessionsUntilLongBreak) + 1;
- const totalCycles = this.state.pomodoroEstimatedTotalCycles || 1;
- const workSessionsRemaining = Math.max(0, this.state.pomodoroEstimatedWorkSessions - this.state.pomodoroSessionsCompletedInCycle);
- const totalWorkSessions = this.state.pomodoroEstimatedWorkSessions;
- const workDuration = this.settings.pomodoroWorkDuration;
- const shortBreakDuration = this.settings.pomodoroShortBreakDuration;
- const longBreakDuration = this.settings.pomodoroLongBreakDuration;
- const sessionsUntilLongBreak = this.settings.pomodoroSessionsUntilLongBreak;
- let totalBreakTime = 0;
- let sessionsInCycle = 0;
- for (let i = 0; i < totalWorkSessions; i++) {
- sessionsInCycle++;
- if (i < totalWorkSessions - 1) {
- if (sessionsInCycle >= sessionsUntilLongBreak) {
- totalBreakTime += longBreakDuration;
- sessionsInCycle = 0;
- } else {
- totalBreakTime += shortBreakDuration;
- }
- }
- }
- const totalTimeMinutes = totalWorkSessions * workDuration + totalBreakTime;
- return {
- current: currentCycle,
- total: totalCycles,
- workSessionsRemaining,
- totalWorkSessions,
- totalTimeMinutes
- };
- }
- /**
- * Reset estimation state (call when user wants to clear estimation)
- */
- resetEstimation() {
- this.state.pomodoroIsEstimationActive = false;
- this.state.pomodoroEstimatedTotalCycles = null;
- this.state.pomodoroEstimatedWorkSessions = null;
- this.state.pomodoroUserHasModifiedSettings = false;
- this.plugin.savePluginData();
- this.notifyUpdate();
- }
- // --- Internal Logic ---
- startTimerInterval() {
- if (this.timerInterval !== null) {
- window.clearInterval(this.timerInterval);
- }
- if (!this.settings.pomodoroEnabled || !this.state.pomodoroIsRunning)
- return;
- this.timerInterval = window.setInterval(() => {
- this.tick();
- }, 1e3);
- }
- stopTimerInterval() {
- if (this.timerInterval !== null) {
- window.clearInterval(this.timerInterval);
- this.timerInterval = null;
- }
- }
- tick() {
- if (!this.state.pomodoroIsRunning || !this.state.pomodoroEndTimeMs) {
- this.stop();
- return;
- }
- const now = Date.now();
- const remainingMs = this.state.pomodoroEndTimeMs - now;
- const remainingSeconds = Math.max(0, Math.round(remainingMs / 1e3));
- this.state.pomodoroTimeLeftInSeconds = remainingSeconds;
- if (remainingSeconds <= 0) {
- this.handleTimerEnd();
- } else {
- this.notifyUpdate();
- }
- }
- handleTimerEnd(skipped = false) {
- this.stopTimerInterval();
- if (this.settings.pomodoroSoundEnabled && !skipped) {
- this.playSoundNotification();
- }
- const currentMode = this.state.pomodoroCurrentMode;
- let nextMode = "idle";
- if (currentMode === "work") {
- this.state.pomodoroSessionsCompletedInCycle++;
- if (this.state.pomodoroSessionsCompletedInCycle >= this.settings.pomodoroSessionsUntilLongBreak) {
- nextMode = "longBreak";
- } else {
- nextMode = "shortBreak";
- }
- } else if (currentMode === "shortBreak" || currentMode === "longBreak") {
- nextMode = "work";
- if (currentMode === "longBreak") {
- this.state.pomodoroSessionsCompletedInCycle = 0;
- }
- }
- const wasRunning = this.state.pomodoroIsRunning;
- this.switchToMode(nextMode);
- if (nextMode !== "idle") {
- this.state.pomodoroIsRunning = true;
- this.state.pomodoroEndTimeMs = Date.now() + this.state.pomodoroTimeLeftInSeconds * 1e3;
- if (!wasRunning) {
- this.startTimerInterval();
- } else if (!this.timerInterval) {
- this.startTimerInterval();
- }
- } else {
- this.state.pomodoroIsRunning = false;
- this.state.pomodoroEndTimeMs = null;
- this.stopTimerInterval();
- }
- this.plugin.savePluginData();
- this.notifyUpdate();
- }
- switchToMode(mode) {
- this.state.pomodoroCurrentMode = mode;
- this.resetTimeForMode(mode);
- }
- resetTimeForMode(mode) {
- switch (mode) {
- case "work":
- this.state.pomodoroTimeLeftInSeconds = this.settings.pomodoroWorkDuration * 60;
- break;
- case "shortBreak":
- this.state.pomodoroTimeLeftInSeconds = this.settings.pomodoroShortBreakDuration * 60;
- break;
- case "longBreak":
- this.state.pomodoroTimeLeftInSeconds = this.settings.pomodoroLongBreakDuration * 60;
- break;
- case "idle":
- this.state.pomodoroTimeLeftInSeconds = this.settings.pomodoroWorkDuration * 60;
- break;
- }
- }
- playSoundNotification() {
- try {
- const audioContext = new (window.AudioContext || window.webkitAudioContext)();
- if (!audioContext)
- return;
- const oscillator = audioContext.createOscillator();
- const gainNode = audioContext.createGain();
- oscillator.connect(gainNode);
- gainNode.connect(audioContext.destination);
- oscillator.type = "sine";
- oscillator.frequency.setValueAtTime(440, audioContext.currentTime);
- gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
- oscillator.start();
- oscillator.stop(audioContext.currentTime + 0.5);
- } catch (e) {
- }
- }
- notifyUpdate() {
- this.plugin.events.emit("pomodoro-update");
- }
- // Call this when global settings change from the settings tab
- onSettingsChanged() {
- if (!this.settings.pomodoroEnabled) {
- this.stop();
- this.state.pomodoroCurrentMode = "idle";
- this.resetTimeForMode("idle");
- } else {
- if (!this.state.pomodoroIsRunning) {
- const currentMode = this.state.pomodoroCurrentMode;
- const activeModes = ["work", "shortBreak", "longBreak"];
- if (activeModes.includes(currentMode)) {
- this.resetTimeForMode(currentMode);
- }
- }
- }
- this.notifyUpdate();
- }
- destroy() {
- this.stopTimerInterval();
- }
- // Recalculate time left based on stored end time, useful on load/reinit
- recalculateTimeLeftFromEndTime() {
- if (this.state.pomodoroIsRunning && this.state.pomodoroEndTimeMs) {
- const now = Date.now();
- const remainingMs = this.state.pomodoroEndTimeMs - now;
- this.state.pomodoroTimeLeftInSeconds = Math.max(0, Math.round(remainingMs / 1e3));
- if (remainingMs <= 0) {
- this.handleTimerEnd(true);
- }
- } else if (!this.state.pomodoroIsRunning) {
- this.state.pomodoroEndTimeMs = null;
- }
- }
- // Call this after pluginState has been externally modified (e.g., by data import)
- reinitializeTimerFromState() {
- this.stopTimerInterval();
- if (this.settings.pomodoroEnabled) {
- this.recalculateTimeLeftFromEndTime();
- if (this.state.pomodoroIsRunning) {
- this.startTimerInterval();
- }
- } else {
- this.stop();
- this.state.pomodoroCurrentMode = "idle";
- this.resetTimeForMode("idle");
- this.state.pomodoroEndTimeMs = null;
- }
- this.notifyUpdate();
- }
-};
-
-// main.ts
-var SpaceforgePlugin = class extends import_obsidian26.Plugin {
- constructor() {
- super(...arguments);
- this.stylesheetPath = "styles.css";
- this.stylesheetId = "spaceforge-styles";
- this.lastStylesModTime = null;
- this.cssHotReloadIntervalId = null;
- // sidebarView: ReviewSidebarView; // Avoid storing direct references to views
- this.clickedDateFromCalendar = null;
- }
- async onload() {
- var _a;
- this.events = new EventEmitter();
- this.settings = { ...DEFAULT_SETTINGS };
- this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA };
- this.reviewScheduleService = new ReviewScheduleService(
- this,
- this.pluginState.schedules,
- this.pluginState.customNoteOrder,
- (_a = this.pluginState.lastLinkAnalysisTimestamp) != null ? _a : null,
- // Coalesce undefined to null
- this.pluginState.history
- );
- this.reviewHistoryService = new ReviewHistoryService(this.pluginState.history);
- this.reviewSessionService = new ReviewSessionService(this, this.pluginState.reviewSessions);
- this.mcqService = new MCQService(this.pluginState.mcqSets, this.pluginState.mcqSessions);
- await this.loadPluginData();
- this.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- this.pomodoroService = new PomodoroService(this);
- this.registerView(
- "spaceforge-review-schedule",
- (leaf) => new ReviewSidebarView(leaf, this)
- );
- this.dataStorage = new DataStorage(
- this,
- this.reviewScheduleService,
- this.reviewHistoryService,
- this.reviewSessionService,
- this.mcqService
- );
- this.reviewController = new ReviewController(this, this.mcqService);
- this.navigationController = new ReviewNavigationController(this);
- this.sessionController = new ReviewSessionController(this);
- this.batchController = new ReviewBatchController(this);
- this.contextMenuHandler = new ContextMenuHandler(this);
- this.initializeMCQComponents();
- EstimationUtils.setPlugin(this);
- this.addIcons();
- this.contextMenuHandler.register();
- this.addRibbonIcon("calendar-clock", "Spaceforge Review", async () => {
- await this.activateSidebarView();
- });
- this.addSettingTab(new SpaceforgeSettingTab(this.app, this));
- this.addCommands();
- this.addCommand({
- id: "add-selected-file-to-review",
- name: "Add Selected File to Review Schedule (File Explorer)",
- callback: async () => {
- const fileExplorerLeaf = this.app.workspace.getLeavesOfType("file-explorer")[0];
- const viewWithFile = fileExplorerLeaf == null ? void 0 : fileExplorerLeaf.view;
- if (viewWithFile == null ? void 0 : viewWithFile.file) {
- const selectedFile = viewWithFile.file;
- if (selectedFile instanceof import_obsidian26.TFile && selectedFile.extension === "md") {
- await this.reviewScheduleService.scheduleNoteForReview(selectedFile.path);
- await this.savePluginData();
- new import_obsidian26.Notice(`Added "${selectedFile.path}" to review schedule.`);
- } else {
- new import_obsidian26.Notice("Selected item is not a markdown file.");
- }
- } else {
- new import_obsidian26.Notice("No file selected in file explorer.");
- }
- }
- });
- this.registerEvent(this.app.workspace.on("file-open", (file) => {
- }));
- this.registerEvent(this.app.vault.on("delete", async (file) => {
- var _a2;
- if (file instanceof import_obsidian26.TFile && file.extension === "md") {
- await this.reviewScheduleService.removeFromReview(file.path);
- await this.savePluginData();
- }
- (_a2 = this.getSidebarView()) == null ? void 0 : _a2.refresh();
- }));
- this.registerEvent(this.app.vault.on("rename", async (file, oldPath) => {
- var _a2;
- if (file instanceof import_obsidian26.TFile && file.extension === "md") {
- this.reviewScheduleService.handleNoteRename(oldPath, file.path);
- await this.savePluginData();
- }
- (_a2 = this.getSidebarView()) == null ? void 0 : _a2.refresh();
- }));
- this.registerInterval(window.setInterval(() => {
- var _a2;
- (_a2 = this.getSidebarView()) == null ? void 0 : _a2.refresh();
- }, 60 * 1e3));
- this.app.workspace.onLayoutReady(() => this.activateSidebarView());
- 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) {
- }
- }, 1e3);
- this.registerInterval(this.cssHotReloadIntervalId);
- }
- if (this.settings.notifyBeforeDue > 0) {
- this.registerInterval(window.setInterval(() => this.checkForDueNotes(), 5 * 60 * 1e3));
- }
- this.registerInterval(window.setInterval(async () => {
- await this.savePluginData();
- }, 5 * 60 * 1e3));
- this.beforeUnloadHandler = (event) => {
- let existingData = {};
- try {
- const loadedData = this.loadData();
- if (loadedData && !(loadedData instanceof Promise)) {
- existingData = loadedData;
- } else if (loadedData instanceof Promise) {
- }
- } catch (loadError) {
- }
- try {
- const reviewData = {
- schedules: this.reviewScheduleService.schedules,
- history: this.reviewHistoryService.history,
- reviewSessions: this.reviewSessionService.reviewSessions,
- mcqSets: this.mcqService.mcqSets,
- mcqSessions: this.mcqService.mcqSessions,
- customNoteOrder: this.reviewScheduleService.customNoteOrder,
- lastLinkAnalysisTimestamp: this.reviewScheduleService.lastLinkAnalysisTimestamp,
- version: this.manifest.version
- };
- const combinedData = { ...existingData, reviewData };
- let backupStr = JSON.stringify(combinedData);
- window.localStorage.setItem("spaceforge-backup", backupStr);
- (async () => {
- try {
- await this.savePluginData();
- } catch (e) {
- }
- })();
- } 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 } });
- window.localStorage.setItem("spaceforge-minimal-backup", minimalBackup);
- } catch (minimalError) {
- }
- }
- };
- window.addEventListener("beforeunload", this.beforeUnloadHandler);
- }
- async onunload() {
- if (this.cssHotReloadIntervalId !== null) {
- window.clearInterval(this.cssHotReloadIntervalId);
- this.cssHotReloadIntervalId = null;
- }
- const styleEl = document.getElementById(this.stylesheetId);
- if (styleEl)
- styleEl.remove();
- if (this.beforeUnloadHandler) {
- window.removeEventListener("beforeunload", this.beforeUnloadHandler);
- }
- let existingData = {};
- try {
- const loaded = await this.loadData();
- if (loaded) {
- existingData = loaded;
- }
- } catch (loadError) {
- }
- try {
- const reviewData = {
- schedules: this.reviewScheduleService.schedules,
- history: this.reviewHistoryService.history,
- reviewSessions: this.reviewSessionService.reviewSessions,
- mcqSets: this.mcqService.mcqSets,
- mcqSessions: this.mcqService.mcqSessions,
- customNoteOrder: this.reviewScheduleService.customNoteOrder,
- lastLinkAnalysisTimestamp: this.reviewScheduleService.lastLinkAnalysisTimestamp,
- version: this.manifest.version
- };
- const combinedData = { ...existingData, reviewData };
- const backupStr = JSON.stringify(combinedData);
- window.localStorage.setItem("spaceforge-backup", backupStr);
- } catch (backupError) {
- }
- try {
- await this.savePluginData();
- } catch (error) {
- }
- if (this.pomodoroService)
- this.pomodoroService.destroy();
- }
- // private async _getEffectiveDataPathFromLocalStorage(): Promise {
- // const customPath = await this.app.loadLocalStorage('spaceforgeCustomDataPath');
- // return (customPath && typeof customPath === 'string' && customPath.trim() !== '') ? customPath.trim() : null;
- // }
- async _getEffectiveDataPath() {
- var _a, _b;
- if ((_a = this.settings) == null ? void 0 : _a.useCustomDataPath) {
- const relativePath = (_b = this.settings.customDataPath) == null ? void 0 : _b.trim();
- if (relativePath && relativePath !== "") {
- let pathForJson = relativePath;
- if (!pathForJson.endsWith("/")) {
- pathForJson += "/";
- }
- pathForJson += "data.json";
- const vaultBasePath = this.app.vault.getRoot().path;
- let absolutePath = (vaultBasePath ? vaultBasePath + "/" : "") + pathForJson;
- absolutePath = (0, import_obsidian26.normalizePath)(absolutePath);
- return absolutePath;
- } else {
- return null;
- }
- }
- return null;
- }
- async loadPluginData() {
- var _a, _b, _c, _d, _e, _f;
- const lsUseCustomPath = await this.app.loadLocalStorage("spaceforge_useCustomDataPath");
- const lsCustomPathRelative = await this.app.loadLocalStorage("spaceforge_customDataPathRelative");
- this.settings = { ...DEFAULT_SETTINGS };
- if (typeof lsUseCustomPath === "boolean") {
- this.settings.useCustomDataPath = lsUseCustomPath;
- }
- if (typeof lsCustomPathRelative === "string") {
- this.settings.customDataPath = lsCustomPathRelative;
- }
- let rawLoadedData;
- const effectivePath = await this._getEffectiveDataPath();
- const defaultPluginDataPath = this.app.vault.configDir + `/plugins/${this.manifest.id}/data.json`;
- try {
- if (effectivePath) {
- const file = this.app.vault.getAbstractFileByPath(effectivePath);
- if (file instanceof import_obsidian26.TFile) {
- const jsonData = await this.app.vault.read(file);
- if (jsonData)
- rawLoadedData = JSON.parse(jsonData);
- new import_obsidian26.Notice(`Spaceforge: Loaded data from custom path: ${effectivePath}`, 3e3);
- } else {
- const oldFile = this.app.vault.getAbstractFileByPath(defaultPluginDataPath);
- if (oldFile instanceof import_obsidian26.TFile) {
- new import_obsidian26.Notice(`Spaceforge: Custom data file not found at ${effectivePath}. Attempting to migrate from default location.`, 5e3);
- try {
- const oldJsonData = await this.app.vault.read(oldFile);
- if (oldJsonData) {
- rawLoadedData = JSON.parse(oldJsonData);
- }
- } catch (migrationReadError) {
- }
- }
- if (!rawLoadedData) {
- new import_obsidian26.Notice(`Spaceforge: No data file found at custom path ${effectivePath}. New data file will be created on save.`, 3e3);
- }
- }
- } else {
- rawLoadedData = await this.loadData();
- }
- let loadedSettings = {};
- if ((rawLoadedData == null ? void 0 : rawLoadedData.settings) && typeof rawLoadedData.settings === "object") {
- loadedSettings = rawLoadedData.settings;
- }
- this.settings = { ...DEFAULT_SETTINGS, ...loadedSettings };
- if (typeof lsUseCustomPath === "boolean") {
- this.settings.useCustomDataPath = lsUseCustomPath;
- }
- if (typeof lsCustomPathRelative === "string") {
- this.settings.customDataPath = lsCustomPathRelative;
- }
- this.pluginState = { ...DEFAULT_APP_DATA.pluginState };
- if (rawLoadedData == null ? void 0 : rawLoadedData.pluginState) {
- this.pluginState = { ...this.pluginState, ...rawLoadedData.pluginState };
- } else if (rawLoadedData && !rawLoadedData.pluginState && rawLoadedData.schedules) {
- this.pluginState = { ...this.pluginState, ...rawLoadedData };
- }
- this.pluginState.pomodoroCurrentMode = this.pluginState.pomodoroCurrentMode || DEFAULT_PLUGIN_STATE_DATA.pomodoroCurrentMode;
- this.pluginState.pomodoroTimeLeftInSeconds = this.pluginState.pomodoroTimeLeftInSeconds || DEFAULT_PLUGIN_STATE_DATA.pomodoroTimeLeftInSeconds;
- this.pluginState.pomodoroSessionsCompletedInCycle = this.pluginState.pomodoroSessionsCompletedInCycle || DEFAULT_PLUGIN_STATE_DATA.pomodoroSessionsCompletedInCycle;
- this.pluginState.pomodoroIsRunning = typeof this.pluginState.pomodoroIsRunning === "boolean" ? this.pluginState.pomodoroIsRunning : DEFAULT_PLUGIN_STATE_DATA.pomodoroIsRunning;
- if (this.pluginState.schedules) {
- for (const path in this.pluginState.schedules) {
- if (Object.prototype.hasOwnProperty.call(this.pluginState.schedules, path)) {
- const schedule = this.pluginState.schedules[path];
- if (!schedule.schedulingAlgorithm) {
- schedule.schedulingAlgorithm = "sm2";
- schedule.fsrsData = void 0;
- if (!schedule.scheduleCategory) {
- schedule.scheduleCategory = this.settings.useInitialSchedule ? "initial" : "spaced";
- }
- }
- if (schedule.schedulingAlgorithm === "sm2") {
- schedule.ease = (_a = schedule.ease) != null ? _a : this.settings.baseEase;
- schedule.interval = (_b = schedule.interval) != null ? _b : 0;
- schedule.repetitionCount = (_c = schedule.repetitionCount) != null ? _c : 0;
- schedule.consecutive = (_d = schedule.consecutive) != null ? _d : 0;
- schedule.scheduleCategory = (_e = schedule.scheduleCategory) != null ? _e : this.settings.useInitialSchedule ? "initial" : "spaced";
- }
- }
- }
- }
- this.reviewScheduleService.schedules = this.pluginState.schedules || {};
- this.reviewHistoryService.history = this.pluginState.history || [];
- this.reviewSessionService.reviewSessions = this.pluginState.reviewSessions || { sessions: {}, activeSessionId: null };
- this.mcqService.mcqSets = this.pluginState.mcqSets || {};
- 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) {
- this.settings = { ...DEFAULT_SETTINGS };
- this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA };
- this.reviewScheduleService.schedules = this.pluginState.schedules || {};
- this.reviewHistoryService.history = this.pluginState.history || [];
- this.reviewSessionService.reviewSessions = this.pluginState.reviewSessions || { sessions: {}, activeSessionId: null };
- this.mcqService.mcqSets = this.pluginState.mcqSets || {};
- this.mcqService.mcqSessions = this.pluginState.mcqSessions || {};
- this.reviewScheduleService.customNoteOrder = this.pluginState.customNoteOrder || [];
- this.reviewScheduleService.lastLinkAnalysisTimestamp = (_f = this.pluginState.lastLinkAnalysisTimestamp) != null ? _f : null;
- if (this.reviewScheduleService) {
- this.reviewScheduleService.updateAlgorithmServicesForSettingsChange();
- }
- new import_obsidian26.Notice("Spaceforge: Error loading data, initialized with defaults.", 5e3);
- }
- }
- async savePluginData() {
- var _a, _b, _c, _d;
- try {
- if (!this.settings || typeof this.settings !== "object") {
- this.settings = JSON.parse(JSON.stringify(DEFAULT_SETTINGS));
- } else {
- this.settings = { ...DEFAULT_SETTINGS, ...this.settings };
- }
- if (!this.pluginState) {
- this.pluginState = { ...DEFAULT_PLUGIN_STATE_DATA };
- }
- const currentPluginState = {
- schedules: this.reviewScheduleService.schedules || {},
- history: this.reviewHistoryService.history || [],
- reviewSessions: this.reviewSessionService.reviewSessions || { sessions: {}, activeSessionId: null },
- mcqSets: this.mcqService.mcqSets || {},
- mcqSessions: this.mcqService.mcqSessions || {},
- customNoteOrder: this.reviewScheduleService.customNoteOrder || [],
- lastLinkAnalysisTimestamp: (_a = this.reviewScheduleService.lastLinkAnalysisTimestamp) != null ? _a : null,
- pomodoroCurrentMode: this.pluginState.pomodoroCurrentMode || DEFAULT_PLUGIN_STATE_DATA.pomodoroCurrentMode,
- pomodoroTimeLeftInSeconds: this.pluginState.pomodoroTimeLeftInSeconds || DEFAULT_PLUGIN_STATE_DATA.pomodoroTimeLeftInSeconds,
- pomodoroSessionsCompletedInCycle: this.pluginState.pomodoroSessionsCompletedInCycle || DEFAULT_PLUGIN_STATE_DATA.pomodoroSessionsCompletedInCycle,
- pomodoroIsRunning: typeof this.pluginState.pomodoroIsRunning === "boolean" ? this.pluginState.pomodoroIsRunning : DEFAULT_PLUGIN_STATE_DATA.pomodoroIsRunning,
- pomodoroEndTimeMs: (_b = this.pluginState.pomodoroEndTimeMs) != null ? _b : null,
- // Add the missing field
- pomodoroEstimatedTotalCycles: (_c = this.pluginState.pomodoroEstimatedTotalCycles) != null ? _c : DEFAULT_PLUGIN_STATE_DATA.pomodoroEstimatedTotalCycles,
- pomodoroEstimatedWorkSessions: (_d = this.pluginState.pomodoroEstimatedWorkSessions) != null ? _d : DEFAULT_PLUGIN_STATE_DATA.pomodoroEstimatedWorkSessions,
- pomodoroIsEstimationActive: typeof this.pluginState.pomodoroIsEstimationActive === "boolean" ? this.pluginState.pomodoroIsEstimationActive : DEFAULT_PLUGIN_STATE_DATA.pomodoroIsEstimationActive,
- pomodoroUserHasModifiedSettings: typeof this.pluginState.pomodoroUserHasModifiedSettings === "boolean" ? this.pluginState.pomodoroUserHasModifiedSettings : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserHasModifiedSettings,
- pomodoroUserOverrideHours: typeof this.pluginState.pomodoroUserOverrideHours === "number" ? this.pluginState.pomodoroUserOverrideHours : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserOverrideHours,
- pomodoroUserOverrideMinutes: typeof this.pluginState.pomodoroUserOverrideMinutes === "number" ? this.pluginState.pomodoroUserOverrideMinutes : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserOverrideMinutes,
- pomodoroUserAddToEstimation: typeof this.pluginState.pomodoroUserAddToEstimation === "boolean" ? this.pluginState.pomodoroUserAddToEstimation : DEFAULT_PLUGIN_STATE_DATA.pomodoroUserAddToEstimation,
- version: this.manifest.version
- };
- this.pluginState = currentPluginState;
- const dataToSave = {
- settings: this.settings,
- // Use the already prepared this.settings
- pluginState: this.pluginState
- };
- const effectiveSavePath = await this._getEffectiveDataPath();
- const defaultPluginDataPath = this.app.vault.configDir + `/plugins/${this.manifest.id}/data.json`;
- if (effectiveSavePath) {
- try {
- const dirPathOnly = effectiveSavePath.substring(0, effectiveSavePath.lastIndexOf("/"));
- if (dirPathOnly && !this.app.vault.getAbstractFileByPath(dirPathOnly)) {
- await this.app.vault.createFolder(dirPathOnly);
- new import_obsidian26.Notice(`Spaceforge: Created directory for custom data: ${dirPathOnly}`, 3e3);
- }
- const file = this.app.vault.getAbstractFileByPath(effectiveSavePath);
- if (file instanceof import_obsidian26.TFile) {
- await this.app.vault.modify(file, JSON.stringify(dataToSave, null, 2));
- } else {
- await this.app.vault.create(effectiveSavePath, JSON.stringify(dataToSave, null, 2));
- }
- const oldFile = this.app.vault.getAbstractFileByPath(defaultPluginDataPath);
- if (oldFile instanceof import_obsidian26.TFile) {
- await this.app.vault.delete(oldFile);
- new import_obsidian26.Notice(`Spaceforge: Removed old data file from default plugin folder as custom path is active.`, 5e3);
- }
- } catch (writeError) {
- new import_obsidian26.Notice(`Error saving data to custom path ${effectiveSavePath}: ${writeError.message}. Falling back to default path for this save.`, 1e4);
- try {
- await this.saveData(dataToSave);
- new import_obsidian26.Notice(`Spaceforge: Data saved to default plugin folder due to error with custom path.`, 5e3);
- } catch (fallbackError) {
- new import_obsidian26.Notice(`CRITICAL: Spaceforge failed to save data to both custom and default locations.`, 1e4);
- }
- }
- } else {
- await this.saveData(dataToSave);
- }
- } catch (error) {
- new import_obsidian26.Notice("Error saving Spaceforge data. Check console for details.", 5e3);
- }
- }
- async activateSidebarView() {
- const existingLeaves = this.app.workspace.getLeavesOfType("spaceforge-review-schedule");
- if (existingLeaves.length > 0) {
- this.app.workspace.revealLeaf(existingLeaves[0]);
- } else {
- const leaf = this.app.workspace.getRightLeaf(false);
- if (leaf) {
- await leaf.setViewState({
- type: "spaceforge-review-schedule",
- active: true
- });
- this.app.workspace.revealLeaf(leaf);
- } else {
- new import_obsidian26.Notice("Spaceforge: Could not open sidebar view.");
- }
- }
- }
- addCommands() {
- this.addCommand({
- id: "spaceforge-next-review-note",
- name: "Next Review Note",
- callback: () => {
- this.navigationController.navigateToNextNote();
- }
- });
- this.addCommand({
- id: "spaceforge-previous-review-note",
- name: "Previous Review Note",
- callback: () => {
- this.navigationController.navigateToPreviousNote();
- }
- });
- this.addCommand({
- id: "spaceforge-review-current-note",
- name: "Review Current Note",
- callback: () => {
- const activeFile = this.app.workspace.getActiveFile();
- if (activeFile && activeFile instanceof import_obsidian26.TFile && activeFile.extension === "md") {
- this.reviewController.reviewNote(activeFile.path);
- } else {
- new import_obsidian26.Notice("No active markdown file to review.");
- }
- }
- });
- this.addCommand({
- id: "spaceforge-add-current-note-to-review",
- name: "Add Current Note to Review Schedule",
- callback: async () => {
- const activeFile = this.app.workspace.getActiveFile();
- if (activeFile && activeFile instanceof import_obsidian26.TFile && activeFile.extension === "md") {
- await this.reviewScheduleService.scheduleNoteForReview(activeFile.path);
- await this.savePluginData();
- new import_obsidian26.Notice(`Added "${activeFile.path}" to review schedule.`);
- } else {
- new import_obsidian26.Notice("No active markdown file to add to review.");
- }
- }
- });
- this.addCommand({
- id: "spaceforge-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 import_obsidian26.TFolder) {
- const folder = activeFile.parent;
- await this.contextMenuHandler.addFolderToReview(folder);
- } else {
- new import_obsidian26.Notice("Could not determine the current note's folder, no active file, or parent is not a folder.");
- }
- }
- });
- }
- addIcons() {
- }
- initializeMCQComponents() {
- this.mcqGenerationService = void 0;
- this.mcqController = void 0;
- if (this.settings.enableMCQ) {
- this.mcqGenerationService = this.createMcqGenerationService();
- if (this.mcqGenerationService) {
- this.mcqController = new MCQController(this, this.mcqService, this.mcqGenerationService);
- } else {
- new import_obsidian26.Notice("MCQ Generation Service could not be initialized. Check API provider settings in Spaceforge settings.");
- }
- }
- }
- createMcqGenerationService() {
- switch (this.settings.mcqApiProvider) {
- case "openrouter" /* OpenRouter */:
- if (!this.settings.openRouterApiKey) {
- new import_obsidian26.Notice("OpenRouter API key is not set in Spaceforge settings.");
- return void 0;
- }
- return new OpenRouterService(this);
- case "openai" /* OpenAI */:
- if (!this.settings.openaiApiKey) {
- new import_obsidian26.Notice("OpenAI API key is not set in Spaceforge settings.");
- return void 0;
- }
- return new OpenAIService(this);
- case "ollama" /* Ollama */:
- if (!this.settings.ollamaApiUrl || !this.settings.ollamaModel) {
- new import_obsidian26.Notice("Ollama API URL or Model is not set in Spaceforge settings.");
- return void 0;
- }
- return new OllamaService(this);
- case "gemini" /* Gemini */:
- if (!this.settings.geminiApiKey) {
- new import_obsidian26.Notice("Gemini API key is not set in Spaceforge settings.");
- return void 0;
- }
- return new GeminiService(this);
- case "claude" /* Claude */:
- if (!this.settings.claudeApiKey || !this.settings.claudeModel) {
- new import_obsidian26.Notice("Claude API key or Model is not set in Spaceforge settings.");
- return void 0;
- }
- return new ClaudeService(this);
- case "together" /* Together */:
- if (!this.settings.togetherApiKey || !this.settings.togetherModel) {
- new import_obsidian26.Notice("Together AI API key or Model is not set in Spaceforge settings.");
- return void 0;
- }
- return new TogetherService(this);
- default:
- new import_obsidian26.Notice(`Unsupported MCQ API provider selected: ${this.settings.mcqApiProvider}`);
- return void 0;
- }
- }
- async checkForDueNotes() {
- }
- addStylesheet() {
- }
- async exportPluginData() {
- }
- async importPluginData(fileContent) {
- }
- getSidebarView() {
- const leaves = this.app.workspace.getLeavesOfType("spaceforge-review-schedule");
- if (leaves.length > 0) {
- const view = leaves[0].view;
- if (view instanceof ReviewSidebarView) {
- return view;
- }
- }
- return null;
- }
-};
diff --git a/releases/1.0.2/manifest.json b/releases/1.0.2/manifest.json
deleted file mode 100644
index 71ad997..0000000
--- a/releases/1.0.2/manifest.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "id": "spaceforge",
- "name": "Spaceforge",
- "version": "1.0.2",
- "minAppVersion": "0.15.0",
- "description": "A spaced repetition FSRS & SM-2 Algorithm powered by AI plugin for efficient knowledge review with pomodoro timer",
- "author": "dralkh",
- "authorUrl": "https://github.com/dralkh",
- "isDesktopOnly": false
-}
\ No newline at end of file
diff --git a/releases/1.0.2/styles.css b/releases/1.0.2/styles.css
deleted file mode 100644
index ecf6c44..0000000
--- a/releases/1.0.2/styles.css
+++ /dev/null
@@ -1,3226 +0,0 @@
-/* styles/_variables.css */
-:root {
- --sf-primary: var(--interactive-accent, #5e81ac);
- --sf-primary-light: color-mix(in srgb, var(--sf-primary), white 75%);
- --sf-primary-dark: color-mix(in srgb, var(--sf-primary), black 20%);
- --sf-secondary: var(--text-accent, #7ba1df);
- --sf-success: #4caf50;
- --sf-success-light: rgba(76, 175, 80, 0.15);
- --sf-warning: #ff9800;
- --sf-warning-light: rgba(255, 152, 0, 0.15);
- --sf-danger: #f44336;
- --sf-danger-light: rgba(244, 67, 54, 0.15);
- --sf-info: #2196f3;
- --sf-info-light: rgba(33, 150, 243, 0.15);
- --sf-text: var(--text-normal, #333);
- --sf-text-muted: var(--text-muted, #888);
- --sf-text-on-primary: var(--text-on-accent, white);
- --sf-bg-primary: var(--background-primary, white);
- --sf-bg-secondary: var(--background-secondary, #f5f5f5);
- --sf-bg-modifier: var(--background-modifier-hover);
- --sf-space-xs: 4px;
- --sf-space-sm: 8px;
- --sf-space-md: 16px;
- --sf-space-lg: 24px;
- --sf-space-xl: 32px;
- --sf-radius-sm: 4px;
- --sf-radius-md: 8px;
- --sf-radius-lg: 12px;
- --sf-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
- --sf-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.12);
- --sf-transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
- --sf-transition: 250ms cubic-bezier(0.4, 0, 0.2, 1);
- --sf-transition-bounce: 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
-}
-.theme-dark {
- --sf-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
- --sf-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.3);
- --sf-primary-light: color-mix(in srgb, var(--sf-primary), black 65%);
- --sf-success-light: rgba(76, 175, 80, 0.2);
- --sf-warning-light: rgba(255, 152, 0, 0.2);
- --sf-danger-light: rgba(244, 67, 54, 0.2);
- --sf-info-light: rgba(33, 150, 243, 0.2);
- --sf-bg-primary: var(--background-primary-alt);
- --sf-bg-secondary: var(--background-secondary-alt);
-}
-.theme-dark .mcq-question-text {
- color: var(--text-normal);
-}
-
-/* styles/_animations.css */
-@keyframes fadeIn {
- from {
- opacity: 0;
- transform: translateY(10px);
- }
- to {
- opacity: 1;
- transform: translateY(0);
- }
-}
-@keyframes pulse {
- 0% {
- opacity: 0.8;
- transform: scale(1);
- }
- 50% {
- opacity: 1;
- transform: scale(1.02);
- }
- 100% {
- opacity: 0.8;
- transform: scale(1);
- }
-}
-@keyframes shimmer {
- 0% {
- background-position: -100% 0;
- }
- 100% {
- background-position: 200% 0;
- }
-}
-@keyframes progressDots {
- from {
- background-position: 0 0;
- }
- to {
- background-position: 20px 0;
- }
-}
-@keyframes celebrationConfetti {
- 0% {
- opacity: 0;
- transform: translateY(0) rotate(0deg);
- }
- 10% {
- opacity: 1;
- }
- 100% {
- opacity: 0;
- transform: translateY(-100px) rotate(720deg);
- }
-}
-@keyframes correctPulse {
- 0% {
- box-shadow: 0 0 0 0 rgba(56, 161, 105, 0.3);
- }
- 100% {
- box-shadow: 0 2px 8px rgba(56, 161, 105, 0.1);
- }
-}
-@keyframes ripple {
- 0% {
- opacity: 1;
- transform: scale(0, 0) translate(-50%, -50%);
- }
- 100% {
- opacity: 0;
- transform: scale(20, 20) translate(-50%, -50%);
- }
-}
-@keyframes keyPress {
- 0% {
- transform: scale(0.98);
- }
- 50% {
- transform: scale(1.02);
- }
- 100% {
- transform: scale(1);
- }
-}
-.sf-animate-fade-in {
- animation: fadeIn 0.5s ease-out forwards;
-}
-.sf-animate-pulse {
- animation: pulse 2s infinite;
-}
-
-/* styles/_buttons.css */
-.sf-btn {
- padding: 8px 16px;
- border-radius: var(--sf-radius-md);
- font-weight: 500;
- cursor: pointer;
- transition: var(--sf-transition);
- border: none;
- font-size: 14px;
- line-height: 1.4;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
- min-height: 36px;
-}
-.sf-btn:hover {
- transform: translateY(-1px);
- filter: brightness(1.1);
- box-shadow: var(--sf-shadow);
-}
-.sf-btn:active {
- transform: translateY(0);
-}
-.sf-btn-primary {
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
-}
-.sf-btn-success {
- background-color: var(--sf-success);
- color: white;
-}
-.sf-btn-warning {
- background-color: var(--sf-warning);
- color: white;
-}
-.sf-btn-danger {
- background-color: var(--sf-danger);
- color: white;
-}
-.sf-btn-ghost {
- background-color: transparent;
- color: var(--sf-text);
- border: 1px solid var(--background-modifier-border);
-}
-.sf-btn-ghost:hover {
- background-color: var(--sf-bg-modifier);
-}
-.sf-btn-block {
- display: flex;
- width: 100%;
-}
-.sf-icon-btn {
- width: 36px;
- height: 36px;
- padding: 0;
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- background-color: transparent;
- color: var(--sf-text-muted);
- cursor: pointer;
- transition: var(--sf-transition);
-}
-.sf-icon-btn:hover {
- color: var(--sf-primary);
- background-color: var(--sf-primary-light);
-}
-
-/* styles/_components.css */
-.sf-card {
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- padding: var(--sf-space-md);
- box-shadow: var(--sf-shadow);
- transition: var(--sf-transition);
-}
-.sf-card:hover {
- box-shadow: var(--sf-shadow-lg);
-}
-.sf-card-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--sf-space-md);
- padding-bottom: var(--sf-space-sm);
- border-bottom: 1px solid var(--background-modifier-border);
-}
-.sf-card-header h3 {
- margin: 0;
- font-size: 16px;
- font-weight: 600;
-}
-.sf-badge {
- display: inline-block;
- padding: 2px 8px;
- border-radius: 100px;
- font-size: 12px;
- font-weight: 500;
- line-height: 1.5;
- text-align: center;
- white-space: nowrap;
- vertical-align: baseline;
-}
-.sf-badge-primary {
- background-color: var(--sf-primary-light);
- color: var(--sf-primary);
-}
-.sf-badge-success {
- background-color: var(--sf-success-light);
- color: var(--sf-success);
-}
-.sf-badge-warning {
- background-color: var(--sf-warning-light);
- color: var(--sf-warning);
-}
-.sf-badge-danger {
- background-color: var(--sf-danger-light);
- color: var(--sf-danger);
-}
-.sf-progress-container {
- width: 100%;
- height: 8px;
- background-color: var(--background-modifier-border);
- border-radius: 4px;
- overflow: hidden;
- margin: var(--sf-space-md) 0;
-}
-.sf-progress-bar {
- height: 100%;
- background-color: var(--sf-primary);
- transition: width 0.5s ease;
- position: relative;
- overflow: hidden;
-}
-.sf-progress-bar::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.15) 50%, transparent 100%);
- background-size: 200% 100%;
- animation: shimmer 2s infinite linear;
-}
-.sf-progress-bar-success {
- background-color: var(--sf-success);
-}
-.sf-progress-bar-warning {
- background-color: var(--sf-warning);
-}
-.sf-progress-bar-danger {
- background-color: var(--sf-danger);
-}
-.review-time-short {
- color: #38a169;
- font-weight: 500;
- background-color: rgba(56, 161, 105, 0.1);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.9em;
-}
-.review-time-medium {
- color: #dd6b20;
- font-weight: 500;
- background-color: rgba(221, 107, 32, 0.1);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.9em;
-}
-.review-time-long {
- color: #e53e3e;
- font-weight: 500;
- background-color: rgba(229, 62, 62, 0.1);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.9em;
-}
-.review-phase-initial {
- color: var(--text-accent);
- font-weight: 500;
- background-color: var(--background-modifier-success-hover);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.9em;
-}
-.review-phase-spaced {
- color: var(--text-success, #50fa7b);
- font-weight: 500;
- background-color: var(--background-modifier-success-hover);
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 0.9em;
-}
-.phase-time {
- font-size: 10px;
- color: var(--sf-text-muted);
- margin-top: 2px;
-}
-
-/* styles/review-buttons.css */
-.review-buttons-container {
- display: flex;
- flex-direction: column;
- gap: var(--sf-space-sm);
- margin: var(--sf-space-md) 0;
-}
-.review-button-complete-blackout,
-.review-button-incorrect,
-.review-button-incorrect-familiar,
-.review-button-correct-difficulty,
-.review-button-correct-hesitation,
-.review-button-perfect-recall,
-.review-button-hard,
-.review-button-fair,
-.review-button-good,
-.review-button-perfect,
-.review-button-postpone,
-.review-button-skip,
-.review-button-mcq {
- padding: 12px 16px;
- font-size: 14px;
- font-weight: 500;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- transition: var(--sf-transition);
- border: none;
- text-align: center;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 10px;
-}
-.review-button-complete-blackout {
- background-color: #e53e3e;
- color: white;
- box-shadow: 0 1px 3px rgba(229, 62, 62, 0.2);
-}
-.review-button-incorrect {
- background-color: #e53e3e;
- color: white;
- box-shadow: 0 1px 3px rgba(229, 62, 62, 0.2);
-}
-.review-button-incorrect-familiar {
- background-color: #feb2b2;
- color: #c53030;
- box-shadow: 0 1px 3px rgba(229, 62, 62, 0.1);
-}
-.review-button-correct-difficulty {
- background-color: #fbd38d;
- color: #c05621;
- box-shadow: 0 1px 3px rgba(192, 86, 33, 0.1);
-}
-.review-button-correct-hesitation {
- background-color: #c6f6d5;
- color: #2f855a;
- box-shadow: 0 1px 3px rgba(47, 133, 90, 0.1);
-}
-.review-button-perfect-recall {
- background-color: #38a169;
- color: white;
- box-shadow: 0 1px 3px rgba(56, 161, 105, 0.2);
-}
-.review-button-hard {
- background-color: #e53e3e;
- color: white;
- box-shadow: 0 1px 3px rgba(229, 62, 62, 0.2);
-}
-.review-button-fair {
- background-color: #fbd38d;
- color: #c05621;
- box-shadow: 0 1px 3px rgba(192, 86, 33, 0.1);
-}
-.review-button-good {
- background-color: #c6f6d5;
- color: #2f855a;
- box-shadow: 0 1px 3px rgba(47, 133, 90, 0.1);
-}
-.review-button-perfect {
- background-color: #38a169;
- color: white;
- box-shadow: 0 1px 3px rgba(56, 161, 105, 0.2);
-}
-.review-button-postpone {
- background-color: #ed8936;
- color: white;
- box-shadow: 0 1px 3px rgba(237, 137, 54, 0.2);
-}
-.review-button-skip {
- background-color: #f7fafc;
- color: #4a5568;
- border: 1px solid #e2e8f0;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
-}
-.review-button-mcq {
- background-color: #4299e1;
- color: white;
- box-shadow: 0 1px 3px rgba(66, 153, 225, 0.2);
-}
-.review-button-mcq-refresh {
- background-color: #667eea;
- color: white;
- box-shadow: 0 1px 3px rgba(102, 126, 234, 0.2);
-}
-.review-button-advance,
-.review-note-advance,
-.review-bulk-advance,
-.review-date-advance-all {
- background-color: #38B2AC;
- color: white;
- box-shadow: 0 1px 3px rgba(56, 178, 172, 0.2);
-}
-.review-button-complete-blackout:hover,
-.review-button-incorrect:hover,
-.review-button-incorrect-familiar:hover,
-.review-button-correct-difficulty:hover,
-.review-button-correct-hesitation:hover,
-.review-button-perfect-recall:hover,
-.review-button-hard:hover,
-.review-button-fair:hover,
-.review-button-good:hover,
-.review-button-perfect:hover,
-.review-button-postpone:hover,
-.review-button-skip:hover,
-.review-button-mcq:hover,
-.review-button-mcq-refresh:hover,
-.review-button-advance:hover,
-.review-note-advance:hover,
-.review-bulk-advance:hover,
-.review-date-advance-all:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow);
- filter: brightness(1.1);
-}
-.review-note-button:disabled,
-.review-bulk-button:disabled,
-.review-date-action-button:disabled,
-button.disabled {
- opacity: 0.5;
- cursor: not-allowed !important;
- filter: grayscale(60%);
- box-shadow: none;
-}
-.review-note-button:disabled:hover,
-.review-bulk-button:disabled:hover,
-.review-date-action-button:disabled:hover,
-button.disabled:hover {
- transform: none;
- filter: grayscale(60%);
- box-shadow: none;
-}
-.review-button-complete-blackout:active,
-.review-button-incorrect:active,
-.review-button-incorrect-familiar:active,
-.review-button-correct-difficulty:active,
-.review-button-correct-hesitation:active,
-.review-button-perfect-recall:active,
-.review-button-hard:active,
-.review-button-fair:active,
-.review-button-good:active,
-.review-button-perfect:active,
-.review-button-postpone:active,
-.review-button-skip:active,
-.review-button-mcq:active,
-.review-button-mcq-refresh:active,
-.review-button-advance:active,
-.review-note-advance:active,
-.review-bulk-advance:active,
-.review-date-advance-all:active {
- transform: translateY(0);
-}
-.review-button-separator {
- border-top: 1px solid var(--background-modifier-border);
- margin: 10px 0;
-}
-.review-info-text {
- margin-top: var(--sf-space-md);
- padding: var(--sf-space-md);
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- border-left: 4px solid var(--sf-primary);
- line-height: 1.5;
-}
-.review-session-info {
- font-weight: bold;
- color: var(--sf-primary);
-}
-.review-nav-buttons {
- display: flex;
- gap: var(--sf-space-sm);
- margin-bottom: var(--sf-space-sm);
- width: 100%;
-}
-.review-nav-buttons button {
- flex: 1;
- padding: var(--sf-space-sm) var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- background-color: var(--background-secondary);
- color: var(--text-normal);
- border: 1px solid var(--background-modifier-border);
- transition: var(--sf-transition);
-}
-.review-skip-next-day-button {
- background-color: var(--warning-color, #e78a4e);
- color: white;
- padding: var(--sf-space-sm) var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- border: none;
- margin-top: var(--sf-space-sm);
- transition: var(--sf-transition);
- width: 100%;
-}
-.review-skip-next-day-button:hover {
- filter: brightness(1.1);
- transform: translateY(-1px);
-}
-.review-nav-buttons button:hover {
- background-color: var(--sf-primary-light);
- border-color: var(--sf-primary);
- color: var(--sf-primary);
-}
-
-/* styles/settings.css */
-.sf-settings-section {
- margin-bottom: var(--sf-space-lg);
- animation: fadeIn 0.3s ease-out;
-}
-.sf-settings-section-header {
- display: flex;
- align-items: center;
- padding: var(--sf-space-sm) var(--sf-space-md);
- margin-top: var(--sf-space-lg);
- margin-bottom: var(--sf-space-sm);
- border-bottom: 1px solid var(--background-modifier-border);
- cursor: pointer;
- border-radius: var(--sf-radius-sm);
- transition: var(--sf-transition);
-}
-.sf-settings-section-header:hover {
- background-color: var(--sf-bg-secondary);
-}
-.sf-settings-icon {
- margin-right: var(--sf-space-md);
- display: flex;
- align-items: center;
- justify-content: center;
- width: 24px;
- height: 24px;
-}
-.sf-settings-section-header h3 {
- margin: 0;
- flex-grow: 1;
- font-size: 18px;
-}
-.sf-settings-collapse-indicator {
- margin-left: var(--sf-space-sm);
- font-size: 12px;
- transition: var(--sf-transition);
-}
-.sf-settings-section-content {
- padding-left: var(--sf-space-md);
- margin-bottom: var(--sf-space-lg);
- transition: max-height 0.3s ease, opacity 0.3s ease;
-}
-.sf-settings-subsection {
- margin: var(--sf-space-lg) 0 var(--sf-space-md) 0;
- font-weight: 600;
- font-size: 16px;
- color: var(--sf-text);
- border-bottom: 1px solid var(--background-modifier-border);
- padding-bottom: 6px;
-}
-.sf-setting-explain {
- font-size: 12px;
- color: var(--sf-text-muted);
- margin-top: -8px;
- margin-bottom: var(--sf-space-md);
- margin-left: 26px;
- font-style: italic;
-}
-.sf-setting-highlight {
- background-color: var(--sf-bg-secondary);
- border-left: 3px solid var(--sf-primary);
- padding: var(--sf-space-md);
- margin: var(--sf-space-md) 0;
- border-radius: var(--sf-radius-sm);
-}
-.sf-setting-grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
- gap: var(--sf-space-md);
- margin: var(--sf-space-md) 0;
-}
-.sf-settings-actions {
- display: flex;
- flex-wrap: wrap;
- gap: var(--sf-space-md);
- margin: var(--sf-space-md) 0 var(--sf-space-lg) 0;
-}
-.sf-settings-actions button {
- flex: 1;
- min-width: 120px;
- padding: var(--sf-space-sm) var(--sf-space-md);
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
- border: none;
- border-radius: var(--sf-radius-sm);
- cursor: pointer;
- font-weight: 500;
- transition: var(--sf-transition);
-}
-.sf-settings-actions button:hover {
- opacity: 0.9;
- transform: translateY(-1px);
-}
-.sf-info-box {
- background-color: var(--sf-bg-secondary);
- padding: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- margin: var(--sf-space-md) 0;
- box-shadow: var(--sf-shadow);
-}
-.sf-info-box h4 {
- margin-top: 0;
- margin-bottom: var(--sf-space-sm);
- color: var(--sf-primary);
-}
-.sf-prompt-label {
- font-weight: 500;
- margin: var(--sf-space-md) 0 var(--sf-space-sm) 0;
-}
-.sf-system-prompts-container {
- margin-top: var(--sf-space-lg);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--sf-radius-md);
- padding: 0 var(--sf-space-md) var(--sf-space-md) var(--sf-space-md);
-}
-.sf-system-prompts-container summary {
- cursor: pointer;
- padding: var(--sf-space-md) 0;
- margin-bottom: var(--sf-space-md);
- font-weight: 500;
-}
-.sf-danger-zone {
- background-color: var(--sf-danger-light);
- padding: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- border: 1px solid var(--sf-danger);
- margin-top: var(--sf-space-lg);
-}
-.sf-danger-zone h4 {
- color: var(--sf-danger);
- margin-top: 0;
-}
-.setting-item {
- padding: var(--sf-space-sm) 0;
- border-top: none !important;
-}
-.setting-item-info {
- font-size: 14px;
-}
-.setting-item-control {
- justify-content: flex-end;
-}
-.prompt-textarea {
- width: 100%;
- font-family: monospace;
- font-size: 13px;
- min-height: 100px;
- background-color: var(--sf-bg-primary);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--sf-radius-sm);
- padding: var(--sf-space-sm);
- resize: vertical;
- transition: var(--sf-transition);
-}
-.prompt-textarea:focus {
- border-color: var(--sf-primary);
- box-shadow: 0 0 0 2px var(--sf-primary-light);
- outline: none;
-}
-
-/* styles/sidebar.css */
-.spaceforge-container {
- padding: var(--sf-space-md);
- overflow-y: auto;
- height: 100%;
- background: var(--background-primary);
- color: var(--text-normal);
- box-shadow: var(--sf-shadow-md);
-}
-.review-upcoming-section {
- margin-top: 15px;
- padding-top: 10px;
- border-top: 1px solid var(--background-modifier-border);
-}
-.review-upcoming-list {
- display: flex;
- flex-direction: column;
- gap: 5px;
-}
-.review-upcoming-day {
- padding: 5px 8px;
- border-radius: var(--radius-s);
- transition: background-color 0.2s ease;
-}
-.review-upcoming-day.clickable {
- cursor: pointer;
-}
-.review-upcoming-day.clickable:hover {
- background-color: var(--background-modifier-hover);
-}
-.review-upcoming-day-summary {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-.review-upcoming-day-name {
- font-weight: 500;
- color: var(--text-normal);
-}
-.review-upcoming-day-count {
- font-size: 0.9em;
- color: var(--text-muted);
-}
-.review-upcoming-day.is-expanded {
- background-color: var(--background-secondary);
- margin-bottom: 5px;
-}
-.review-upcoming-notes-container {
- margin-top: 8px;
- padding-left: 10px;
- border-left: 2px solid var(--background-modifier-border-hover);
- display: flex;
- flex-direction: column;
- gap: 5px;
-}
-.review-upcoming-notes-container .review-note-item {
- padding: 5px 0;
-}
-.review-note-drag-handle.is-disabled {
- opacity: 0.3;
- cursor: default;
-}
-.review-note-drag-handle.is-disabled .drag-handle-line {
- background-color: var(--text-faint);
-}
-.review-header {
- display: flex;
- flex-direction: column;
- margin-bottom: var(--sf-space-md);
- border-bottom: 1px solid var(--background-modifier-border);
- padding-bottom: var(--sf-space-sm);
-}
-.review-header h2 {
- margin: 0 0 var(--sf-space-sm) 0;
- font-size: 20px;
- color: var(--text-normal);
- font-weight: 600;
- letter-spacing: -0.01em;
-}
-.review-stats {
- margin-top: var(--sf-space-xs);
- font-size: 14px;
- color: var(--text-muted);
- display: flex;
- gap: var(--sf-space-md);
- flex-wrap: wrap;
- align-items: center;
- justify-content: center;
-}
-.review-stats-count {
- font-weight: 500;
- display: flex;
- align-items: center;
- gap: 5px;
- text-align: center;
-}
-.review-stats-time {
- font-style: italic;
- display: flex;
- align-items: center;
- gap: 5px;
-}
-.review-stats-time::before {
- content: "";
- display: inline-block;
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background-color: var(--text-accent);
-}
-.review-stats-custom-order {
- color: var(--interactive-accent) !important;
- font-style: italic;
- font-size: 0.85em;
- display: flex;
- align-items: center;
- gap: 5px;
-}
-.review-stats-custom-order::before {
- content: "\2b50";
- font-style: normal;
-}
-.review-buttons-container {
- display: flex;
- flex-direction: column;
- gap: 4px;
- margin-bottom: var(--sf-space-md);
-}
-.review-buttons-container > * {
- margin: 0 !important;
-}
-.review-all-button,
-.review-all-mcq-button {
- width: 100%;
- padding: var(--sf-space-sm) var(--sf-space-md);
- margin: 0 !important;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- background-color: var(--interactive-accent);
- color: var(--text-on-accent);
- border: none;
- transition: var(--sf-transition);
- box-shadow: var(--sf-shadow-sm);
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 8px;
-}
-.review-all-mcq-button {
- background-color: var(--text-accent);
-}
-.review-all-button:hover,
-.review-all-mcq-button:hover {
- transform: translateY(-1px);
- box-shadow: 0 2px 5px rgba(66, 153, 225, 0.4);
- filter: brightness(1.05);
-}
-.review-all-button:active,
-.review-all-mcq-button:active {
- transform: translateY(0);
-}
-.review-view-toggle {
- display: flex;
- margin-top: var(--sf-space-sm);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--sf-radius-md);
- overflow: hidden;
-}
-.review-view-btn {
- flex: 1;
- text-align: center;
- padding: 6px;
- cursor: pointer;
- color: var(--text-normal);
- background-color: var(--background-secondary);
- transition: var(--sf-transition);
- font-weight: 500;
-}
-.review-view-btn.active {
- background-color: var(--interactive-accent);
- color: var(--text-on-accent);
-}
-.review-bulk-actions {
- display: flex;
- gap: var(--sf-space-sm);
- margin-bottom: var(--sf-space-md);
- flex-wrap: wrap;
-}
-.review-bulk-button {
- flex: 1;
- padding: var(--sf-space-sm) var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- border: none;
- transition: var(--sf-transition);
- box-shadow: var(--sf-shadow-sm);
- background-color: var(--background-modifier-border);
- color: var(--text-normal);
-}
-.review-bulk-button:hover {
- transform: translateY(-1px);
- filter: brightness(1.1);
-}
-.review-bulk-button:active {
- transform: translateY(0);
-}
-.review-date-section {
- margin-bottom: var(--sf-space-lg);
- border-radius: var(--radius-m, 6px);
- overflow: hidden;
- background-color: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
-}
-.review-date-section-overdue {
- border-left: 3px solid var(--text-error);
- background-color: var(--background-secondary);
-}
-.review-date-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--sf-space-sm) var(--sf-space-md);
- background-color: var(--background-secondary-alt);
- border-bottom: 1px solid var(--background-modifier-border);
-}
-.review-date-header-container {
- display: flex;
- align-items: center;
- gap: var(--sf-space-sm);
- flex-grow: 1;
- flex-shrink: 1;
- overflow: hidden;
- margin-right: var(--sf-space-sm);
-}
-.review-date-header h3 {
- margin: 0;
- font-size: 16px;
- color: var(--text-normal);
- font-weight: 600;
- white-space: normal;
- overflow-wrap: break-word;
- flex-grow: 1;
- min-width: 0;
-}
-.review-date-section-overdue .review-date-header h3 {
- color: var(--text-error);
-}
-.review-date-postpone-all {
- padding: 4px 10px;
- font-size: 12px;
- border-radius: 6px;
- flex-shrink: 0;
- cursor: pointer;
- background-color: #edf2f7;
- color: #4a5568;
- border: none;
- transition: var(--sf-transition);
-}
-.review-date-postpone-all:hover {
- background-color: var(--sf-warning);
- color: white;
-}
-.review-date-time {
- font-size: 12px;
- color: var(--text-muted);
- font-style: italic;
- padding: 3px 8px;
- background-color: var(--background-primary);
- border-radius: 100px;
- border: 1px solid var(--background-modifier-border);
-}
-.review-notes-container {
- padding: var(--sf-space-sm);
- background-color: var(--sf-bg-primary);
-}
-.review-note-item {
- display: flex;
- align-items: flex-start;
- padding: var(--sf-space-sm);
- margin-bottom: var(--sf-space-sm);
- border-radius: var(--radius-s, 4px);
- background-color: var(--background-secondary);
- justify-content: space-between;
- flex-wrap: wrap;
- transition: var(--sf-transition);
- border: 1px solid transparent;
-}
-.review-note-item:last-child {
- margin-bottom: 0;
-}
-.review-note-item:hover {
- border-color: var(--interactive-hover);
- background-color: var(--background-modifier-hover);
-}
-.review-note-item.selected {
- background-color: var(--background-modifier-form-focus);
- border-color: var(--interactive-accent);
-}
-.review-note-item.overdue-note {
- background-color: var(--background-modifier-error-hover);
- border-left: 3px solid var(--text-error);
-}
-.review-note-title {
- font-weight: 500;
- flex-grow: 1;
- margin-right: 10px;
- min-width: 150px;
- overflow: visible;
- text-overflow: clip;
- white-space: normal;
- position: relative;
-}
-.review-note-info {
- display: flex;
- align-items: center;
- gap: var(--sf-space-sm);
- margin-right: 10px;
-}
-.review-note-phase {
- font-size: 12px;
- padding: 2px 6px;
- border-radius: 3px;
- display: inline-block;
- background-color: var(--sf-bg-primary);
- border: 1px solid var(--background-modifier-border);
-}
-.review-note-time {
- font-size: 12px;
- color: var(--sf-text-muted);
-}
-.review-note-buttons {
- display: flex;
- align-items: center;
- gap: 5px;
- flex-shrink: 0;
-}
-.review-note-actions {
- display: flex;
- gap: 5px;
- flex-shrink: 0;
-}
-.review-note-button,
-.review-note-postpone,
-.review-note-remove {
- padding: 4px 10px;
- font-size: 12px;
- border-radius: var(--radius-s, 4px);
- cursor: pointer;
- border: 1px solid var(--background-modifier-border);
- transition: var(--sf-transition);
- background-color: var(--background-primary);
-}
-@media (max-width: 400px) {
- .review-note-actions {
- flex-direction: column;
- width: 100%;
- }
- .review-note-button,
- .review-note-postpone,
- .review-note-remove {
- width: 100%;
- margin-bottom: 5px;
- }
- .review-note-remove {
- order: -1;
- }
-}
-.review-note-button {
- background-color: var(--interactive-accent);
- color: var(--text-on-accent);
- border-color: var(--interactive-accent);
- padding: 6px 12px;
- font-size: 13px;
-}
-.review-note-postpone {
- background-color: var(--background-modifier-border);
- color: var(--text-normal);
- padding: 6px 12px;
- font-size: 13px;
-}
-.review-note-remove {
- background-color: var(--text-error);
- color: white;
- border-color: var(--text-error);
- padding: 6px 12px;
- font-size: 13px;
-}
-.review-note-button:hover,
-.review-note-postpone:hover,
-.review-note-remove:hover {
- transform: translateY(-1px);
- filter: brightness(1.1);
-}
-.review-note-drag-handle {
- display: flex;
- flex-direction: column;
- gap: 2px;
- cursor: grab;
- padding: 6px 8px;
- margin-left: 5px;
- flex-shrink: 0;
- border-radius: var(--sf-radius-sm);
- transition: var(--sf-transition);
-}
-.review-note-drag-handle:hover {
- background-color: var(--sf-bg-modifier);
-}
-.drag-handle-line {
- width: 15px;
- height: 2px;
- background-color: var(--sf-text-muted);
- border-radius: 1px;
- transition: var(--sf-transition);
-}
-.review-note-drag-handle:hover .drag-handle-line {
- background-color: var(--sf-primary);
-}
-.review-note-drag-handle:active {
- cursor: grabbing;
-}
-.review-note-item.dragging {
- opacity: 0.5;
- box-shadow: var(--sf-shadow-lg);
-}
-.review-note-item.drag-over {
- background-color: var(--sf-primary-light);
- border: 1px dashed var(--sf-primary);
-}
-.review-all-caught-up {
- padding: var(--sf-space-md);
- text-align: center;
- font-weight: 500;
- color: var(--sf-text-muted);
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- box-shadow: var(--sf-shadow-md);
- border: 1px solid var(--background-modifier-border);
- position: relative;
- overflow: hidden;
-}
-.review-all-caught-up::before {
- content: "\1f389";
- font-size: 24px;
- display: block;
- margin-bottom: var(--sf-space-sm);
-}
-.review-all-caught-up::after {
- content: "";
- position: absolute;
- top: 0;
- left: -100%;
- width: 300%;
- height: 100%;
- background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.1) 50%, transparent 100%);
- animation: shimmer 2s infinite;
-}
-.review-session-section {
- margin-bottom: var(--sf-space-lg);
- padding: var(--sf-space-md);
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- box-shadow: var(--sf-shadow-md);
- border-left: 4px solid var(--sf-primary);
-}
-.review-session-info {
- margin-bottom: var(--sf-space-md);
-}
-.review-session-name {
- font-weight: 500;
- margin-bottom: 5px;
- font-size: 16px;
- color: var(--sf-primary);
-}
-.review-session-progress {
- font-size: 12px;
- color: var(--sf-text-muted);
- margin-bottom: 5px;
-}
-.review-session-progress-bar-container {
- height: 5px;
- background-color: var(--background-modifier-border);
- border-radius: 3px;
- overflow: hidden;
- margin-bottom: 10px;
- position: relative;
-}
-.review-session-progress-bar {
- height: 100%;
- background-color: var(--sf-primary);
- position: relative;
- overflow: hidden;
-}
-.review-session-progress-bar::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.15) 50%, transparent 100%);
- animation: shimmer 2s infinite;
-}
-.review-session-continue,
-.review-session-end {
- width: 100%;
- padding: 8px 12px;
- margin-bottom: 5px;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- border: none;
- transition: var(--sf-transition);
-}
-.review-session-continue {
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
-}
-.review-session-end {
- background-color: var(--background-modifier-border);
- color: var(--sf-text);
-}
-.review-session-continue:hover,
-.review-session-end:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow-lg);
-}
-.review-upcoming-section {
- margin-top: var(--sf-space-lg);
-}
-.review-upcoming-section h3 {
- font-size: 16px;
- margin-bottom: var(--sf-space-sm);
- color: var(--sf-text);
- font-weight: 600;
-}
-.review-upcoming-list {
- margin-top: var(--sf-space-xs);
- display: flex;
- flex-direction: column;
- gap: var(--sf-space-xs);
-}
-.review-upcoming-day {
- padding: var(--sf-space-xs) var(--sf-space-sm);
- border-radius: var(--sf-radius-sm);
- background-color: var(--sf-bg-secondary);
- transition: var(--sf-transition);
- border: 1px solid transparent;
- display: flex;
- justify-content: space-between;
-}
-.review-upcoming-day:hover {
- border-color: var(--interactive-accent);
- transform: translateX(3px);
-}
-.review-upcoming-day-name {
- font-weight: 500;
-}
-.review-upcoming-day-count {
- color: var(--sf-text-muted);
- font-size: 12px;
- background-color: var(--sf-bg-primary);
- padding: 1px 6px;
- border-radius: 100px;
-}
-.review-buttons-container .pomodoro-toggle-container {
- width: 100%;
- margin: 0 !important;
-}
-.review-buttons-container .pomodoro-visibility-toggle-btn {
- width: 100%;
- padding: calc(var(--sf-space-xs) / 1.5) var(--sf-space-sm);
- margin: 0 !important;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- background-color: var(--background-modifier-border);
- color: var(--text-normal);
- border: 1px solid var(--background-modifier-border);
- transition: var(--sf-transition);
-}
-.review-buttons-container .pomodoro-visibility-toggle-btn:hover {
- background-color: var(--background-modifier-hover);
-}
-.review-buttons-container .pomodoro-section-content {
- width: 100%;
- margin: 0 !important;
-}
-.pomodoro-container {
- padding: 2px var(--sf-space-xs);
- margin: 0 !important;
- background-color: var(--background-secondary);
- border-radius: var(--radius-m);
- border: 1px solid var(--background-modifier-border);
- display: flex;
- flex-direction: column;
- gap: var(--sf-space-xs);
-}
-.pomodoro-main-controls {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 4px;
-}
-.pomodoro-timer-display {
- font-size: 22px;
- font-weight: 600;
- font-family: monospace;
- color: var(--text-normal);
- padding: calc(var(--sf-space-xs) / 2) 8px;
- border-radius: var(--radius-s);
- background-color: var(--background-primary);
- min-width: 65px;
- text-align: center;
- order: 2;
- flex-grow: 1;
- border: none;
- transition: border-color 0.3s ease;
-}
-.pomodoro-timer-fade {
- transition: opacity 0.3s ease-in-out, visibility 0.3s ease-in-out;
-}
-.pomodoro-timer-fade.timer-visible {
- opacity: 1;
- visibility: visible;
-}
-.pomodoro-timer-display {
- border-left: 3px solid transparent;
- border-right: 3px solid transparent;
- background-color: var(--background-primary);
- color: var(--text-normal);
-}
-.pomodoro-timer-display.mode-work {
- border-left-color: var(--interactive-accent);
- border-right-color: var(--interactive-accent);
-}
-.pomodoro-timer-display.mode-shortBreak {
- border-left-color: var(--color-green);
- border-right-color: var(--color-green);
-}
-.pomodoro-timer-display.mode-longBreak {
- border-left-color: var(--color-blue);
- border-right-color: var(--color-blue);
-}
-.pomodoro-timer-display.mode-idle {
- border-left-color: var(--text-muted);
- border-right-color: var(--text-muted);
- color: var(--text-muted);
-}
-.pomodoro-quick-settings-toggle {
- cursor: pointer;
- padding: 5px;
- order: 4;
- border-radius: var(--radius-s);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-muted);
-}
-.pomodoro-quick-settings-toggle:hover {
- background-color: var(--background-modifier-hover);
- color: var(--text-normal);
-}
-.pomodoro-quick-settings-toggle svg {
- width: 18px;
- height: 18px;
-}
-.pomodoro-main-controls .pomodoro-start-btn,
-.pomodoro-main-controls .pomodoro-stop-btn,
-.pomodoro-main-controls .pomodoro-skip-btn {
- flex-grow: 0;
- flex-shrink: 0;
- padding: 4px 8px;
- border-radius: var(--radius-s);
- border: 1px solid var(--background-modifier-border);
- background-color: var(--background-secondary-alt);
- cursor: pointer;
- transition: var(--sf-transition);
- display: flex;
- align-items: center;
- justify-content: center;
- color: var(--text-normal);
-}
-.pomodoro-main-controls .pomodoro-start-btn {
- order: 1;
-}
-.pomoro-main-controls .pomodoro-stop-btn {
- order: 1;
-}
-.pomodoro-main-controls .pomodoro-skip-btn {
- order: 3;
-}
-.pomodoro-main-controls button:hover {
- background-color: var(--background-modifier-hover);
- border-color: var(--interactive-accent-hover);
-}
-.pomodoro-main-controls button svg {
- width: 16px;
- height: 16px;
-}
-.pomodoro-quick-settings-panel {
- padding: var(--sf-space-xs);
- border-top: 1px solid var(--background-modifier-border);
- margin-top: var(--sf-space-xs);
- display: flex;
- flex-direction: column;
- gap: calc(var(--sf-space-xs) / 2);
- background-color: var(--background-primary);
- border-radius: var(--radius-s);
-}
-.pomodoro-quick-setting {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: var(--sf-space-sm);
-}
-.pomodoro-quick-setting label {
- font-size: 13px;
- color: var(--text-muted);
- flex-shrink: 0;
-}
-.pomodoro-quick-setting input[type=number] {
- width: 60px;
- text-align: right;
- padding: 4px 6px;
- border-radius: var(--radius-s);
- border: 1px solid var(--background-modifier-border);
- background-color: var(--background-secondary);
-}
-.pomodoro-quick-save-btn {
- margin-top: var(--sf-space-xs);
- padding: 6px 12px;
- border-radius: var(--radius-s);
- border: none;
- background-color: var(--interactive-accent);
- color: var(--text-on-accent);
- cursor: pointer;
- transition: var(--sf-transition);
- align-self: flex-end;
-}
-.pomodoro-quick-save-btn:hover {
- background-color: var(--interactive-accent-hover);
-}
-.pomodoro-container.is-paused .pomodoro-timer-display {
- opacity: 0.7;
-}
-.pomodoro-container.is-idle .pomodoro-timer-display {
- opacity: 0.5;
-}
-
-/* styles/calendar.css */
-.calendar-container-wrapper {
- height: 100%;
- animation: fadeIn 0.5s ease-out;
-}
-.calendar-container {
- padding: var(--sf-space-md);
- width: 100%;
- background-color: var(--background-primary);
- border-radius: var(--radius-m, 8px);
- box-shadow: var(--sf-shadow-md);
-}
-.is-collapsed .calendar-container {
- padding: var(--sf-space-sm);
-}
-.is-collapsed .calendar-header {
- margin-bottom: var(--sf-space-sm);
-}
-.is-collapsed .calendar-month-title {
- font-size: 16px;
-}
-.is-collapsed .calendar-nav-btn {
- padding: 4px 8px;
- font-size: 13px;
-}
-.is-collapsed .calendar-today-btn {
- margin-left: 5px;
- padding: 4px 8px;
- font-size: 12px;
-}
-.is-collapsed .calendar-grid {
- gap: 1px;
- grid-auto-rows: minmax(40px, auto);
-}
-.is-collapsed .calendar-weekday {
- font-size: 10px;
- padding: 2px 0;
-}
-.is-collapsed .calendar-day {
- min-height: 40px;
- padding: 2px;
- font-size: 10px;
- border-radius: 3px;
-}
-.is-collapsed .calendar-day-number {
- font-size: 11px;
- margin-bottom: 1px;
-}
-.is-collapsed .calendar-day.today .calendar-day-number::after {
- width: 10px;
-}
-.is-collapsed .calendar-review-count {
- width: 16px;
- height: 16px;
- font-size: 8px;
- top: 2px;
- right: 2px;
-}
-.is-collapsed .calendar-time-estimate {
- font-size: 8px;
- padding: 0 2px;
-}
-.is-collapsed .calendar-day {
- min-height: 40px;
- padding: 4px;
- font-size: 12px;
- border-radius: 4px;
-}
-.is-collapsed .calendar-day-number {
- font-size: 13px;
- margin-bottom: 2px;
-}
-.is-collapsed .calendar-day.today .calendar-day-number::after {
- width: 12px;
-}
-.is-collapsed .calendar-review-count {
- width: 20px;
- height: 20px;
- font-size: 10px;
- top: 4px;
- right: 4px;
-}
-.is-collapsed .calendar-time-estimate {
- font-size: 10px;
- padding: 1px 3px;
-}
-@media (max-width: 768px) {
- .calendar-grid {
- gap: 2px;
- grid-template-columns: repeat(7, 1fr);
- }
- .calendar-day {
- min-height: 30px;
- padding: 3px;
- font-size: 11px;
- border-radius: 4px;
- aspect-ratio: auto;
- }
- .calendar-weekday {
- font-size: 10px;
- padding: 3px 0;
- }
- .calendar-day-number {
- font-size: 12px;
- margin-bottom: 3px;
- }
- .calendar-review-count {
- width: 18px;
- height: 18px;
- font-size: 9px;
- top: 3px;
- right: 3px;
- }
- .calendar-time-estimate {
- font-size: 9px;
- padding: 1px 3px;
- }
-}
-.calendar-header {
- display: flex;
- align-items: center;
- margin-bottom: var(--sf-space-md);
- padding-bottom: 5px;
- border-bottom: 1px solid var(--background-modifier-border);
-}
-.calendar-month-title {
- flex: 1;
- text-align: center;
- font-weight: 600;
- font-size: 18px;
- color: var(--text-normal);
-}
-.calendar-nav-btn {
- cursor: pointer;
- padding: 6px 12px;
- border-radius: var(--sf-radius-sm);
- background-color: transparent;
- transition: var(--sf-transition);
- color: var(--text-muted);
- font-size: 15px;
-}
-.calendar-nav-btn:hover {
- background-color: var(--background-modifier-hover);
- color: var(--text-normal);
-}
-.calendar-today-btn {
- margin-left: 10px;
- font-size: 14px;
- cursor: pointer;
- padding: 6px 12px;
- border-radius: 20px;
- background-color: var(--interactive-accent);
- color: var(--text-on-accent);
- border: none;
- transition: var(--sf-transition);
- font-weight: 500;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
-}
-.calendar-today-btn:hover {
- background-color: var(--interactive-accent-hover);
- transform: translateY(-1px);
-}
-.calendar-grid {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- grid-auto-rows: minmax(50px, auto);
- gap: 4px;
- margin-top: var(--sf-space-md);
-}
-.calendar-weekday {
- text-align: center;
- font-weight: 600;
- font-size: 14px;
- padding: 8px 0;
- color: var(--text-muted);
-}
-.calendar-day {
- position: relative;
- height: auto;
- min-height: 50px;
- width: 100%;
- padding: 8px;
- border: 1px solid var(--background-modifier-border);
- border-radius: 8px;
- font-size: 14px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- flex-direction: column;
- background-color: var(--background-secondary-alt);
- box-sizing: border-box;
-}
-.calendar-day:hover {
- border-color: var(--interactive-accent);
- box-shadow: var(--sf-shadow-lg);
- transform: translateY(-2px);
- z-index: 10;
-}
-.calendar-day.empty {
- background-color: var(--background-secondary);
- cursor: default;
- opacity: 0.5;
-}
-.calendar-day.empty:hover {
- transform: none;
- box-shadow: none;
- border-color: var(--background-modifier-border);
-}
-.calendar-day.today {
- background-color: rgba(var(--interactive-accent-rgb), 0.15);
- border-color: var(--interactive-accent);
- font-weight: 600;
- box-shadow: var(--sf-shadow-sm);
-}
-.calendar-day.has-reviews {
- background-color: rgba(var(--interactive-accent-rgb), 0.1);
- border-color: var(--interactive-accent);
- box-shadow: var(--sf-shadow-sm);
-}
-.calendar-day.has-reviews:hover {
- background-color: rgba(var(--interactive-accent-rgb), 0.2);
-}
-.calendar-day-number {
- font-weight: 600;
- margin-bottom: 6px;
- font-size: 15px;
-}
-.calendar-day.today .calendar-day-number {
- color: var(--interactive-accent);
- position: relative;
-}
-.calendar-day.today .calendar-day-number::after {
- content: "";
- position: absolute;
- bottom: -2px;
- left: 0;
- height: 2px;
- width: 16px;
- background-color: var(--interactive-accent);
- border-radius: 1px;
-}
-.calendar-day.light-load .calendar-review-count {
- background-color: var(--sf-success);
- animation: fadeScale 0.3s ease-out forwards;
-}
-.calendar-day.medium-load .calendar-review-count {
- background-color: var(--sf-warning);
- animation: fadeScale 0.3s ease-out forwards;
-}
-.calendar-day.heavy-load .calendar-review-count {
- background-color: var(--sf-danger);
- animation: fadeScale 0.3s ease-out forwards;
-}
-.calendar-review-count {
- position: absolute;
- top: 8px;
- right: 8px;
- width: 24px;
- height: 24px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- font-size: 12px;
- font-weight: 700;
- color: white;
- box-shadow: var(--sf-shadow-sm);
- transition: transform 0.2s ease;
-}
-.calendar-day:hover .calendar-review-count {
- transform: scale(1.1);
-}
-.calendar-time-estimate {
- margin-top: auto;
- align-self: flex-end;
- font-size: 11px;
- font-weight: 500;
- color: var(--text-muted);
- padding: 2px 5px;
- background-color: var(--background-modifier-hover);
- border-radius: 4px;
- opacity: 1;
- transition: opacity 0.2s ease;
-}
-.day-reviews-container {
- padding: var(--sf-space-md);
- animation: fadeIn 0.4s ease-out;
-}
-.day-reviews-header {
- display: flex;
- align-items: center;
- margin-bottom: var(--sf-space-md);
-}
-.day-reviews-back-btn {
- margin-right: 10px;
- cursor: pointer;
- width: 30px;
- height: 30px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%;
- transition: var(--sf-transition);
- background-color: var(--sf-bg-secondary);
- color: var(--sf-text-muted);
-}
-.day-reviews-back-btn:hover {
- background-color: var(--sf-primary-light);
- color: var(--sf-primary);
-}
-.day-reviews-header-content {
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
-}
-.day-reviews-date {
- font-weight: 600;
- color: var(--sf-primary);
- font-size: 16px;
-}
-.day-reviews-postpone-all {
- padding: 4px 10px;
- font-size: 12px;
- border-radius: var(--sf-radius-sm);
- cursor: pointer;
- background-color: var(--sf-bg-secondary);
- color: var(--sf-text);
- border: 1px solid var(--background-modifier-border);
- transition: var(--sf-transition);
-}
-.day-reviews-postpone-all:hover {
- background-color: var(--sf-warning);
- color: white;
- border-color: var(--sf-warning);
-}
-.day-reviews-stats {
- margin-bottom: var(--sf-space-md);
- font-size: 14px;
- color: var(--sf-text-muted);
- padding: var(--sf-space-sm) var(--sf-space-md);
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-.day-reviews-count {
- font-weight: 500;
- color: var(--sf-primary);
-}
-.day-reviews-time {
- font-style: italic;
-}
-.day-reviews-all-button {
- width: 100%;
- padding: var(--sf-space-sm) var(--sf-space-md);
- margin-bottom: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
- border: none;
- transition: var(--sf-transition);
- box-shadow: var(--sf-shadow);
-}
-.day-reviews-all-button:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow-lg);
- filter: brightness(1.05);
-}
-.day-reviews-notes {
- margin-top: var(--sf-space-md);
- display: flex;
- flex-direction: column;
- gap: var(--sf-space-sm);
-}
-.day-reviews-skip-next-button {
- width: 100%;
- padding: var(--sf-space-sm) var(--sf-space-md);
- margin-bottom: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 500;
- text-align: center;
- background-color: var(--warning-color, #e78a4e);
- color: white;
- border: none;
- transition: var(--sf-transition);
- box-shadow: var(--sf-shadow);
- margin-top: var(--sf-space-sm);
-}
-.day-reviews-skip-next-button:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow-lg);
- filter: brightness(1.05);
-}
-@media (max-width: 768px) {
- .calendar-grid {
- gap: 2px;
- grid-template-columns: repeat(7, 1fr);
- }
- .calendar-day {
- min-height: 30px;
- padding: 3px;
- font-size: 11px;
- border-radius: 4px;
- aspect-ratio: auto;
- }
- .calendar-weekday {
- font-size: 10px;
- padding: 3px 0;
- }
- .calendar-day-number {
- font-size: 12px;
- margin-bottom: 3px;
- }
- .calendar-review-count {
- width: 18px;
- height: 18px;
- font-size: 9px;
- top: 3px;
- right: 3px;
- }
- .calendar-time-estimate {
- font-size: 9px;
- padding: 1px 3px;
- }
-}
-
-/* styles/mcq.css */
-.mcq-header-container {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: var(--sf-space-md);
- padding-bottom: var(--sf-space-sm);
- border-bottom: 1px solid var(--background-modifier-border);
- position: relative;
- flex-shrink: 0;
-}
-.mcq-header-container h2 {
- margin: 0;
- font-size: 1.8rem;
- font-weight: 600;
- color: var(--sf-primary);
- letter-spacing: -0.02em;
-}
-.mcq-refresh-btn {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 36px;
- height: 36px;
- padding: 6px;
- color: var(--sf-text-muted);
- background-color: transparent;
- border-radius: 50%;
- cursor: pointer;
- transition: var(--sf-transition-bounce);
-}
-.mcq-refresh-btn:hover {
- color: var(--sf-primary);
- background-color: var(--sf-primary-light);
- transform: rotate(15deg);
-}
-.mcq-progress {
- position: relative;
- margin-bottom: 24px;
- font-weight: 600;
- color: var(--text-normal);
- display: flex;
- align-items: center;
- gap: 12px;
- font-size: 1em;
- height: 32px;
- flex-shrink: 0;
-}
-.mcq-progress span {
- white-space: nowrap;
-}
-.mcq-progress::after {
- content: "";
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: 6px;
- background: var(--background-modifier-border);
- border-radius: 6px;
- overflow: hidden;
-}
-.mcq-progress::before {
- content: "";
- position: absolute;
- bottom: 0;
- left: 0;
- height: 6px;
- background: var(--interactive-accent);
- border-radius: 6px;
- z-index: 2;
- width: 0%;
- transition: width 0.5s ease;
- box-shadow: 0 0 8px rgba(var(--interactive-accent-rgb), 0.4);
-}
-.mcq-progress-info {
- font-size: 14px;
- color: var(--text-muted);
- margin-top: -12px;
- margin-bottom: 16px;
- padding-left: 2px;
-}
-.mcq-question-container {
- margin-bottom: 20px;
- background-color: var(--background-primary);
- border-radius: var(--radius-m, 8px);
- padding: 20px;
- border: 1px solid var(--background-modifier-border);
- animation: fadeIn 0.5s ease-out;
- min-height: 300px;
- max-height: 900px;
- overflow-y: auto;
- width: 100%;
- box-sizing: border-box;
- display: flex;
- flex-direction: column;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- flex-grow: 1;
- overflow-y: auto;
-}
-.mcq-question-text {
- font-size: 1em;
- margin-bottom: 10px;
- font-weight: 600;
- line-height: 1.4;
- color: var(--text-normal);
- padding-bottom: 10px;
- border-bottom: 1px solid var(--background-modifier-border);
-}
-.spaceforge-mcq-modal.modal-content {
- min-width: 500px;
- max-width: 90vw;
- min-height: 300px;
- padding: 28px !important;
- display: flex;
- flex-direction: column;
-}
-[data-progress="0"] .mcq-progress::before {
- width: 0% !important;
-}
-[data-progress="5"] .mcq-progress::before {
- width: 5% !important;
-}
-[data-progress="10"] .mcq-progress::before {
- width: 10% !important;
-}
-[data-progress="15"] .mcq-progress::before {
- width: 15% !important;
-}
-[data-progress="20"] .mcq-progress::before {
- width: 20% !important;
-}
-[data-progress="25"] .mcq-progress::before {
- width: 25% !important;
-}
-[data-progress="30"] .mcq-progress::before {
- width: 30% !important;
-}
-[data-progress="35"] .mcq-progress::before {
- width: 35% !important;
-}
-[data-progress="40"] .mcq-progress::before {
- width: 40% !important;
-}
-[data-progress="45"] .mcq-progress::before {
- width: 45% !important;
-}
-[data-progress="50"] .mcq-progress::before {
- width: 50% !important;
-}
-[data-progress="55"] .mcq-progress::before {
- width: 55% !important;
-}
-[data-progress="60"] .mcq-progress::before {
- width: 60% !important;
-}
-[data-progress="65"] .mcq-progress::before {
- width: 65% !important;
-}
-[data-progress="70"] .mcq-progress::before {
- width: 70% !important;
-}
-[data-progress="75"] .mcq-progress::before {
- width: 75% !important;
-}
-[data-progress="80"] .mcq-progress::before {
- width: 80% !important;
-}
-[data-progress="85"] .mcq-progress::before {
- width: 85% !important;
-}
-[data-progress="90"] .mcq-progress::before {
- width: 90% !important;
-}
-[data-progress="95"] .mcq-progress::before {
- width: 95% !important;
-}
-[data-progress="100"] .mcq-progress::before {
- width: 100% !important;
-}
-.mcq-attempt-warning {
- margin-bottom: var(--sf-space-lg);
- padding: var(--sf-space-md) var(--sf-space-md);
- background-color: var(--sf-warning-light);
- border-left: 4px solid var(--sf-warning);
- color: var(--sf-warning);
- font-weight: 500;
- border-radius: var(--sf-radius-md);
- display: flex;
- align-items: center;
- gap: var(--sf-space-md);
- box-shadow: var(--sf-shadow);
- animation: pulse 2s infinite;
- flex-shrink: 0;
-}
-.mcq-choices-container {
- display: flex;
- flex-direction: column;
- gap: 10px;
- margin-top: 20px;
- padding-right: 8px;
- overflow-y: auto;
- max-height: 450px;
-}
-.mcq-choice {
- width: 100%;
-}
-.mcq-choice-btn {
- width: 100%;
- text-align: left;
- padding: 8px 10px;
- background-color: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: 8px;
- cursor: pointer;
- transition: all 0.2s ease;
- display: flex;
- align-items: flex-start;
- font-weight: 500;
- line-height: 1.4;
- color: var(--text-normal);
- position: relative;
- min-height: auto;
- height: auto;
- margin-bottom: 6px;
- box-sizing: border-box;
- font-size: 14px;
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-.mcq-choice-btn:hover {
- background-color: var(--background-modifier-hover);
- border-color: var(--interactive-accent);
- transform: translateX(4px);
- box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
-}
-.mcq-choice-btn:active {
- transform: translateX(2px);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
-}
-.mcq-choice-btn::before {
- content: "";
- position: absolute;
- left: 0;
- top: 0;
- height: 100%;
- width: 4px;
- background-color: transparent;
- transition: background-color 0.2s ease;
-}
-.mcq-choice-btn:hover::before {
- background-color: var(--sf-primary-light);
-}
-.mcq-choice-letter {
- font-weight: 700;
- margin-right: 8px;
- color: var(--interactive-accent);
- font-size: 0.9em;
- min-width: 25px;
- text-align: center;
- background-color: rgba(var(--interactive-accent-rgb), 0.1);
- height: 30px;
- width: 30px;
- border-radius: 15px;
- display: flex;
- align-items: center;
- justify-content: center;
- box-shadow: 0 1px 3px rgba(var(--interactive-accent-rgb), 0.2);
-}
-.mcq-choice-text {
- flex: 1;
- overflow-wrap: break-word;
- word-break: break-word;
- white-space: normal;
- padding-right: 60px;
- font-size: 15px;
- line-height: 1.5;
-}
-.mcq-shortcut-hint {
- position: absolute;
- top: 8px;
- right: 10px;
- font-size: 12px;
- color: var(--text-muted);
- background-color: var(--background-secondary-alt);
- padding: 3px 8px;
- border-radius: 12px;
- opacity: 0.7;
- transition: opacity 0.2s ease;
-}
-.mcq-choice-btn:hover .mcq-shortcut-hint {
- opacity: 1;
-}
-.spaceforge-mcq-modal.modal {
- max-width: 95vw;
- max-height: 95vh;
- display: flex;
- justify-content: center;
- align-items: center;
-}
-.mcq-choice-correct {
- background-color: var(--sf-success-light) !important;
- border-color: var(--sf-success) !important;
- color: var(--text-normal) !important;
- box-shadow: 0 2px 8px rgba(56, 161, 105, 0.1) !important;
- animation: correctPulse 0.5s ease-out;
- transform: none !important;
-}
-.mcq-choice-correct::before {
- background-color: var(--sf-success) !important;
- width: 6px;
-}
-.mcq-choice-correct .mcq-choice-letter {
- color: var(--sf-success);
-}
-.mcq-choice-incorrect {
- background-color: var(--sf-danger-light) !important;
- border-color: var(--sf-danger) !important;
- color: var(--text-normal) !important;
- animation: none;
- box-shadow: 0 2px 8px rgba(229, 62, 62, 0.1) !important;
-}
-.mcq-choice-incorrect::before {
- background-color: var(--sf-danger) !important;
- width: 6px;
-}
-.mcq-choice-incorrect .mcq-choice-letter {
- color: var(--sf-danger);
-}
-.mcq-skip-container {
- margin-bottom: var(--sf-space-md);
- text-align: right;
-}
-.mcq-skip-button,
-.mcq-continue-button,
-.mcq-postpone-button {
- padding: var(--sf-space-sm) var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- font-weight: 500;
- transition: background-color 0.2s ease, color 0.2s ease;
- cursor: pointer;
- border: none;
- background-color: var(--background-modifier-border);
- color: var(--text-normal);
- min-height: 40px;
-}
-.mcq-postpone-button {
- background-color: var(--sf-warning);
- color: white;
- margin-right: var(--sf-space-sm);
-}
-.mcq-skip-button:hover,
-.mcq-continue-button:hover {
- filter: brightness(1.1);
-}
-.mcq-continue-button {
- margin-top: var(--sf-space-md);
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
- width: 100%;
- padding: var(--sf-space-md);
- font-size: 1.05em;
- min-height: 48px;
-}
-.mcq-correct-answer-display {
- background-color: var(--background-secondary);
- padding: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- margin-bottom: var(--sf-space-md);
- border: 1px solid var(--background-modifier-border);
- animation: fadeIn 0.5s ease-out;
- color: var(--text-normal);
- box-shadow: var(--sf-shadow);
-}
-.mcq-score {
- margin: var(--sf-space-lg) 0;
- text-align: center;
- padding: var(--sf-space-lg);
- background-color: var(--background-modifier-hover);
- border-radius: var(--sf-radius-md);
- box-shadow: var(--sf-shadow);
- position: relative;
-}
-.mcq-score-text {
- font-size: 2em;
- font-weight: 600;
- color: var(--text-normal);
- margin-bottom: var(--sf-space-sm);
- text-shadow: 0 0 8px rgba(var(--text-normal-rgb), 0.3);
-}
-.mcq-score::after {
- content: "";
- position: absolute;
- top: 0;
- left: -100%;
- width: 300%;
- height: 100%;
- background: linear-gradient(90deg, transparent 0%, rgba(var(--interactive-accent-rgb), 0.1) 50%, transparent 100%);
- animation: shimmer 2s infinite;
-}
-.mcq-results {
- margin-top: var(--sf-space-lg);
-}
-.mcq-results h3 {
- margin-bottom: var(--sf-space-md);
- padding-bottom: var(--sf-space-md);
- border-bottom: 1px solid var(--background-modifier-border);
- font-size: 1.4em;
- color: var(--text-normal);
- font-weight: 600;
-}
-.mcq-result-item {
- margin-bottom: var(--sf-space-lg);
- padding: var(--sf-space-md);
- background-color: var(--background-secondary);
- border-radius: var(--sf-radius-md);
- box-shadow: var(--sf-shadow);
- border-left: 4px solid var(--background-modifier-border);
- transition: var(--sf-transition);
-}
-.mcq-result-item:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow-lg);
-}
-.mcq-result-question {
- font-weight: 600;
- margin-bottom: var(--sf-space-md);
- font-size: 1.1em;
- color: var(--text-normal);
- padding-bottom: var(--sf-space-sm);
- border-bottom: 1px solid var(--background-modifier-border);
-}
-.mcq-result-your-answer,
-.mcq-result-correct-answer,
-.mcq-result-attempts,
-.mcq-result-time {
- margin-top: var(--sf-space-sm);
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- gap: var(--sf-space-sm);
-}
-.mcq-result-label {
- font-weight: 600;
- color: var(--text-normal);
-}
-.mcq-result-correct {
- color: var(--sf-success);
- font-weight: 500;
-}
-.mcq-result-incorrect {
- color: var(--sf-danger);
- font-weight: 500;
-}
-.mcq-result-final-correct {
- color: var(--sf-warning);
- font-weight: 500;
-}
-.mcq-result-attempts,
-.mcq-result-time {
- font-size: 0.9em;
- color: var(--text-normal);
-}
-.mcq-close-btn {
- width: 100%;
- padding: var(--sf-space-md);
- margin-top: var(--sf-space-lg);
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
- border: none;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- font-weight: 600;
- font-size: 1.1em;
- transition: var(--sf-transition);
- box-shadow: var(--sf-shadow);
-}
-.mcq-close-btn:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow-lg);
-}
-.mcq-close-btn:active {
- transform: translateY(0);
-}
-.mcq-note-info {
- font-style: italic;
- margin-bottom: var(--sf-space-md);
- padding: var(--sf-space-md) var(--sf-space-md);
- background-color: var(--background-secondary);
- border-radius: var(--sf-radius-md);
- border-left: 4px solid var(--interactive-accent);
- display: flex;
- align-items: center;
- gap: 10px;
- box-shadow: var(--sf-shadow);
- color: var(--text-normal);
-}
-.mcq-note-info::before {
- content: "\1f4c4";
- font-style: normal;
-}
-.mcq-collection-progress {
- width: 100%;
- height: 6px;
- margin-top: var(--sf-space-lg);
- background-color: var(--background-modifier-border);
- border-radius: 10px;
- overflow: hidden;
- position: relative;
-}
-.mcq-collection-progress::after {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- height: 100%;
- background-color: var(--sf-primary);
- transition: width 0.5s ease;
-}
-.batch-review-status {
- margin-top: var(--sf-space-md);
- color: var(--sf-text-muted);
- font-style: italic;
- text-align: center;
-}
-.mcq-note-scores {
- margin-top: var(--sf-space-xl);
- max-height: 350px;
- overflow-y: auto;
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- padding: var(--sf-space-lg);
- box-shadow: var(--sf-shadow);
- scrollbar-width: thin;
-}
-.mcq-note-scores::-webkit-scrollbar {
- width: 6px;
-}
-.mcq-note-scores::-webkit-scrollbar-track {
- background: var(--sf-bg-primary);
-}
-.mcq-note-scores::-webkit-scrollbar-thumb {
- background-color: var(--background-modifier-border);
- border-radius: 10px;
-}
-.mcq-note-score {
- margin-bottom: var(--sf-space-md);
- padding-bottom: var(--sf-space-md);
- border-bottom: 1px solid var(--background-modifier-border);
- transition: var(--sf-transition);
-}
-.mcq-note-score:hover {
- background-color: var(--sf-bg-modifier);
- padding: 10px;
- margin: -10px -10px 6px -10px;
- border-radius: var(--sf-radius-md);
-}
-.mcq-note-score:last-child {
- border-bottom: none;
- margin-bottom: 0;
- padding-bottom: 0;
-}
-.mcq-note-score-title {
- font-weight: 600;
- margin-bottom: 6px;
- color: var(--text-normal);
- display: flex;
- align-items: center;
- gap: var(--sf-space-sm);
-}
-.mcq-note-score-value {
- font-weight: 500;
- padding: 4px 10px;
- border-radius: 100px;
- display: inline-block;
- margin-top: 4px;
- font-size: 0.95em;
-}
-.batch-review-info {
- margin-bottom: var(--sf-space-lg);
- background-color: var(--sf-info-light);
- padding: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- border-left: 4px solid var(--sf-info);
-}
-.batch-review-time {
- font-style: italic;
- color: var(--sf-text-muted);
- margin-top: var(--sf-space-xs);
-}
-.batch-review-buttons {
- display: flex;
- flex-direction: column;
- gap: var(--sf-space-md);
- margin: var(--sf-space-lg) 0;
-}
-.batch-review-buttons button {
- padding: var(--sf-space-md);
- font-size: 14px;
- font-weight: 500;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- transition: var(--sf-transition);
- border: none;
-}
-.batch-review-start-button {
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
-}
-.batch-review-toggle-button {
- background-color: var(--sf-secondary);
- color: var(--sf-text-on-primary);
-}
-.batch-review-regenerate-button {
- background-color: var(--sf-info);
- color: white;
-}
-.batch-review-cancel-button {
- background-color: var(--sf-bg-secondary);
- color: var(--sf-text);
- border: 1px solid var(--background-modifier-border) !important;
-}
-.batch-review-progress {
- margin-bottom: var(--sf-space-lg);
- padding: var(--sf-space-md);
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
-}
-.batch-review-current-note {
- font-weight: bold;
- color: var(--sf-primary);
- margin-bottom: var(--sf-space-xs);
-}
-.batch-review-summary-stats {
- background-color: var(--sf-bg-secondary);
- padding: var(--sf-space-md);
- border-radius: var(--sf-radius-md);
- margin: var(--sf-space-lg) 0;
- box-shadow: var(--sf-shadow);
-}
-.batch-review-success {
- color: var(--sf-success);
- font-weight: bold;
-}
-.batch-review-needs-improvement {
- color: var(--sf-danger);
- font-weight: bold;
-}
-.batch-review-results {
- margin-top: var(--sf-space-lg);
-}
-.batch-review-result-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: var(--sf-space-md);
- margin-bottom: var(--sf-space-xs);
- background-color: var(--sf-bg-secondary);
- border-radius: var(--sf-radius-md);
- transition: var(--sf-transition);
-}
-.batch-review-result-item:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow);
-}
-.batch-review-result-filename {
- font-weight: 500;
- flex: 1;
-}
-.batch-review-result-mcq-score {
- font-style: italic;
- margin-left: 10px;
-}
-.batch-review-complete-blackout,
-.batch-review-incorrect,
-.batch-review-incorrect-familiar,
-.batch-review-correct-difficulty,
-.batch-review-correct-hesitation,
-.batch-review-perfect-recall,
-.batch-review-hard,
-.batch-review-fair,
-.batch-review-good,
-.batch-review-perfect {
- padding: 5px 10px;
- border-radius: 100px;
- min-width: 100px;
- text-align: center;
- font-weight: 500;
- font-size: 13px;
-}
-.batch-review-complete-blackout {
- background-color: var(--sf-danger-light);
- color: var(--sf-danger);
-}
-.batch-review-incorrect {
- background-color: var(--sf-danger-light);
- color: var(--sf-danger);
-}
-.batch-review-incorrect-familiar {
- background-color: var(--sf-danger-light);
- color: var(--sf-danger);
-}
-.batch-review-correct-hesitation {
- background-color: var(--sf-success-light);
- color: var(--sf-success);
-}
-.batch-review-hard {
- background-color: var(--sf-danger-light);
- color: var(--sf-danger);
-}
-.batch-review-fair {
- background-color: var(--sf-warning-light);
- color: var(--sf-warning);
-}
-.batch-review-good {
- background-color: var(--sf-success-light);
- color: var(--sf-success);
-}
-.batch-review-perfect {
- background-color: var(--sf-success-light);
- color: var(--sf-success);
-}
-.batch-review-good {
- background-color: var(--sf-success-light);
- color: var(--sf-success);
-}
-.batch-review-perfect {
- background-color: var(--sf-success-light);
- color: var(--sf-success);
-}
-.batch-review-close-button {
- background-color: var(--sf-primary);
- color: var(--sf-text-on-primary);
- padding: var(--sf-space-md);
- font-size: 14px;
- font-weight: 500;
- border-radius: var(--sf-radius-md);
- cursor: pointer;
- transition: var(--sf-transition);
- margin-top: var(--sf-space-lg);
- width: 100%;
- border: none;
-}
-.batch-review-close-button:hover {
- transform: translateY(-2px);
- box-shadow: var(--sf-shadow);
-}
-.mcq-choice-btn {
- position: relative;
- overflow: hidden;
- z-index: 1;
-}
-.mcq-choice-btn::after {
- content: "";
- position: absolute;
- top: 50%;
- left: 50%;
- width: 5px;
- height: 5px;
- background: rgba(var(--sf-primary), 0.3);
- opacity: 0;
- border-radius: 100%;
- transform: scale(1, 1) translate(-50%, -50%);
- transform-origin: 50% 50%;
- z-index: -1;
-}
-.mcq-choice-btn:focus {
- outline: none;
-}
-.mcq-choice-btn:focus::after {
- animation: ripple 0.8s ease-out;
-}
-.mcq-celebration {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- pointer-events: none;
- overflow: hidden;
- z-index: 10;
-}
-.mcq-celebration-item {
- position: absolute;
- width: 10px;
- height: 10px;
- background: var(--sf-primary);
- border-radius: 2px;
- animation: celebrationConfetti 3s ease-out forwards;
-}
-[class*=" or "],
-[class^="or "] {
- background-color: var(--sf-bg-primary) !important;
- color: var(--sf-text) !important;
- border: 1px solid var(--background-modifier-border) !important;
- padding: 2px 6px !important;
- border-radius: 4px !important;
-}
-.mcq-choice-selected {
- border-color: var(--interactive-accent) !important;
- box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.5), 0 1px 3px rgba(0, 0, 0, 0.1);
- transform: translateX(4px);
-}
-.mcq-key-pressed {
- transform: scale(0.98);
- opacity: 0.9;
-}
-.theme-dark .spaceforge-mcq-modal .mcq-choice-btn {
- color: var(--text-normal) !important;
-}
-.theme-dark .spaceforge-mcq-modal .mcq-choice-btn .mcq-choice-text {
- color: var(--text-normal) !important;
-}
-.theme-dark .spaceforge-mcq-modal .mcq-choice-btn .mcq-shortcut-hint {
- color: var(--text-muted) !important;
-}
-.mcq-result-algorithm-details {
- margin-top: var(--sf-space-sm);
- padding-top: var(--sf-space-sm);
- border-top: 1px dashed var(--background-modifier-border);
- font-size: 0.9em;
-}
-.mcq-result-algorithm-details .mcq-result-label {
- font-weight: 600;
- color: var(--text-normal);
- margin-right: var(--sf-space-xs);
-}
-.mcq-result-algorithm-value {
- color: var(--text-muted);
-}
-.mcq-result-algorithm-item {
- margin-bottom: var(--sf-space-xs);
- display: flex;
- align-items: baseline;
-}
-.mcq-result-algorithm-details h4.mcq-result-label {
- margin-bottom: var(--sf-space-sm);
- font-size: 1.1em;
- color: var(--text-normal);
-}
-:root {
- --mcq-correct-answer-text: var(--sf-success-dark, #276749);
-}
-.mcq-detailed-breakdown {
- margin-top: var(--sf-space-xl);
- padding-top: var(--sf-space-lg);
- border-top: 1px solid var(--background-modifier-border);
-}
-.mcq-detailed-breakdown h3 {
- margin-bottom: var(--sf-space-lg);
- font-size: 1.3em;
- color: var(--text-normal);
- font-weight: 600;
-}
-.mcq-breakdown-item {
- margin-bottom: var(--sf-space-lg);
- padding: var(--sf-space-md);
- background-color: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--sf-radius-md);
- box-shadow: var(--sf-shadow-sm);
-}
-.mcq-breakdown-q-header {
- font-weight: 600;
- color: var(--text-muted);
-}
-.mcq-breakdown-item div {
- margin-bottom: var(--sf-space-xs);
- line-height: 1.5;
-}
-.mcq-breakdown-item span {
- display: inline;
-}
-.mcq-breakdown-item div > span:first-child {
- font-weight: 500;
- color: var(--text-faint);
-}
-.mcq-breakdown-item .correctness-indicator {
- font-weight: bold;
-}
-.mcq-breakdown-item .correct-answer-text {
- color: var(--mcq-correct-answer-text);
- font-weight: bold;
-}
-
-/* styles/pomodoro.css */
-.pomodoro-visibility-toggle-container {
- display: none !important;
-}
-.sidebar-pomodoro-section {
- width: 100%;
- margin-top: 8px;
- margin-bottom: 8px;
- padding: 8px;
- background-color: var(--background-secondary);
- border-radius: var(--radius-m);
- border: 1px solid var(--background-modifier-border);
- box-sizing: border-box;
-}
-.sidebar-pomodoro-section-container {
- width: 100%;
- margin-top: 8px;
- margin-bottom: 8px;
- padding: 8px;
- background-color: var(--background-secondary);
- border-radius: var(--radius-m);
- border: 1px solid var(--background-modifier-border);
- box-sizing: border-box;
-}
-.pomodoro-visibility-toggle-container {
- margin-bottom: 8px;
-}
-.pomodoro-visibility-toggle-container .pomodoro-visibility-toggle-btn {
- width: 100%;
- padding: 6px 12px;
- background-color: var(--background-modifier-border);
- color: var(--text-normal);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--radius-s);
- font-weight: 500;
- text-align: center;
- cursor: pointer;
- transition: var(--sf-transition);
-}
-.pomodoro-visibility-toggle-container .pomodoro-visibility-toggle-btn:hover {
- background-color: var(--background-modifier-hover);
-}
-.pomodoro-main-controls {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- width: 100%;
-}
-.pomodoro-main-controls .pomodoro-start-btn,
-.pomodoro-main-controls .pomodoro-stop-btn,
-.pomodoro-main-controls .pomodoro-skip-btn {
- padding: 6px;
- border-radius: var(--radius-s);
- background-color: var(--background-secondary-alt);
- border: 1px solid var(--background-modifier-border);
- color: var(--text-normal);
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: var(--sf-transition);
-}
-.pomodoro-main-controls .pomodoro-start-btn:hover,
-.pomodoro-main-controls .pomodoro-stop-btn:hover,
-.pomodoro-main-controls .pomodoro-skip-btn:hover {
- background-color: var(--background-modifier-hover);
- border-color: var(--interactive-accent-hover);
-}
-.pomodoro-main-controls .pomodoro-start-btn svg,
-.pomodoro-main-controls .pomodoro-stop-btn svg,
-.pomodoro-main-controls .pomodoro-skip-btn svg {
- width: 18px;
- height: 18px;
-}
-.pomodoro-timer-display {
- font-size: 20px;
- font-weight: 600;
- font-family: monospace;
- color: var(--text-normal);
- padding: 4px 10px;
- border-radius: var(--radius-s);
- background-color: var(--background-primary);
- min-width: 60px;
- text-align: center;
- flex-grow: 1;
- margin: 0 5px;
- border: 1px solid var(--background-modifier-border);
- cursor: pointer;
- transition: background-color 0.2s ease-in-out;
-}
-.pomodoro-timer-display:hover {
- background-color: var(--background-secondary-alt);
-}
-.pomodoro-timer-display.mode-work {
- border-left-color: var(--interactive-accent);
- border-right-color: var(--interactive-accent);
-}
-.pomodoro-timer-display.mode-shortBreak {
- border-left-color: var(--sf-success);
- border-right-color: var(--sf-success);
-}
-.pomodoro-timer-display.mode-longBreak {
- border-left-color: var(--sf-info);
- border-right-color: var(--sf-info);
-}
-.pomodoro-timer-display.mode-idle {
- border-left-color: var(--text-muted);
- border-right-color: var(--text-muted);
- color: var(--text-muted);
-}
-.pomodoro-settings-panel-container {
- position: relative;
- width: 100%;
- z-index: 10;
-}
-.pomodoro-quick-settings-panel {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- background-color: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-top: none;
- border-radius: 0 0 var(--radius-m) var(--radius-m);
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- padding: 0;
- flex-direction: column;
- gap: 8px;
- overflow: hidden;
- max-height: 0;
- opacity: 0;
- transition:
- max-height 0.35s cubic-bezier(0.25, 0.1, 0.25, 1),
- opacity 0.3s ease-out,
- padding 0.35s cubic-bezier(0.25, 0.1, 0.25, 1);
-}
-.pomodoro-quick-settings-panel[style*="display: flex"] {
- padding: 12px;
- max-height: 500px;
- opacity: 1;
- border-top: 1px solid var(--background-modifier-border);
-}
-.pomodoro-quick-setting {
- display: flex;
- justify-content: space-between;
- align-items: center;
-}
-.pomodoro-quick-setting label {
- font-size: 13px;
- color: var(--text-muted);
-}
-.pomodoro-quick-setting input[type=number] {
- width: 50px;
- background-color: var(--background-secondary);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--radius-s);
- padding: 4px 6px;
- text-align: right;
-}
-.pomodoro-quick-settings-buttons {
- display: flex;
- justify-content: flex-end;
- align-items: center;
- gap: 8px;
- margin-top: 8px;
-}
-.pomodoro-quick-save-btn,
-.pomodoro-quick-calculate-btn {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- padding: 6px 10px;
- font-size: 13px;
- line-height: 1.3;
- border-radius: var(--radius-s);
- cursor: pointer;
- transition: var(--sf-transition);
- border: none;
- box-sizing: border-box;
- appearance: none;
- -webkit-appearance: none;
- -moz-appearance: none;
- text-align: center;
-}
-.pomodoro-quick-calculate-btn {
- background-color: var(--background-modifier-border);
- color: var(--text-normal);
-}
-.pomodoro-quick-calculate-btn:hover {
- background-color: var(--background-modifier-hover);
-}
-.pomodoro-calculation-result {
- font-size: 0.9em;
- margin-top: 8px;
- padding: 10px;
- background-color: var(--background-secondary-alt);
- border-radius: var(--radius-s);
- border: 1px solid var(--background-modifier-border);
- line-height: 1.4;
-}
-.pomodoro-calculation-result p {
- margin: 4px 0;
-}
-.pomodoro-cycle-progress {
- font-size: 0.75em;
- color: var(--text-muted);
- text-align: center;
- margin-top: 4px;
- padding: 2px 6px;
- border-radius: var(--radius-s);
- transition: var(--sf-transition);
- line-height: 1.2;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.pomodoro-cycle-progress.cycle-active {
- color: var(--sf-info);
- background-color: var(--background-modifier-hover);
- font-weight: 500;
-}
-.pomodoro-override-container {
- margin: 12px 0;
- padding: 10px;
- background-color: var(--background-secondary-alt);
- border-radius: var(--radius-s);
- border: 1px solid var(--background-modifier-border);
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
-}
-.pomodoro-override-label {
- font-size: 0.9em;
- color: var(--text-normal);
- font-weight: 500;
- margin-bottom: 6px;
- display: block;
-}
-.pomodoro-override-inputs {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-bottom: 8px;
-}
-.pomodoro-override-hours,
-.pomodoro-override-minutes {
- width: 50px;
- background-color: var(--background-primary);
- border: 1px solid var(--background-modifier-border);
- border-radius: var(--radius-s);
- padding: 4px 6px;
- text-align: center;
- font-size: 0.9em;
-}
-.pomodoro-override-hours:focus,
-.pomodoro-override-minutes:focus {
- outline: none;
- border-color: var(--interactive-accent);
- box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2);
-}
-.pomodoro-override-label-small {
- font-size: 0.8em;
- color: var(--text-muted);
- font-weight: 400;
-}
-.pomodoro-override-toggle-container {
- display: flex;
- align-items: center;
- gap: 8px;
-}
-.pomodoro-add-to-estimation {
- margin: 0;
-}
-.pomodoro-toggle-label {
- font-size: 0.85em;
- color: var(--text-normal);
- cursor: pointer;
- user-select: none;
-}
-.pomodoro-override-info {
- color: var(--sf-info);
- font-style: italic;
- font-size: 0.9em;
- margin-top: 4px;
- padding: 4px 6px;
- background-color: var(--background-primary);
- border-radius: var(--radius-s);
- border-left: 3px solid var(--sf-info);
-}
-
-/* styles/responsive.css */
-@media (max-width: 768px) {
- .sf-space-lg {
- --sf-space-lg: 16px;
- }
- .sf-space-xl {
- --sf-space-xl: 24px;
- }
- .mcq-header-container h2 {
- font-size: 1.5rem;
- }
- .mcq-question-text {
- font-size: 1.1em;
- }
- .mcq-choice-btn {
- padding: var(--sf-space-sm) var(--sf-space-md);
- }
- .mcq-score-text {
- font-size: 1.6em;
- }
- .review-note-item {
- flex-direction: column;
- align-items: flex-start;
- }
- .review-note-title {
- margin-right: 0;
- margin-bottom: 5px;
- width: 100%;
- }
- .review-note-info {
- margin-right: 0;
- margin-bottom: 5px;
- width: 100%;
- }
- .review-note-buttons {
- width: 100%;
- justify-content: space-between;
- }
- .review-note-actions {
- flex-grow: 1;
- justify-content: flex-start;
- }
- .review-note-drag-handle {
- margin-left: 0;
- }
- .sf-setting-grid {
- grid-template-columns: 1fr;
- }
- .setting-item {
- flex-direction: column;
- align-items: flex-start;
- }
- .setting-item-info {
- margin-bottom: 8px;
- }
- .setting-item-control {
- width: 100%;
- }
-}
-@media (max-width: 480px) {
- .mcq-header-container h2 {
- font-size: 1.3rem;
- }
- .mcq-refresh-btn {
- width: 30px;
- height: 30px;
- }
- .review-stats {
- flex-direction: column;
- align-items: flex-start;
- gap: 5px;
- }
- .review-view-toggle {
- margin-top: 12px;
- }
- .calendar-day {
- min-height: 40px;
- padding: 2px;
- }
- .calendar-weekday {
- font-size: 10px;
- }
- .calendar-review-count {
- width: 16px;
- height: 16px;
- font-size: 9px;
- }
- .review-date-header {
- flex-direction: column;
- align-items: flex-start;
- }
- .review-date-postpone-all {
- margin-top: 8px;
- }
- .sf-settings-section-header h3 {
- font-size: 16px;
- }
- .sf-settings-section-content {
- padding-left: 0;
- }
-}
-@media (max-height: 580px) and (orientation: landscape) {
- .mcq-question-container {
- max-height: 60vh;
- min-height: 200px;
- }
- .mcq-choice-btn {
- min-height: 50px;
- padding: 10px 16px;
- }
- .calendar-day {
- height: 40px;
- }
- .mcq-progress {
- margin-bottom: 12px;
- height: 24px;
- }
-}
-@media (min-width: 1600px) {
- .sf-space-lg {
- --sf-space-lg: 28px;
- }
- .sf-space-xl {
- --sf-space-xl: 36px;
- }
- .mcq-question-container {
- max-height: 1000px;
- }
- .calendar-grid {
- grid-auto-rows: minmax(90px, auto);
- }
- .calendar-day {
- height: 90px;
- }
-}
-
-/* styles/styles.css */
diff --git a/services/calendar-event-service.ts b/services/calendar-event-service.ts
new file mode 100644
index 0000000..e9926f2
--- /dev/null
+++ b/services/calendar-event-service.ts
@@ -0,0 +1,350 @@
+import { CalendarEvent, DateEvents, UpcomingEvent, EventCategory, EventRecurrence } from '../models/calendar-event';
+import { DateUtils } from '../utils/dates';
+
+/**
+ * Service for managing calendar events
+ */
+export class CalendarEventService {
+ /**
+ * Storage for calendar events
+ */
+ private events: Map = new Map();
+
+ /**
+ * Initialize the service with existing events
+ *
+ * @param events Array of existing events
+ */
+ initialize(events: CalendarEvent[] = []): void {
+ this.events.clear();
+ events.forEach(event => {
+ this.events.set(event.id, event);
+ });
+ }
+
+ /**
+ * Get all events
+ *
+ * @returns Array of all events
+ */
+ getAllEvents(): CalendarEvent[] {
+ return Array.from(this.events.values());
+ }
+
+ /**
+ * Get event by ID
+ *
+ * @param id Event ID
+ * @returns Event or null if not found
+ */
+ getEventById(id: string): CalendarEvent | null {
+ return this.events.get(id) || null;
+ }
+
+ /**
+ * Create a new event
+ *
+ * @param eventData Event data (without id, createdAt, updatedAt)
+ * @returns Created event
+ */
+ createEvent(eventData: Omit): CalendarEvent {
+ const now = Date.now();
+ const event: CalendarEvent = {
+ ...eventData,
+ id: this.generateId(),
+ createdAt: now,
+ updatedAt: now
+ };
+
+ this.events.set(event.id, event);
+ return event;
+ }
+
+ /**
+ * Update an existing event
+ *
+ * @param id Event ID
+ * @param updates Partial event data to update
+ * @returns Updated event or null if not found
+ */
+ updateEvent(id: string, updates: Partial): CalendarEvent | null {
+ const existingEvent = this.events.get(id);
+ if (!existingEvent) {
+ return null;
+ }
+
+ const updatedEvent: CalendarEvent = {
+ ...existingEvent,
+ ...updates,
+ id, // Ensure ID doesn't change
+ createdAt: existingEvent.createdAt, // Preserve creation time
+ updatedAt: Date.now()
+ };
+
+ this.events.set(id, updatedEvent);
+ return updatedEvent;
+ }
+
+ /**
+ * Delete an event
+ *
+ * @param id Event ID
+ * @returns True if deleted, false if not found
+ */
+ deleteEvent(id: string): boolean {
+ return this.events.delete(id);
+ }
+
+ /**
+ * Get events for a specific date range
+ *
+ * @param startDate Start date timestamp
+ * @param endDate End date timestamp
+ * @returns Array of events in the date range
+ */
+ getEventsInRange(startDate: number, endDate: number): CalendarEvent[] {
+ return this.getAllEvents().filter(event => {
+ 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;
+ });
+ }
+
+ /**
+ * Get events for a specific date
+ *
+ * @param date Date timestamp
+ * @returns Array of events for the date
+ */
+ getEventsForDate(date: number): CalendarEvent[] {
+ const targetDate = DateUtils.startOfDay(new Date(date));
+ return this.getAllEvents().filter(event => {
+ const eventDate = DateUtils.startOfDay(new Date(event.date));
+ return eventDate === targetDate;
+ });
+ }
+
+ /**
+ * Get events grouped by date for a date range
+ *
+ * @param startDate Start date timestamp
+ * @param endDate End date timestamp
+ * @returns Map of date keys to DateEvents
+ */
+ getEventsGroupedByDate(startDate: number, endDate: number): Map {
+ const eventsByDate = new Map();
+ const eventsInRange = this.getEventsInRange(startDate, endDate);
+
+ eventsInRange.forEach(event => {
+ const eventDateStart = DateUtils.startOfDay(new Date(event.date));
+ const dateKey = eventDateStart.toString();
+
+ if (!eventsByDate.has(dateKey)) {
+ eventsByDate.set(dateKey, {
+ timestamp: eventDateStart,
+ events: []
+ });
+ }
+
+ const dateEvents = eventsByDate.get(dateKey);
+ if (dateEvents) {
+ dateEvents.events.push(event);
+ }
+ });
+
+ return eventsByDate;
+ }
+
+ /**
+ * Get upcoming events
+ *
+ * @param daysAhead Number of days ahead to look
+ * @param fromDate Optional start date (defaults to today)
+ * @returns Array of upcoming events
+ */
+ getUpcomingEvents(daysAhead: number = 7, fromDate: Date = new Date()): UpcomingEvent[] {
+ const today = DateUtils.startOfDay(fromDate);
+ const endDate = DateUtils.addDays(today, daysAhead);
+ const eventsInRange = this.getEventsInRange(today, endDate);
+
+ return eventsInRange
+ .map(event => {
+ const eventDate = DateUtils.startOfDay(new Date(event.date));
+ const daysUntil = Math.floor((eventDate - today) / (24 * 60 * 60 * 1000));
+
+ return {
+ event,
+ daysUntil,
+ isToday: daysUntil === 0,
+ isTomorrow: daysUntil === 1
+ };
+ })
+ .sort((a, b) => {
+ // Sort by days until, then by time
+ 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;
+ });
+ }
+
+ /**
+ * Generate recurring event instances
+ *
+ * @param event Recurring event
+ * @param startDate Start date for generating instances
+ * @param endDate End date for generating instances
+ * @returns Array of event instances
+ */
+ generateRecurringInstances(event: CalendarEvent, startDate: number, endDate: number): CalendarEvent[] {
+ if (event.recurrence === EventRecurrence.None) {
+ return [event];
+ }
+
+ const instances: CalendarEvent[] = [];
+ const start = DateUtils.startOfDay(new Date(startDate));
+ const end = DateUtils.startOfDay(new Date(endDate));
+ const eventDate = DateUtils.startOfDay(new Date(event.date));
+ const recurrenceEnd = event.recurrenceEndDate ? DateUtils.startOfDay(new Date(event.recurrenceEndDate)) : end;
+
+ let currentDate = new Date(eventDate);
+
+ while (currentDate.getTime() <= Math.min(end, recurrenceEnd)) {
+ if (currentDate.getTime() >= start) {
+ const instance: CalendarEvent = {
+ ...event,
+ date: currentDate.getTime(),
+ id: `${event.id}-${currentDate.getTime()}` // Unique ID for instance
+ };
+ instances.push(instance);
+ }
+
+ // Move to next occurrence
+ switch (event.recurrence) {
+ case EventRecurrence.Daily:
+ currentDate.setDate(currentDate.getDate() + 1);
+ break;
+ case EventRecurrence.Weekly:
+ currentDate.setDate(currentDate.getDate() + 7);
+ break;
+ case EventRecurrence.Monthly:
+ currentDate.setMonth(currentDate.getMonth() + 1);
+ break;
+ case EventRecurrence.Yearly:
+ currentDate.setFullYear(currentDate.getFullYear() + 1);
+ break;
+ }
+ }
+
+ return instances;
+ }
+
+ /**
+ * Get events by category
+ *
+ * @param category Event category
+ * @returns Array of events in the category
+ */
+ getEventsByCategory(category: EventCategory): CalendarEvent[] {
+ return this.getAllEvents().filter(event => event.category === category);
+ }
+
+ /**
+ * Search events by title or description
+ *
+ * @param query Search query
+ * @returns Array of matching events
+ */
+ searchEvents(query: string): CalendarEvent[] {
+ const lowerQuery = query.toLowerCase();
+ return this.getAllEvents().filter(event =>
+ event.title.toLowerCase().includes(lowerQuery) ||
+ (event.description && event.description.toLowerCase().includes(lowerQuery))
+ );
+ }
+
+ /**
+ * Export events to JSON
+ *
+ * @returns JSON string of all events
+ */
+ exportEvents(): string {
+ return JSON.stringify(this.getAllEvents(), null, 2);
+ }
+
+ /**
+ * Import events from JSON
+ *
+ * @param json JSON string of events
+ * @returns Number of imported events
+ */
+ importEvents(json: string): number {
+ 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);
+ return 0;
+ }
+ }
+
+ /**
+ * Generate a unique ID for new events
+ *
+ * @returns Unique ID string
+ */
+ private generateId(): string {
+ return `event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
+ }
+
+ /**
+ * Get event color (custom color or category color)
+ *
+ * @param event Event
+ * @returns Color hex code
+ */
+ getEventColor(event: CalendarEvent): string {
+ return event.color || this.getCategoryColor(event.category);
+ }
+
+ /**
+ * Get category color
+ *
+ * @param category Event category
+ * @returns Color hex code
+ */
+ getCategoryColor(category: EventCategory): string {
+ const colors = {
+ [EventCategory.Work]: '#4A90E2',
+ [EventCategory.Personal]: '#7B68EE',
+ [EventCategory.Study]: '#50C878',
+ [EventCategory.Meeting]: '#FF6B6B',
+ [EventCategory.Health]: '#FF9F40',
+ [EventCategory.Social]: '#FF69B4',
+ [EventCategory.Other]: '#95A5A6'
+ };
+
+ return colors[category] || colors[EventCategory.Other];
+ }
+}
\ No newline at end of file
diff --git a/styles.css b/styles.css
index ecf6c44..b9f9288 100644
--- a/styles.css
+++ b/styles.css
@@ -12,6 +12,8 @@
--sf-danger-light: rgba(244, 67, 54, 0.15);
--sf-info: #2196f3;
--sf-info-light: rgba(33, 150, 243, 0.15);
+ --sf-overdue-accent: #ff8c42;
+ --sf-overdue-bg: rgba(255, 140, 66, 0.08);
--sf-text: var(--text-normal, #333);
--sf-text-muted: var(--text-muted, #888);
--sf-text-on-primary: var(--text-on-accent, white);
@@ -40,6 +42,7 @@
--sf-warning-light: rgba(255, 152, 0, 0.2);
--sf-danger-light: rgba(244, 67, 54, 0.2);
--sf-info-light: rgba(33, 150, 243, 0.2);
+ --sf-overdue-bg: rgba(255, 140, 66, 0.12);
--sf-bg-primary: var(--background-primary-alt);
--sf-bg-secondary: var(--background-secondary-alt);
}
@@ -945,7 +948,7 @@ button.disabled:hover {
border: 1px solid var(--background-modifier-border);
}
.review-date-section-overdue {
- border-left: 3px solid var(--text-error);
+ border-left: 3px solid var(--sf-overdue-accent);
background-color: var(--background-secondary);
}
.review-date-header {
@@ -976,7 +979,7 @@ button.disabled:hover {
min-width: 0;
}
.review-date-section-overdue .review-date-header h3 {
- color: var(--text-error);
+ color: var(--sf-overdue-accent);
}
.review-date-postpone-all {
padding: 4px 10px;
@@ -1030,8 +1033,8 @@ button.disabled:hover {
border-color: var(--interactive-accent);
}
.review-note-item.overdue-note {
- background-color: var(--background-modifier-error-hover);
- border-left: 3px solid var(--text-error);
+ background-color: var(--sf-overdue-bg);
+ border-left: 3px solid var(--sf-overdue-accent);
}
.review-note-title {
font-weight: 500;
@@ -1913,6 +1916,888 @@ button.disabled:hover {
padding: 1px 3px;
}
}
+.calendar-events-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ margin-top: 2px;
+ align-items: center;
+}
+.calendar-event-tab {
+ font-size: 9px;
+ padding: 1px 3px;
+ border-radius: 2px;
+ color: white;
+ cursor: pointer;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 500;
+ line-height: 1.2;
+}
+.calendar-event-tab:hover {
+ transform: scale(1.05);
+ opacity: 0.9;
+ z-index: 10;
+}
+.calendar-events-more {
+ font-size: 8px;
+ padding: 1px 3px;
+ border-radius: 2px;
+ background-color: var(--text-muted);
+ color: var(--background-primary);
+ font-weight: 600;
+ min-width: 12px;
+ text-align: center;
+ line-height: 1.2;
+}
+.calendar-day.has-events {
+ border-left: 3px solid var(--interactive-accent);
+}
+.upcoming-events-wrapper {
+ margin-top: var(--sf-space-md);
+ padding-top: var(--sf-space-md);
+ border-top: 1px solid var(--background-modifier-border);
+}
+.upcoming-events-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: var(--sf-space-sm);
+ padding-bottom: var(--sf-space-sm);
+ border-bottom: 1px solid var(--background-modifier-border-hover);
+}
+.upcoming-events-title {
+ font-weight: 600;
+ font-size: 16px;
+ color: var(--text-normal);
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-xs);
+}
+.upcoming-events-subtitle {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 400;
+}
+.upcoming-events-container {
+ max-height: 300px;
+ overflow-y: auto;
+ padding-right: var(--sf-space-xs);
+}
+.upcoming-events-container::-webkit-scrollbar {
+ width: 4px;
+}
+.upcoming-events-container::-webkit-scrollbar-track {
+ background: var(--background-secondary);
+ border-radius: 2px;
+}
+.upcoming-events-container::-webkit-scrollbar-thumb {
+ background: var(--text-muted);
+ border-radius: 2px;
+}
+.upcoming-events-container::-webkit-scrollbar-thumb:hover {
+ background: var(--text-faint);
+}
+.upcoming-events-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: var(--sf-space-lg) var(--sf-space-md);
+ text-align: center;
+ color: var(--text-muted);
+}
+.upcoming-events-empty-icon {
+ font-size: 24px;
+ margin-bottom: var(--sf-space-sm);
+ opacity: 0.5;
+}
+.upcoming-events-empty-text {
+ font-weight: 500;
+ margin-bottom: var(--sf-space-xs);
+ color: var(--text-muted);
+}
+.upcoming-events-empty-subtext {
+ font-size: 12px;
+ color: var(--text-faint);
+}
+.upcoming-events-day-section {
+ margin-bottom: var(--sf-space-md);
+ border-radius: var(--radius-m);
+ background-color: var(--background-secondary);
+ overflow: hidden;
+ transition: var(--sf-transition);
+}
+.upcoming-events-day-section:hover {
+ background-color: var(--background-modifier-hover);
+}
+.upcoming-events-day-section.today {
+ background-color: rgba(var(--interactive-accent-rgb), 0.1);
+ border: 1px solid rgba(var(--interactive-accent-rgb), 0.2);
+}
+.upcoming-events-day-section.tomorrow {
+ background-color: rgba(var(--interactive-accent-rgb), 0.05);
+}
+.upcoming-events-day-header {
+ display: flex;
+ align-items: center;
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ background-color: var(--background-modifier-border);
+ border-bottom: 1px solid var(--background-modifier-border-hover);
+}
+.upcoming-events-day-title {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-normal);
+}
+.upcoming-events-day-section.today .upcoming-events-day-title {
+ color: var(--interactive-accent);
+}
+.upcoming-events-day-events {
+ padding: var(--sf-space-xs);
+}
+.upcoming-events-event-item {
+ display: flex;
+ align-items: flex-start;
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ margin: var(--sf-space-xs) 0;
+ border-radius: var(--radius-s);
+ cursor: pointer;
+ transition: var(--sf-transition);
+ background-color: var(--background-primary);
+ border: 1px solid transparent;
+}
+.upcoming-events-event-item:hover {
+ background-color: var(--background-modifier-hover);
+ border-color: var(--background-modifier-border);
+ transform: translateX(2px);
+}
+.upcoming-events-event-color {
+ width: 4px;
+ height: 100%;
+ min-height: 32px;
+ border-radius: 2px;
+ margin-right: var(--sf-space-sm);
+ flex-shrink: 0;
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+.upcoming-events-event-content {
+ flex: 1;
+ min-width: 0;
+}
+.upcoming-events-event-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 2px;
+ gap: var(--sf-space-sm);
+}
+.upcoming-events-event-title {
+ font-weight: 500;
+ font-size: 13px;
+ color: var(--text-normal);
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.upcoming-events-event-time {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-weight: 400;
+ flex-shrink: 0;
+}
+.upcoming-events-event-details {
+ margin-bottom: 2px;
+}
+.upcoming-events-event-location {
+ font-size: 11px;
+ color: var(--text-muted);
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ margin-bottom: 2px;
+}
+.upcoming-events-event-description {
+ font-size: 11px;
+ color: var(--text-faint);
+ line-height: 1.3;
+ margin-bottom: 2px;
+}
+.upcoming-events-event-category {
+ font-size: 10px;
+ color: var(--text-muted);
+ background-color: var(--background-modifier-border);
+ padding: 1px 4px;
+ border-radius: 2px;
+ display: inline-block;
+ text-transform: capitalize;
+ font-weight: 500;
+}
+.event-modal-datetime-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+ margin-bottom: var(--sf-space-md);
+}
+.event-modal-category-color-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+ margin-bottom: var(--sf-space-md);
+}
+.event-modal-buttons {
+ display: flex;
+ gap: var(--sf-space-sm);
+ justify-content: flex-end;
+ margin-top: var(--sf-space-lg);
+ padding-top: var(--sf-space-md);
+ border-top: 1px solid var(--background-modifier-border);
+}
+.event-modal-buttons button {
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ border-radius: var(--radius-s);
+ border: 1px solid var(--background-modifier-border);
+ cursor: pointer;
+ transition: var(--sf-transition);
+ font-weight: 500;
+}
+.event-modal-buttons button.mod-cta {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+ border-color: var(--interactive-accent);
+}
+.event-modal-buttons button.mod-cta:hover {
+ background-color: var(--interactive-accent-hover);
+}
+.event-modal-buttons button:not(.mod-cta) {
+ background-color: var(--background-secondary);
+ color: var(--text-normal);
+}
+.event-modal-buttons button:not(.mod-cta):hover {
+ background-color: var(--background-modifier-hover);
+}
+.event-modal-buttons button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+@media (max-width: 768px) {
+ .event-modal-datetime-row,
+ .event-modal-category-color-row {
+ grid-template-columns: 1fr;
+ gap: var(--sf-space-sm);
+ }
+ .upcoming-events-container {
+ max-height: 200px;
+ }
+ .calendar-event-tab {
+ font-size: 8px;
+ padding: 1px 2px;
+ }
+ .upcoming-events-event-item {
+ padding: var(--sf-space-xs) var(--sf-space-sm);
+ }
+ .upcoming-events-event-title {
+ font-size: 12px;
+ }
+ .upcoming-events-event-time {
+ font-size: 10px;
+ }
+}
+.calendar-day {
+ position: relative;
+}
+.calendar-day-add-event {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+ border-radius: 50%;
+ font-size: 10px;
+ cursor: pointer;
+ opacity: 0;
+ transform: scale(0.8);
+ transition: all 0.2s ease;
+ z-index: 20;
+ box-shadow: var(--sf-shadow-sm);
+}
+.calendar-day:hover .calendar-day-add-event {
+ opacity: 1;
+ transform: scale(1);
+}
+.calendar-day-add-event:hover {
+ background-color: var(--interactive-accent-hover);
+ transform: scale(1.1);
+ box-shadow: var(--sf-shadow-md);
+}
+.calendar-day.empty:hover .calendar-day-add-event {
+ opacity: 0.7;
+}
+.calendar-day.has-reviews .calendar-day-add-event {
+ top: 28px;
+}
+.calendar-day.has-events .calendar-day-add-event {
+ top: 28px;
+}
+.calendar-day.has-reviews.has-events .calendar-day-add-event {
+ top: 44px;
+}
+.event-modal {
+ max-width: 600px;
+ max-height: 90vh;
+ overflow-y: auto;
+}
+.event-modal .modal-content {
+ border-radius: 12px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+.event-modal-header {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-md);
+ padding: var(--sf-space-lg) var(--sf-space-lg) var(--sf-space-md) var(--sf-space-lg);
+ background: linear-gradient(135deg, var(--interactive-accent) 0%, var(--interactive-accent-hover) 100%);
+ color: var(--text-on-accent);
+ border-radius: 12px 12px 0 0;
+}
+.event-modal-header-icon {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ font-size: 20px;
+}
+.event-modal-header-text {
+ flex: 1;
+}
+.event-modal-title {
+ margin: 0 0 var(--sf-space-xs) 0;
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--text-on-accent);
+}
+.event-modal-subtitle {
+ margin: 0;
+ font-size: 14px;
+ opacity: 0.9;
+ color: var(--text-on-accent);
+}
+.event-modal-form {
+ padding: var(--sf-space-lg);
+}
+.event-modal-section {
+ margin-bottom: var(--sf-space-lg);
+}
+.event-modal-label {
+ display: block;
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-normal);
+ margin-bottom: var(--sf-space-sm);
+}
+.event-modal-label.required::after {
+ content: " *";
+ color: var(--text-error);
+}
+.event-modal-input,
+.event-modal-textarea,
+.event-modal-select {
+ width: 100%;
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ border: 2px solid var(--background-modifier-border);
+ border-radius: 8px;
+ font-size: 14px;
+ background-color: var(--background-primary);
+ color: var(--text-normal);
+ transition: all 0.2s ease;
+ box-sizing: border-box;
+}
+.event-modal-input:focus,
+.event-modal-textarea:focus,
+.event-modal-select:focus {
+ outline: none;
+ border-color: var(--interactive-accent);
+ box-shadow: 0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
+}
+.event-modal-input:hover,
+.event-modal-textarea:hover,
+.event-modal-select:hover {
+ border-color: var(--background-modifier-border-hover);
+}
+.event-modal-textarea {
+ resize: vertical;
+ min-height: 80px;
+ font-family: inherit;
+}
+.event-modal-datetime-section {
+ background-color: var(--background-secondary);
+ padding: var(--sf-space-md);
+ border-radius: 8px;
+ border: 1px solid var(--background-modifier-border);
+}
+.event-modal-datetime-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+ margin-bottom: var(--sf-space-md);
+}
+.event-modal-input-group {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+.event-modal-input-icon {
+ position: absolute;
+ left: var(--sf-space-sm);
+ color: var(--text-muted);
+ font-size: 16px;
+ z-index: 1;
+ pointer-events: none;
+}
+.event-modal-input-group .event-modal-input {
+ padding-left: 36px;
+}
+.event-modal-all-day-container {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-sm);
+}
+.event-modal-checkbox {
+ width: 20px;
+ height: 20px;
+ accent-color: var(--interactive-accent);
+ cursor: pointer;
+}
+.event-modal-checkbox-label {
+ font-size: 14px;
+ color: var(--text-normal);
+ cursor: pointer;
+ user-select: none;
+}
+.event-modal-category-color-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+}
+.event-modal-category-container,
+.event-modal-color-container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sf-space-sm);
+}
+.event-modal-color-label {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+.event-modal-color-picker {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-sm);
+}
+.event-modal-color-input {
+ width: 50px;
+ height: 36px;
+ border: 2px solid var(--background-modifier-border);
+ border-radius: 6px;
+ cursor: pointer;
+ padding: 2px;
+}
+.event-modal-color-preview {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+ border: 2px solid var(--background-modifier-border);
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+.event-modal-color-preview:hover {
+ transform: scale(1.05);
+}
+.event-modal-recurrence-container {
+ margin-bottom: var(--sf-space-md);
+}
+.event-modal-recurrence-end-container {
+ background-color: var(--background-secondary);
+ padding: var(--sf-space-md);
+ border-radius: 8px;
+ border: 1px solid var(--background-modifier-border);
+ margin-top: var(--sf-space-md);
+}
+.event-modal-actions {
+ display: flex;
+ gap: var(--sf-space-sm);
+ justify-content: flex-end;
+ padding: var(--sf-space-lg);
+ border-top: 1px solid var(--background-modifier-border);
+ background-color: var(--background-secondary);
+ margin: 0 -var(--sf-space-lg) -var(--sf-space-lg);
+ border-radius: 0 0 12px 12px;
+}
+.event-modal-btn {
+ padding: var(--sf-space-sm) var(--sf-space-lg);
+ border-radius: 8px;
+ border: none;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 120px;
+}
+.event-modal-btn-primary {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+.event-modal-btn-primary:hover:not(:disabled) {
+ background-color: var(--interactive-accent-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(var(--interactive-accent-rgb), 0.3);
+}
+.event-modal-btn-secondary {
+ background-color: var(--background-modifier-border);
+ color: var(--text-normal);
+}
+.event-modal-btn-secondary:hover {
+ background-color: var(--background-modifier-border-hover);
+ transform: translateY(-1px);
+}
+.event-modal-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+@media (max-width: 768px) {
+ .event-modal {
+ max-width: 95vw;
+ margin: var(--sf-space-md);
+ }
+ .event-modal-datetime-grid,
+ .event-modal-category-color-grid {
+ grid-template-columns: 1fr;
+ gap: var(--sf-space-sm);
+ }
+ .event-modal-header {
+ padding: var(--sf-space-md);
+ }
+ .event-modal-form {
+ padding: var(--sf-space-md);
+ }
+ .event-modal-actions {
+ padding: var(--sf-space-md);
+ flex-direction: column;
+ }
+ .event-modal-btn {
+ width: 100%;
+ }
+}
+.event-modal {
+ animation: modalSlideIn 0.3s ease-out;
+}
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+.event-modal-input:focus,
+.event-modal-textarea:focus,
+.event-modal-select:focus {
+ transform: translateY(-1px);
+}
+.event-modal-input.error,
+.event-modal-textarea.error,
+.event-modal-select.error {
+ border-color: var(--text-error);
+ box-shadow: 0 0 0 3px rgba(var(--text-error-rgb), 0.1);
+}
+.event-modal-btn.loading {
+ position: relative;
+ color: transparent;
+}
+.event-modal-btn.loading::after {
+ content: "";
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ top: 50%;
+ left: 50%;
+ margin-left: -8px;
+ margin-top: -8px;
+ border: 2px solid var(--text-on-accent);
+ border-radius: 50%;
+ border-top-color: transparent;
+ animation: spin 1s ease-in-out infinite;
+}
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+.event-modal {
+ max-width: 500px;
+ max-height: 85vh;
+}
+.event-modal .modal-content {
+ border-radius: 8px;
+}
+.event-modal-header {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-sm);
+ padding: var(--sf-space-md) var(--sf-space-lg);
+ background: var(--interactive-accent);
+ color: var(--text-on-accent);
+ border-radius: 8px 8px 0 0;
+}
+.event-modal-header-icon {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ font-size: 14px;
+}
+.event-modal-header-text {
+ flex: 1;
+}
+.event-modal-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-on-accent);
+}
+.event-modal-form {
+ padding: var(--sf-space-md);
+}
+.event-modal-row {
+ display: flex;
+ gap: var(--sf-space-sm);
+ margin-bottom: var(--sf-space-sm);
+}
+.event-modal-row.event-modal-full-width {
+ display: block;
+}
+.event-modal-field-group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.event-modal-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-normal);
+ margin: 0;
+}
+.event-modal-label.required::after {
+ content: " *";
+ color: var(--text-error);
+}
+.event-modal-input,
+.event-modal-textarea,
+.event-modal-select {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ font-size: 13px;
+ background-color: var(--background-primary);
+ color: var(--text-normal);
+ transition: border-color 0.2s ease;
+ box-sizing: border-box;
+}
+.event-modal-input:focus,
+.event-modal-textarea:focus,
+.event-modal-select:focus {
+ outline: none;
+ border-color: var(--interactive-accent);
+}
+.event-modal-textarea {
+ resize: vertical;
+ min-height: 40px;
+ font-family: inherit;
+}
+.event-modal-checkbox {
+ width: 16px;
+ height: 16px;
+ accent-color: var(--interactive-accent);
+ cursor: pointer;
+ margin-right: 6px;
+}
+.event-modal-checkbox-label {
+ font-size: 13px;
+ color: var(--text-normal);
+ cursor: pointer;
+ user-select: none;
+ display: flex;
+ align-items: center;
+}
+.event-modal-color-picker-compact {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.event-modal-color-input {
+ width: 32px;
+ height: 28px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ cursor: pointer;
+ padding: 1px;
+}
+.event-modal-recurrence-end-row {
+ display: none;
+}
+.event-modal-recurrence-end-row.visible {
+ display: flex;
+}
+.event-modal-actions {
+ display: flex;
+ gap: var(--sf-space-sm);
+ justify-content: flex-end;
+ padding: var(--sf-space-md) var(--sf-space-lg);
+ border-top: 1px solid var(--background-modifier-border);
+ background-color: var(--background-secondary);
+ margin: 0 -var(--sf-space-md) -var(--sf-space-md);
+ border-radius: 0 0 8px 8px;
+}
+.event-modal-btn {
+ padding: 6px 16px;
+ border-radius: 4px;
+ border: none;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 80px;
+}
+.event-modal-btn-primary {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+.event-modal-btn-primary:hover:not(:disabled) {
+ background-color: var(--interactive-accent-hover);
+}
+.event-modal-btn-secondary {
+ background-color: var(--background-modifier-border);
+ color: var(--text-normal);
+}
+.event-modal-btn-secondary:hover {
+ background-color: var(--background-modifier-border-hover);
+}
+.event-modal-btn-danger {
+ background-color: var(--color-red, #dc3545);
+ color: white;
+}
+.event-modal-btn-danger:hover {
+ background-color: var(--color-red-hover, #c82333);
+ transform: translateY(-1px);
+}
+.event-modal-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+@media (max-width: 600px) {
+ .event-modal {
+ max-width: 95vw;
+ margin: var(--sf-space-sm);
+ }
+ .event-modal-row {
+ flex-direction: column;
+ gap: var(--sf-space-xs);
+ }
+ .event-modal-header {
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ }
+ .event-modal-form {
+ padding: var(--sf-space-sm);
+ }
+ .event-modal-actions {
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ flex-direction: row;
+ }
+ .event-modal-btn {
+ flex: 1;
+ min-width: auto;
+ }
+}
+.calendar-event-tooltip {
+ position: fixed;
+ z-index: 1000;
+ background: var(--background-primary);
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 8px;
+ padding: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ max-width: 280px;
+ font-size: 13px;
+ line-height: 1.4;
+ pointer-events: none;
+ opacity: 0;
+ transform: translateY(-4px);
+ transition: all 0.2s ease;
+}
+.calendar-event-tooltip .tooltip-title {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-normal);
+ margin-bottom: 6px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid var(--background-modifier-border);
+}
+.calendar-event-tooltip .tooltip-date,
+.calendar-event-tooltip .tooltip-time,
+.calendar-event-tooltip .tooltip-description,
+.calendar-event-tooltip .tooltip-location,
+.calendar-event-tooltip .tooltip-category {
+ margin: 3px 0;
+ color: var(--text-muted);
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+.calendar-event-tooltip .tooltip-description {
+ max-height: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+}
+.event-modal-datetime-section,
+.event-modal-datetime-grid,
+.event-modal-input-group,
+.event-modal-input-icon,
+.event-modal-all-day-container,
+.event-modal-category-color-grid,
+.event-modal-category-container,
+.event-modal-color-container,
+.event-modal-color-label,
+.event-modal-recurrence-container,
+.event-modal-recurrence-end-container {
+ display: none;
+}
/* styles/mcq.css */
.mcq-header-container {
diff --git a/styles/calendar.css b/styles/calendar.css
index ead463a..360dbd3 100644
--- a/styles/calendar.css
+++ b/styles/calendar.css
@@ -509,3 +509,1070 @@
padding: 1px 3px; /* Adjusted padding */
}
}
+
+/* Calendar Events Styles */
+
+/* Event tabs in calendar cells */
+.calendar-events-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 2px;
+ margin-top: 2px;
+ align-items: center;
+}
+
+.calendar-event-tab {
+ font-size: 9px;
+ padding: 1px 3px;
+ border-radius: 2px;
+ color: white;
+ cursor: pointer;
+ transition: transform 0.2s ease, opacity 0.2s ease;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 500;
+ line-height: 1.2;
+}
+
+.calendar-event-tab:hover {
+ transform: scale(1.05);
+ opacity: 0.9;
+ z-index: 10;
+}
+
+.calendar-events-more {
+ font-size: 8px;
+ padding: 1px 3px;
+ border-radius: 2px;
+ background-color: var(--text-muted);
+ color: var(--background-primary);
+ font-weight: 600;
+ min-width: 12px;
+ text-align: center;
+ line-height: 1.2;
+}
+
+.calendar-day.has-events {
+ border-left: 3px solid var(--interactive-accent);
+}
+
+/* Upcoming Events Section */
+.upcoming-events-wrapper {
+ margin-top: var(--sf-space-md);
+ padding-top: var(--sf-space-md);
+ border-top: 1px solid var(--background-modifier-border);
+}
+
+.upcoming-events-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: var(--sf-space-sm);
+ padding-bottom: var(--sf-space-sm);
+ border-bottom: 1px solid var(--background-modifier-border-hover);
+}
+
+.upcoming-events-title {
+ font-weight: 600;
+ font-size: 16px;
+ color: var(--text-normal);
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-xs);
+}
+
+.upcoming-events-subtitle {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 400;
+}
+
+.upcoming-events-container {
+ max-height: 300px;
+ overflow-y: auto;
+ padding-right: var(--sf-space-xs);
+}
+
+.upcoming-events-container::-webkit-scrollbar {
+ width: 4px;
+}
+
+.upcoming-events-container::-webkit-scrollbar-track {
+ background: var(--background-secondary);
+ border-radius: 2px;
+}
+
+.upcoming-events-container::-webkit-scrollbar-thumb {
+ background: var(--text-muted);
+ border-radius: 2px;
+}
+
+.upcoming-events-container::-webkit-scrollbar-thumb:hover {
+ background: var(--text-faint);
+}
+
+/* Empty State */
+.upcoming-events-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: var(--sf-space-lg) var(--sf-space-md);
+ text-align: center;
+ color: var(--text-muted);
+}
+
+.upcoming-events-empty-icon {
+ font-size: 24px;
+ margin-bottom: var(--sf-space-sm);
+ opacity: 0.5;
+}
+
+.upcoming-events-empty-text {
+ font-weight: 500;
+ margin-bottom: var(--sf-space-xs);
+ color: var(--text-muted);
+}
+
+.upcoming-events-empty-subtext {
+ font-size: 12px;
+ color: var(--text-faint);
+}
+
+/* Day Sections */
+.upcoming-events-day-section {
+ margin-bottom: var(--sf-space-md);
+ border-radius: var(--radius-m);
+ background-color: var(--background-secondary);
+ overflow: hidden;
+ transition: var(--sf-transition);
+}
+
+.upcoming-events-day-section:hover {
+ background-color: var(--background-modifier-hover);
+}
+
+.upcoming-events-day-section.today {
+ background-color: rgba(var(--interactive-accent-rgb), 0.1);
+ border: 1px solid rgba(var(--interactive-accent-rgb), 0.2);
+}
+
+.upcoming-events-day-section.tomorrow {
+ background-color: rgba(var(--interactive-accent-rgb), 0.05);
+}
+
+.upcoming-events-day-header {
+ display: flex;
+ align-items: center;
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ background-color: var(--background-modifier-border);
+ border-bottom: 1px solid var(--background-modifier-border-hover);
+}
+
+.upcoming-events-day-title {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-normal);
+}
+
+.upcoming-events-day-section.today .upcoming-events-day-title {
+ color: var(--interactive-accent);
+}
+
+/* Event Items */
+.upcoming-events-day-events {
+ padding: var(--sf-space-xs);
+}
+
+.upcoming-events-event-item {
+ display: flex;
+ align-items: flex-start;
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ margin: var(--sf-space-xs) 0;
+ border-radius: var(--radius-s);
+ cursor: pointer;
+ transition: var(--sf-transition);
+ background-color: var(--background-primary);
+ border: 1px solid transparent;
+}
+
+.upcoming-events-event-item:hover {
+ background-color: var(--background-modifier-hover);
+ border-color: var(--background-modifier-border);
+ transform: translateX(2px);
+}
+
+.upcoming-events-event-color {
+ width: 4px;
+ height: 100%;
+ min-height: 32px;
+ border-radius: 2px;
+ margin-right: var(--sf-space-sm);
+ flex-shrink: 0;
+ margin-top: 2px;
+ margin-bottom: 2px;
+}
+
+.upcoming-events-event-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.upcoming-events-event-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 2px;
+ gap: var(--sf-space-sm);
+}
+
+.upcoming-events-event-title {
+ font-weight: 500;
+ font-size: 13px;
+ color: var(--text-normal);
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.upcoming-events-event-time {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-weight: 400;
+ flex-shrink: 0;
+}
+
+.upcoming-events-event-details {
+ margin-bottom: 2px;
+}
+
+.upcoming-events-event-location {
+ font-size: 11px;
+ color: var(--text-muted);
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ margin-bottom: 2px;
+}
+
+.upcoming-events-event-description {
+ font-size: 11px;
+ color: var(--text-faint);
+ line-height: 1.3;
+ margin-bottom: 2px;
+}
+
+.upcoming-events-event-category {
+ font-size: 10px;
+ color: var(--text-muted);
+ background-color: var(--background-modifier-border);
+ padding: 1px 4px;
+ border-radius: 2px;
+ display: inline-block;
+ text-transform: capitalize;
+ font-weight: 500;
+}
+
+/* Event Modal Styles */
+.event-modal-datetime-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+ margin-bottom: var(--sf-space-md);
+}
+
+.event-modal-category-color-row {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+ margin-bottom: var(--sf-space-md);
+}
+
+.event-modal-buttons {
+ display: flex;
+ gap: var(--sf-space-sm);
+ justify-content: flex-end;
+ margin-top: var(--sf-space-lg);
+ padding-top: var(--sf-space-md);
+ border-top: 1px solid var(--background-modifier-border);
+}
+
+.event-modal-buttons button {
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ border-radius: var(--radius-s);
+ border: 1px solid var(--background-modifier-border);
+ cursor: pointer;
+ transition: var(--sf-transition);
+ font-weight: 500;
+}
+
+.event-modal-buttons button.mod-cta {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+ border-color: var(--interactive-accent);
+}
+
+.event-modal-buttons button.mod-cta:hover {
+ background-color: var(--interactive-accent-hover);
+}
+
+.event-modal-buttons button:not(.mod-cta) {
+ background-color: var(--background-secondary);
+ color: var(--text-normal);
+}
+
+.event-modal-buttons button:not(.mod-cta):hover {
+ background-color: var(--background-modifier-hover);
+}
+
+.event-modal-buttons button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+ .event-modal-datetime-row,
+ .event-modal-category-color-row {
+ grid-template-columns: 1fr;
+ gap: var(--sf-space-sm);
+ }
+
+ .upcoming-events-container {
+ max-height: 200px;
+ }
+
+ .calendar-event-tab {
+ font-size: 8px;
+ padding: 1px 2px;
+ }
+
+ .upcoming-events-event-item {
+ padding: var(--sf-space-xs) var(--sf-space-sm);
+ }
+
+ .upcoming-events-event-title {
+ font-size: 12px;
+ }
+
+ .upcoming-events-event-time {
+ font-size: 10px;
+ }
+}
+
+/* Day hover add event button */
+.calendar-day {
+ position: relative;
+}
+
+.calendar-day-add-event {
+ position: absolute;
+ top: 2px;
+ right: 2px;
+ width: 16px;
+ height: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+ border-radius: 50%;
+ font-size: 10px;
+ cursor: pointer;
+ opacity: 0;
+ transform: scale(0.8);
+ transition: all 0.2s ease;
+ z-index: 20;
+ box-shadow: var(--sf-shadow-sm);
+}
+
+.calendar-day:hover .calendar-day-add-event {
+ opacity: 1;
+ transform: scale(1);
+}
+
+.calendar-day-add-event:hover {
+ background-color: var(--interactive-accent-hover);
+ transform: scale(1.1);
+ box-shadow: var(--sf-shadow-md);
+}
+
+.calendar-day.empty:hover .calendar-day-add-event {
+ opacity: 0.7;
+}
+
+/* Adjust positioning when there are other elements */
+.calendar-day.has-reviews .calendar-day-add-event {
+ top: 28px;
+}
+
+.calendar-day.has-events .calendar-day-add-event {
+ top: 28px;
+}
+
+.calendar-day.has-reviews.has-events .calendar-day-add-event {
+ top: 44px;
+}
+
+/* Enhanced Event Modal Styles */
+.event-modal {
+ max-width: 600px;
+ max-height: 90vh;
+ overflow-y: auto;
+}
+
+.event-modal .modal-content {
+ border-radius: 12px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+/* Header Styles */
+.event-modal-header {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-md);
+ padding: var(--sf-space-lg) var(--sf-space-lg) var(--sf-space-md) var(--sf-space-lg);
+ background: linear-gradient(135deg, var(--interactive-accent) 0%, var(--interactive-accent-hover) 100%);
+ color: var(--text-on-accent);
+ border-radius: 12px 12px 0 0;
+}
+
+.event-modal-header-icon {
+ width: 40px;
+ height: 40px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ font-size: 20px;
+}
+
+.event-modal-header-text {
+ flex: 1;
+}
+
+.event-modal-title {
+ margin: 0 0 var(--sf-space-xs) 0;
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--text-on-accent);
+}
+
+.event-modal-subtitle {
+ margin: 0;
+ font-size: 14px;
+ opacity: 0.9;
+ color: var(--text-on-accent);
+}
+
+/* Form Styles */
+.event-modal-form {
+ padding: var(--sf-space-lg);
+}
+
+.event-modal-section {
+ margin-bottom: var(--sf-space-lg);
+}
+
+.event-modal-label {
+ display: block;
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-normal);
+ margin-bottom: var(--sf-space-sm);
+}
+
+.event-modal-label.required::after {
+ content: " *";
+ color: var(--text-error);
+}
+
+.event-modal-input,
+.event-modal-textarea,
+.event-modal-select {
+ width: 100%;
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ border: 2px solid var(--background-modifier-border);
+ border-radius: 8px;
+ font-size: 14px;
+ background-color: var(--background-primary);
+ color: var(--text-normal);
+ transition: all 0.2s ease;
+ box-sizing: border-box;
+}
+
+.event-modal-input:focus,
+.event-modal-textarea:focus,
+.event-modal-select:focus {
+ outline: none;
+ border-color: var(--interactive-accent);
+ box-shadow: 0 0 0 3px rgba(var(--interactive-accent-rgb), 0.1);
+}
+
+.event-modal-input:hover,
+.event-modal-textarea:hover,
+.event-modal-select:hover {
+ border-color: var(--background-modifier-border-hover);
+}
+
+.event-modal-textarea {
+ resize: vertical;
+ min-height: 80px;
+ font-family: inherit;
+}
+
+/* Date & Time Section */
+.event-modal-datetime-section {
+ background-color: var(--background-secondary);
+ padding: var(--sf-space-md);
+ border-radius: 8px;
+ border: 1px solid var(--background-modifier-border);
+}
+
+.event-modal-datetime-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+ margin-bottom: var(--sf-space-md);
+}
+
+.event-modal-input-group {
+ position: relative;
+ display: flex;
+ align-items: center;
+}
+
+.event-modal-input-icon {
+ position: absolute;
+ left: var(--sf-space-sm);
+ color: var(--text-muted);
+ font-size: 16px;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.event-modal-input-group .event-modal-input {
+ padding-left: 36px;
+}
+
+/* All Day Toggle */
+.event-modal-all-day-container {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-sm);
+}
+
+.event-modal-checkbox {
+ width: 20px;
+ height: 20px;
+ accent-color: var(--interactive-accent);
+ cursor: pointer;
+}
+
+.event-modal-checkbox-label {
+ font-size: 14px;
+ color: var(--text-normal);
+ cursor: pointer;
+ user-select: none;
+}
+
+/* Category & Color Section */
+.event-modal-category-color-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: var(--sf-space-md);
+}
+
+.event-modal-category-container,
+.event-modal-color-container {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sf-space-sm);
+}
+
+.event-modal-color-label {
+ font-size: 12px;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.event-modal-color-picker {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-sm);
+}
+
+.event-modal-color-input {
+ width: 50px;
+ height: 36px;
+ border: 2px solid var(--background-modifier-border);
+ border-radius: 6px;
+ cursor: pointer;
+ padding: 2px;
+}
+
+.event-modal-color-preview {
+ width: 36px;
+ height: 36px;
+ border-radius: 6px;
+ border: 2px solid var(--background-modifier-border);
+ cursor: pointer;
+ transition: transform 0.2s ease;
+}
+
+.event-modal-color-preview:hover {
+ transform: scale(1.05);
+}
+
+/* Recurrence Section */
+.event-modal-recurrence-container {
+ margin-bottom: var(--sf-space-md);
+}
+
+.event-modal-recurrence-end-container {
+ background-color: var(--background-secondary);
+ padding: var(--sf-space-md);
+ border-radius: 8px;
+ border: 1px solid var(--background-modifier-border);
+ margin-top: var(--sf-space-md);
+}
+
+/* Action Buttons */
+.event-modal-actions {
+ display: flex;
+ gap: var(--sf-space-sm);
+ justify-content: flex-end;
+ padding: var(--sf-space-lg);
+ border-top: 1px solid var(--background-modifier-border);
+ background-color: var(--background-secondary);
+ margin: 0 -var(--sf-space-lg) -var(--sf-space-lg);
+ border-radius: 0 0 12px 12px;
+}
+
+.event-modal-btn {
+ padding: var(--sf-space-sm) var(--sf-space-lg);
+ border-radius: 8px;
+ border: none;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 120px;
+}
+
+.event-modal-btn-primary {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+
+.event-modal-btn-primary:hover:not(:disabled) {
+ background-color: var(--interactive-accent-hover);
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(var(--interactive-accent-rgb), 0.3);
+}
+
+.event-modal-btn-secondary {
+ background-color: var(--background-modifier-border);
+ color: var(--text-normal);
+}
+
+.event-modal-btn-secondary:hover {
+ background-color: var(--background-modifier-border-hover);
+ transform: translateY(-1px);
+}
+
+.event-modal-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ transform: none;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .event-modal {
+ max-width: 95vw;
+ margin: var(--sf-space-md);
+ }
+
+ .event-modal-datetime-grid,
+ .event-modal-category-color-grid {
+ grid-template-columns: 1fr;
+ gap: var(--sf-space-sm);
+ }
+
+ .event-modal-header {
+ padding: var(--sf-space-md);
+ }
+
+ .event-modal-form {
+ padding: var(--sf-space-md);
+ }
+
+ .event-modal-actions {
+ padding: var(--sf-space-md);
+ flex-direction: column;
+ }
+
+ .event-modal-btn {
+ width: 100%;
+ }
+}
+
+/* Animations */
+.event-modal {
+ animation: modalSlideIn 0.3s ease-out;
+}
+
+@keyframes modalSlideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-20px) scale(0.95);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0) scale(1);
+ }
+}
+
+/* Focus States */
+.event-modal-input:focus,
+.event-modal-textarea:focus,
+.event-modal-select:focus {
+ transform: translateY(-1px);
+}
+
+/* Success/Error States */
+.event-modal-input.error,
+.event-modal-textarea.error,
+.event-modal-select.error {
+ border-color: var(--text-error);
+ box-shadow: 0 0 0 3px rgba(var(--text-error-rgb), 0.1);
+}
+
+/* Loading State */
+.event-modal-btn.loading {
+ position: relative;
+ color: transparent;
+}
+
+.event-modal-btn.loading::after {
+ content: "";
+ position: absolute;
+ width: 16px;
+ height: 16px;
+ top: 50%;
+ left: 50%;
+ margin-left: -8px;
+ margin-top: -8px;
+ border: 2px solid var(--text-on-accent);
+ border-radius: 50%;
+ border-top-color: transparent;
+ animation: spin 1s ease-in-out infinite;
+}
+
+@keyframes spin {
+ to { transform: rotate(360deg); }
+}
+
+/* Compact Event Modal Styles */
+.event-modal {
+ max-width: 500px;
+ max-height: 85vh;
+}
+
+.event-modal .modal-content {
+ border-radius: 8px;
+}
+
+/* Compact Header */
+.event-modal-header {
+ display: flex;
+ align-items: center;
+ gap: var(--sf-space-sm);
+ padding: var(--sf-space-md) var(--sf-space-lg);
+ background: var(--interactive-accent);
+ color: var(--text-on-accent);
+ border-radius: 8px 8px 0 0;
+}
+
+.event-modal-header-icon {
+ width: 24px;
+ height: 24px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: rgba(255, 255, 255, 0.2);
+ border-radius: 50%;
+ font-size: 14px;
+}
+
+.event-modal-header-text {
+ flex: 1;
+}
+
+.event-modal-title {
+ margin: 0;
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-on-accent);
+}
+
+/* Compact Form Layout */
+.event-modal-form {
+ padding: var(--sf-space-md);
+}
+
+.event-modal-row {
+ display: flex;
+ gap: var(--sf-space-sm);
+ margin-bottom: var(--sf-space-sm);
+}
+
+.event-modal-row.event-modal-full-width {
+ display: block;
+}
+
+.event-modal-field-group {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.event-modal-label {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text-normal);
+ margin: 0;
+}
+
+.event-modal-label.required::after {
+ content: " *";
+ color: var(--text-error);
+}
+
+.event-modal-input,
+.event-modal-textarea,
+.event-modal-select {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ font-size: 13px;
+ background-color: var(--background-primary);
+ color: var(--text-normal);
+ transition: border-color 0.2s ease;
+ box-sizing: border-box;
+}
+
+.event-modal-input:focus,
+.event-modal-textarea:focus,
+.event-modal-select:focus {
+ outline: none;
+ border-color: var(--interactive-accent);
+}
+
+.event-modal-textarea {
+ resize: vertical;
+ min-height: 40px;
+ font-family: inherit;
+}
+
+/* Compact Checkbox */
+.event-modal-checkbox {
+ width: 16px;
+ height: 16px;
+ accent-color: var(--interactive-accent);
+ cursor: pointer;
+ margin-right: 6px;
+}
+
+.event-modal-checkbox-label {
+ font-size: 13px;
+ color: var(--text-normal);
+ cursor: pointer;
+ user-select: none;
+ display: flex;
+ align-items: center;
+}
+
+/* Compact Color Picker */
+.event-modal-color-picker-compact {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.event-modal-color-input {
+ width: 32px;
+ height: 28px;
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 4px;
+ cursor: pointer;
+ padding: 1px;
+}
+
+/* Recurrence End Date Row */
+.event-modal-recurrence-end-row {
+ display: none;
+}
+
+.event-modal-recurrence-end-row.visible {
+ display: flex;
+}
+
+/* Compact Actions */
+.event-modal-actions {
+ display: flex;
+ gap: var(--sf-space-sm);
+ justify-content: flex-end;
+ padding: var(--sf-space-md) var(--sf-space-lg);
+ border-top: 1px solid var(--background-modifier-border);
+ background-color: var(--background-secondary);
+ margin: 0 -var(--sf-space-md) -var(--sf-space-md);
+ border-radius: 0 0 8px 8px;
+}
+
+.event-modal-btn {
+ padding: 6px 16px;
+ border-radius: 4px;
+ border: none;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ min-width: 80px;
+}
+
+.event-modal-btn-primary {
+ background-color: var(--interactive-accent);
+ color: var(--text-on-accent);
+}
+
+.event-modal-btn-primary:hover:not(:disabled) {
+ background-color: var(--interactive-accent-hover);
+}
+
+.event-modal-btn-secondary {
+ background-color: var(--background-modifier-border);
+ color: var(--text-normal);
+}
+
+.event-modal-btn-secondary:hover {
+ background-color: var(--background-modifier-border-hover);
+}
+
+.event-modal-btn-danger {
+ background-color: var(--color-red, #dc3545);
+ color: white;
+}
+
+.event-modal-btn-danger:hover {
+ background-color: var(--color-red-hover, #c82333);
+ transform: translateY(-1px);
+}
+
+.event-modal-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+/* Responsive Compact Design */
+@media (max-width: 600px) {
+ .event-modal {
+ max-width: 95vw;
+ margin: var(--sf-space-sm);
+ }
+
+ .event-modal-row {
+ flex-direction: column;
+ gap: var(--sf-space-xs);
+ }
+
+ .event-modal-header {
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ }
+
+ .event-modal-form {
+ padding: var(--sf-space-sm);
+ }
+
+ .event-modal-actions {
+ padding: var(--sf-space-sm) var(--sf-space-md);
+ flex-direction: row;
+ }
+
+ .event-modal-btn {
+ flex: 1;
+ min-width: auto;
+ }
+}
+
+/* Event Tooltip Styles */
+.calendar-event-tooltip {
+ position: fixed;
+ z-index: 1000;
+ background: var(--background-primary);
+ border: 1px solid var(--background-modifier-border);
+ border-radius: 8px;
+ padding: 12px;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
+ max-width: 280px;
+ font-size: 13px;
+ line-height: 1.4;
+ pointer-events: none;
+ opacity: 0;
+ transform: translateY(-4px);
+ transition: all 0.2s ease;
+}
+
+.calendar-event-tooltip .tooltip-title {
+ font-weight: 600;
+ font-size: 14px;
+ color: var(--text-normal);
+ margin-bottom: 6px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid var(--background-modifier-border);
+}
+
+.calendar-event-tooltip .tooltip-date,
+.calendar-event-tooltip .tooltip-time,
+.calendar-event-tooltip .tooltip-description,
+.calendar-event-tooltip .tooltip-location,
+.calendar-event-tooltip .tooltip-category {
+ margin: 3px 0;
+ color: var(--text-muted);
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.calendar-event-tooltip .tooltip-description {
+ max-height: 60px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+}
+
+/* Remove some of the previous enhanced styles to avoid conflicts */
+.event-modal-datetime-section,
+.event-modal-datetime-grid,
+.event-modal-input-group,
+.event-modal-input-icon,
+.event-modal-all-day-container,
+.event-modal-category-color-grid,
+.event-modal-category-container,
+.event-modal-color-container,
+.event-modal-color-label,
+.event-modal-recurrence-container,
+.event-modal-recurrence-end-container {
+ display: none;
+}
diff --git a/ui/calendar-view.ts b/ui/calendar-view.ts
index daf01c4..72441b4 100644
--- a/ui/calendar-view.ts
+++ b/ui/calendar-view.ts
@@ -1,8 +1,11 @@
import { Notice, setIcon } from "obsidian";
import SpaceforgePlugin from "../main";
import { ReviewSchedule } from "../models/review-schedule";
+import { CalendarEvent } from "../models/calendar-event";
import { DateUtils } from "../utils/dates";
import { EstimationUtils } from "../utils/estimation";
+import { UpcomingEvents } from "./upcoming-events";
+import { EventModal } from "./event-modal";
/**
* Interface for grouped reviews by date
@@ -13,6 +16,14 @@ interface DateReviews {
totalTime: number;
}
+/**
+ * Interface for grouped events by date
+ */
+interface DateEvents {
+ timestamp: number;
+ events: CalendarEvent[];
+}
+
/**
* Calendar view component for review schedule
*/
@@ -37,10 +48,21 @@ export class CalendarView {
*/
reviewsByDate: Map = new Map();
+ /**
+ * Events grouped by date
+ */
+ eventsByDate: Map = new Map();
+
// Persistent UI elements
private calendarHeaderEl: HTMLElement | null = null;
private monthTitleEl: HTMLElement | null = null;
private calendarGridEl: HTMLElement | null = null;
+ private upcomingEventsEl: HTMLElement | null = null;
+ private upcomingEventsComponent: UpcomingEvents | null = null;
+
+ // Tooltip elements
+ private tooltipEl: HTMLElement | null = null;
+ private tooltipTimeout: NodeJS.Timeout | null = null;
/**
* Initialize calendar view
@@ -63,10 +85,16 @@ export class CalendarView {
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
}
+
+ // Render upcoming events
+ if (this.upcomingEventsComponent) {
+ await this.upcomingEventsComponent.render();
+ }
}
private ensureCalendarBaseStructure(): void {
@@ -103,12 +131,25 @@ export class CalendarView {
this.currentDate = new Date();
this.render();
});
+
+ const addEventBtn = this.calendarHeaderEl.createDiv("calendar-add-event-btn");
+ setIcon(addEventBtn, "plus");
+ addEventBtn.addEventListener("click", () => {
+ this.openCreateEventModal();
+ });
}
if (!this.calendarGridEl || !calendarContainer.contains(this.calendarGridEl)) {
this.calendarGridEl?.remove();
this.calendarGridEl = calendarContainer.createDiv("calendar-grid");
}
+
+ // Create or update upcoming events container
+ if (!this.upcomingEventsEl || !calendarContainer.contains(this.upcomingEventsEl)) {
+ this.upcomingEventsEl?.remove();
+ this.upcomingEventsEl = calendarContainer.createDiv("upcoming-events-wrapper");
+ this.upcomingEventsComponent = new UpcomingEvents(this.upcomingEventsEl, this.plugin);
+ }
}
/**
@@ -151,6 +192,43 @@ export class CalendarView {
}
}
}
+
+ /**
+ * Load and organize event data by date
+ */
+ async loadEventsData(): Promise {
+ this.eventsByDate = new Map();
+
+ if (!this.plugin.settings.enableCalendarEvents || !this.plugin.calendarEventService) {
+ return;
+ }
+
+ 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()
+ );
+
+ for (const event of eventsInRange) {
+ const eventDayStart = DateUtils.startOfUTCDay(new Date(event.date));
+ const dateKey = eventDayStart.toString();
+
+ if (!this.eventsByDate.has(dateKey)) {
+ this.eventsByDate.set(dateKey, {
+ timestamp: eventDayStart,
+ events: []
+ });
+ }
+
+ const dateEvents = this.eventsByDate.get(dateKey);
+ if (dateEvents) {
+ dateEvents.events.push(event);
+ }
+ }
+ }
/**
* Render or update the calendar grid content
@@ -207,6 +285,9 @@ 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");
@@ -237,6 +318,59 @@ export class CalendarView {
else if (dateReviews.notes.length > 5) dayCell.addClass("medium-load");
else dayCell.addClass("light-load");
}
+
+ // 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
+ const eventColor = this.plugin.calendarEventService?.getEventColor(event) || '#95A5A6';
+ eventTab.style.backgroundColor = eventColor;
+
+ // Add hover handler for tooltip
+ eventTab.addEventListener("mouseenter", (e) => {
+ this.showEventTooltip(eventTab, event);
+ });
+
+ 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}`);
+ }
+ }
+
+ // Add hover plus button for creating events
+ const addEventBtn = dayCell.createDiv("calendar-day-add-event");
+ setIcon(addEventBtn, "plus");
+ addEventBtn.addEventListener("click", (e) => {
+ e.stopPropagation(); // Prevent day cell click
+ this.openCreateEventModalForDate(currentDateObj);
+ });
dayOfMonth++;
} else {
dayCell.addClass("empty");
@@ -278,4 +412,233 @@ export class CalendarView {
today.getDate() === day
);
}
+
+ /**
+ * Show event details modal
+ *
+ * @param event Event to show details for
+ */
+ showEventDetails(event: CalendarEvent): void {
+ // 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'
+ });
+
+ 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);
+ }
+
+ /**
+ * Open create event modal
+ */
+ openCreateEventModal(): void {
+ const modal = new EventModal(
+ this.plugin.app,
+ this.plugin,
+ null,
+ async (savedEvent) => {
+ // Refresh the calendar view after saving
+ await this.render();
+ }
+ );
+ modal.open();
+ }
+
+ /**
+ * Open create event modal for a specific date
+ *
+ * @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 any || 'personal',
+ recurrence: 'none' as any
+ };
+
+ const modal = new EventModal(
+ this.plugin.app,
+ this.plugin,
+ null,
+ async (savedEvent) => {
+ // Refresh the calendar view after saving
+ await this.render();
+ }
+ );
+ modal.open();
+
+ // Pre-fill the date in the modal after it opens
+ setTimeout(() => {
+ const dateInput = modal.contentEl.querySelector('input[type="date"]') as HTMLInputElement;
+ if (dateInput) {
+ dateInput.value = date.toISOString().split('T')[0];
+ }
+ }, 100);
+ }
+
+ /**
+ * Open edit event modal
+ *
+ * @param event Event to edit
+ */
+ openEditEventModal(event: CalendarEvent): void {
+ const modal = new EventModal(
+ this.plugin.app,
+ this.plugin,
+ event,
+ async (savedEvent) => {
+ // Refresh the calendar view after saving
+ await this.render();
+ }
+ );
+ modal.open();
+ }
+
+ /**
+ * Clean up resources when the calendar view is destroyed
+ */
+ destroy(): void {
+ this.hideEventTooltip();
+
+ // Clear other references
+ this.calendarHeaderEl = null;
+ this.monthTitleEl = null;
+ this.calendarGridEl = null;
+ this.upcomingEventsEl = null;
+ this.upcomingEventsComponent = null;
+ }
+
+ /**
+ * Show event tooltip
+ *
+ * @param targetEl Element to show tooltip for
+ * @param event Event to show details for
+ */
+ showEventTooltip(targetEl: HTMLElement, event: CalendarEvent): void {
+ // Clear any existing timeout
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ }
+
+ // Small delay before showing tooltip to avoid flickering
+ this.tooltipTimeout = setTimeout(() => {
+ this.createEventTooltip(targetEl, event);
+ }, 300);
+ }
+
+ /**
+ * Hide event tooltip
+ */
+ hideEventTooltip(): void {
+ // Clear any pending tooltip
+ if (this.tooltipTimeout) {
+ clearTimeout(this.tooltipTimeout);
+ this.tooltipTimeout = null;
+ }
+
+ // Remove existing tooltip
+ if (this.tooltipEl) {
+ this.tooltipEl.remove();
+ this.tooltipEl = null;
+ }
+ }
+
+ /**
+ * Create and show the tooltip element
+ *
+ * @param targetEl Element to position tooltip relative to
+ * @param event Event to show details for
+ */
+ private createEventTooltip(targetEl: HTMLElement, event: CalendarEvent): void {
+ // Remove existing tooltip if any
+ this.hideEventTooltip();
+
+ // Create tooltip element
+ this.tooltipEl = document.createElement("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'
+ });
+
+ let tooltipContent = `${event.title}
`;
+ tooltipContent += `š
${dateStr}
`;
+
+ if (event.time) {
+ tooltipContent += `š ${event.time}
`;
+ }
+
+ if (event.description) {
+ tooltipContent += `š ${event.description}
`;
+ }
+
+ if (event.location) {
+ tooltipContent += `š ${event.location}
`;
+ }
+
+ tooltipContent += `š·ļø ${event.category}
`;
+
+ this.tooltipEl.innerHTML = tooltipContent;
+
+ // 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) {
+ left = window.innerWidth - this.tooltipEl.offsetWidth - 8;
+ }
+ 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`;
+
+ // Add fade-in animation
+ this.tooltipEl.style.opacity = '0';
+ this.tooltipEl.style.transform = 'translateY(-4px)';
+
+ requestAnimationFrame(() => {
+ if (this.tooltipEl) {
+ this.tooltipEl.style.opacity = '1';
+ this.tooltipEl.style.transform = 'translateY(0)';
+ }
+ });
+ }
}
diff --git a/ui/event-modal.ts b/ui/event-modal.ts
new file mode 100644
index 0000000..24d8d64
--- /dev/null
+++ b/ui/event-modal.ts
@@ -0,0 +1,436 @@
+import { App, Modal, Setting, Notice, setIcon } from "obsidian";
+import SpaceforgePlugin from "../main";
+import { CalendarEvent, EventCategory, EventRecurrence, EVENT_CATEGORY_COLORS } from "../models/calendar-event";
+
+/**
+ * Modal for creating or editing calendar events
+ */
+export class EventModal extends Modal {
+ plugin: SpaceforgePlugin;
+ event: CalendarEvent | null;
+ onSave: (event: CalendarEvent) => void;
+
+ // Form elements
+ titleInput: HTMLInputElement;
+ descriptionInput: HTMLTextAreaElement;
+ dateInput: HTMLInputElement;
+ timeInput: HTMLInputElement;
+ locationInput: HTMLInputElement;
+ categorySelect: HTMLSelectElement;
+ colorInput: HTMLInputElement;
+ isAllDayToggle: HTMLInputElement;
+ recurrenceSelect: HTMLSelectElement;
+ recurrenceEndDateInput: HTMLInputElement;
+
+ constructor(app: App, plugin: SpaceforgePlugin, event: CalendarEvent | null = null, onSave: (event: CalendarEvent) => void) {
+ super(app);
+ this.plugin = plugin;
+ this.event = event;
+ this.onSave = onSave;
+ }
+
+ onOpen() {
+ 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");
+ headerText.createEl("h2", {
+ text: this.event ? "Edit Event" : "New Event",
+ cls: "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", {
+ type: "text",
+ cls: "event-modal-input",
+ placeholder: "Event title..."
+ });
+ if (this.event?.title) {
+ this.titleInput.value = this.event.title;
+ }
+ this.titleInput.addEventListener("input", () => this.validateForm());
+
+ // Date & Time row
+ 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", {
+ type: "date",
+ cls: "event-modal-input"
+ });
+ if (this.event?.date) {
+ const date = new Date(this.event.date);
+ this.dateInput.value = date.toISOString().split('T')[0];
+ } else {
+ 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", {
+ type: "time",
+ cls: "event-modal-input"
+ });
+ if (this.event?.time) {
+ this.timeInput.value = this.event.time;
+ }
+ this.timeInput.addEventListener("change", () => this.updateAllDayToggle());
+
+ // All-day toggle row
+ const allDayRow = formContainer.createDiv("event-modal-row");
+ const allDayGroup = allDayRow.createDiv("event-modal-field-group");
+ 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", {
+ text: "All-day event",
+ cls: "event-modal-checkbox-label"
+ });
+
+ // Location row
+ 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", {
+ type: "text",
+ cls: "event-modal-input",
+ placeholder: "Location (optional)..."
+ });
+ if (this.event?.location) {
+ this.locationInput.value = this.event.location;
+ }
+
+ // Category & Color row
+ 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", {
+ cls: "event-modal-select"
+ });
+
+ Object.values(EventCategory).forEach(category => {
+ 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", {
+ type: "color",
+ cls: "event-modal-color-input"
+ }) as HTMLInputElement;
+
+ if (this.event?.color) {
+ this.colorInput.value = this.event.color;
+ } else {
+ this.updateColorFromCategory();
+ }
+
+ // Description row (full width)
+ 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", {
+ cls: "event-modal-textarea",
+ placeholder: "Event details (optional)..."
+ });
+ this.descriptionInput.rows = 2;
+ if (this.event?.description) {
+ this.descriptionInput.value = this.event.description;
+ }
+
+ // Recurrence row
+ 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", {
+ cls: "event-modal-select"
+ });
+
+ Object.values(EventRecurrence).forEach(recurrence => {
+ 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", {
+ type: "date",
+ cls: "event-modal-input"
+ });
+ if (this.event?.recurrenceEndDate) {
+ const date = new Date(this.event.recurrenceEndDate);
+ this.recurrenceEndDateInput.value = date.toISOString().split('T')[0];
+ }
+
+ // Enhanced buttons section
+ const buttonContainer = contentEl.createDiv("event-modal-actions");
+
+ 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", {
+ text: "Delete",
+ cls: "event-modal-btn event-modal-btn-danger"
+ });
+ deleteButton.addEventListener("click", () => this.deleteEvent());
+ }
+
+ 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());
+
+ // Initial setup
+ this.validateForm();
+ this.updateAllDayToggle();
+ this.toggleTimeInput();
+ this.toggleRecurrenceEndDate();
+ this.updateColorPreview();
+ }
+
+ onClose() {
+ const { contentEl } = this;
+ contentEl.empty();
+ }
+
+ /**
+ * Validate form and enable/disable save button
+ */
+ 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;
+ }
+ }
+
+ /**
+ * Update color based on selected category
+ */
+ private updateColorFromCategory(): void {
+ const category = this.categorySelect.value as EventCategory;
+ const categoryColor = EVENT_CATEGORY_COLORS[category];
+ this.colorInput.value = categoryColor;
+ this.updateColorPreview();
+ }
+
+ /**
+ * Update color preview
+ */
+ private updateColorPreview(): void {
+ const colorPreview = this.contentEl.querySelector(".event-modal-color-preview") as HTMLElement;
+ if (colorPreview) {
+ colorPreview.style.backgroundColor = this.colorInput.value;
+ }
+ }
+
+ /**
+ * Toggle time input based on all-day setting
+ */
+ 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 = "";
+ }
+ }
+
+ /**
+ * Update all-day toggle based on time input
+ */
+ private updateAllDayToggle(): void {
+ const hasTime = this.timeInput.value !== "";
+ if (hasTime && this.isAllDayToggle.checked) {
+ this.isAllDayToggle.checked = false;
+ this.toggleTimeInput();
+ }
+ }
+
+ /**
+ * Toggle recurrence end date based on recurrence setting
+ */
+ 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 = "";
+ }
+ }
+
+ /**
+ * Get display text for recurrence option
+ */
+ private getRecurrenceDisplayText(recurrence: EventRecurrence): string {
+ switch (recurrence) {
+ case EventRecurrence.None: return "None";
+ case EventRecurrence.Daily: return "Daily";
+ case EventRecurrence.Weekly: return "Weekly";
+ case EventRecurrence.Monthly: return "Monthly";
+ case EventRecurrence.Yearly: return "Yearly";
+ default: return recurrence;
+ }
+ }
+
+ /**
+ * Delete the event
+ */
+ 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");
+ }
+ } catch (error) {
+ console.error("Error deleting event:", error);
+ new Notice("Error deleting event");
+ }
+ }
+
+ /**
+ * Save the event
+ */
+ private async saveEvent(): Promise {
+ try {
+ const title = this.titleInput.value.trim();
+ const description = this.descriptionInput.value.trim();
+ const date = new Date(this.dateInput.value);
+ const time = this.timeInput.value;
+ const location = this.locationInput.value.trim();
+ const category = this.categorySelect.value as EventCategory;
+ const color = this.colorInput.value;
+ const isAllDay = this.isAllDayToggle.checked;
+ const recurrence = this.recurrenceSelect.value as EventRecurrence;
+ const recurrenceEndDateText = this.recurrenceEndDateInput.value;
+ const recurrenceEndDate = recurrenceEndDateText ? new Date(recurrenceEndDateText).getTime() : undefined;
+
+ if (!title || !this.dateInput.value) {
+ new Notice("Please fill in all required fields");
+ return;
+ }
+
+ let eventData: Omit;
+
+ if (this.event) {
+ // Update existing event
+ eventData = {
+ ...this.event,
+ title,
+ description: description || undefined,
+ date: date.getTime(),
+ time: time || undefined,
+ location: location || undefined,
+ category,
+ color: color !== EVENT_CATEGORY_COLORS[category] ? color : undefined,
+ isAllDay,
+ recurrence,
+ recurrenceEndDate
+ };
+
+ const updatedEvent = this.plugin.calendarEventService?.updateEvent(this.event.id, eventData);
+ if (updatedEvent) {
+ this.onSave(updatedEvent);
+ new Notice("Event updated successfully");
+ }
+ } else {
+ // Create new event
+ eventData = {
+ title,
+ description: description || undefined,
+ date: date.getTime(),
+ time: time || undefined,
+ location: location || undefined,
+ category,
+ color: color !== EVENT_CATEGORY_COLORS[category] ? color : undefined,
+ isAllDay,
+ recurrence,
+ recurrenceEndDate
+ };
+
+ const newEvent = this.plugin.calendarEventService?.createEvent(eventData);
+ if (newEvent) {
+ this.onSave(newEvent);
+ new Notice("Event created successfully");
+ }
+ }
+
+ // Save plugin data
+ await this.plugin.savePluginData();
+
+ this.close();
+ } catch (error) {
+ console.error("Error saving event:", error);
+ new Notice("Error saving event");
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/settings-tab.ts b/ui/settings-tab.ts
index e6d0a7f..2f7795c 100644
--- a/ui/settings-tab.ts
+++ b/ui/settings-tab.ts
@@ -106,6 +106,7 @@ export class SpaceforgeSettingTab extends PluginSettingTab {
pomodoroUserOverrideHours: this.plugin.pluginState.pomodoroUserOverrideHours ?? 0,
pomodoroUserOverrideMinutes: this.plugin.pluginState.pomodoroUserOverrideMinutes ?? 0,
pomodoroUserAddToEstimation: this.plugin.pluginState.pomodoroUserAddToEstimation ?? false,
+ calendarEvents: this.plugin.pluginState.calendarEvents ?? {},
};
const dataToExport: SpaceforgePluginData = {
diff --git a/ui/upcoming-events.ts b/ui/upcoming-events.ts
new file mode 100644
index 0000000..19e3ef5
--- /dev/null
+++ b/ui/upcoming-events.ts
@@ -0,0 +1,283 @@
+import { Notice, setIcon } from "obsidian";
+import SpaceforgePlugin from "../main";
+import { UpcomingEvent } from "../models/calendar-event";
+import { DateUtils } from "../utils/dates";
+
+/**
+ * Upcoming events component for calendar view
+ */
+export class UpcomingEvents {
+ /**
+ * Reference to the main plugin
+ */
+ plugin: SpaceforgePlugin;
+
+ /**
+ * Container element for upcoming events
+ */
+ containerEl: HTMLElement;
+
+ /**
+ * Upcoming events data
+ */
+ upcomingEvents: UpcomingEvent[] = [];
+
+ /**
+ * Initialize upcoming events component
+ *
+ * @param containerEl Container element
+ * @param plugin Reference to the main plugin
+ */
+ constructor(containerEl: HTMLElement, plugin: SpaceforgePlugin) {
+ this.containerEl = containerEl;
+ this.plugin = plugin;
+ }
+
+ /**
+ * Render upcoming events list
+ */
+ async render(): Promise {
+ if (!this.plugin.settings.enableCalendarEvents || !this.plugin.settings.showUpcomingEvents) {
+ this.containerEl.empty();
+ return;
+ }
+
+ if (!this.plugin.calendarEventService) {
+ this.containerEl.empty();
+ return;
+ }
+
+ // Load upcoming events data
+ await this.loadUpcomingEvents();
+
+ // Clear container
+ this.containerEl.empty();
+
+ if (this.upcomingEvents.length === 0) {
+ this.renderEmptyState();
+ return;
+ }
+
+ this.renderEventsList();
+ }
+
+ /**
+ * Load upcoming events data
+ */
+ async loadUpcomingEvents(): Promise {
+ this.upcomingEvents = this.plugin.calendarEventService.getUpcomingEvents(
+ this.plugin.settings.upcomingEventsDays || 7
+ );
+ }
+
+ /**
+ * Render empty state when no upcoming events
+ */
+ 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");
+ }
+
+ /**
+ * Render the events list
+ */
+ private renderEventsList(): void {
+ // Create header
+ const header = this.containerEl.createDiv("upcoming-events-header");
+
+ const headerTitle = header.createDiv("upcoming-events-title");
+ headerTitle.setText("Upcoming Events");
+
+ const headerSubtitle = header.createDiv("upcoming-events-subtitle");
+ const daysText = this.plugin.settings.upcomingEventsDays || 7;
+ headerSubtitle.setText(`Next ${daysText} days`);
+
+ // Create events container
+ const eventsContainer = this.containerEl.createDiv("upcoming-events-container");
+
+ // Group events by day
+ const eventsByDay = this.groupEventsByDay();
+
+ // Render each day's events
+ eventsByDay.forEach((dayEvents, dayKey) => {
+ this.renderDaySection(eventsContainer, dayKey, dayEvents);
+ });
+ }
+
+ /**
+ * Group upcoming events by day
+ */
+ private groupEventsByDay(): Map {
+ const eventsByDay = new Map();
+
+ 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'
+ });
+ }
+
+ if (!eventsByDay.has(dayKey)) {
+ eventsByDay.set(dayKey, []);
+ }
+
+ eventsByDay.get(dayKey)!.push(upcomingEvent);
+ });
+
+ return eventsByDay;
+ }
+
+ /**
+ * Render a day section with its events
+ */
+ 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") {
+ daySection.addClass("tomorrow");
+ }
+
+ // Events for this day
+ const dayEventsContainer = daySection.createDiv("upcoming-events-day-events");
+
+ dayEvents.forEach(upcomingEvent => {
+ this.renderEventItem(dayEventsContainer, upcomingEvent);
+ });
+ }
+
+ /**
+ * Render a single event item
+ */
+ 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.backgroundColor = 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);
+ } else if (event.isAllDay) {
+ 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");
+ });
+ }
+
+ /**
+ * Show event details modal
+ *
+ * @param event Event to show details for
+ */
+ private showEventDetails(event: any): void {
+ const eventDate = new Date(event.date);
+ 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);
+ }
+
+ /**
+ * Refresh the upcoming events display
+ */
+ async refresh(): Promise {
+ await this.render();
+ }
+
+ /**
+ * Clean up the component
+ */
+ destroy(): void {
+ this.containerEl.empty();
+ }
+}
\ No newline at end of file