From 49af0fb0a2b57c083b94ffd47ec0eb60514e572f Mon Sep 17 00:00:00 2001 From: stfrigerio Date: Sun, 13 Apr 2025 14:31:35 +0200 Subject: [PATCH] added mood notes button --- main.ts | 6 +- src/DBService.ts | 8 + .../MoodNoteModal/MoodNoteEntryModal.tsx | 238 ++++++++++++++++++ .../MoodNoteModal/helpers/getContrastYIQ.ts | 37 +++ .../MoodNote/moodButtonProcessor.ts | 54 ++++ 5 files changed, 342 insertions(+), 1 deletion(-) create mode 100644 src/components/MoodNoteModal/MoodNoteEntryModal.tsx create mode 100644 src/components/MoodNoteModal/helpers/getContrastYIQ.ts create mode 100644 src/webcomponents/MoodNote/moodButtonProcessor.ts diff --git a/main.ts b/main.ts index b4f5226..659d96f 100644 --- a/main.ts +++ b/main.ts @@ -3,7 +3,8 @@ import { FileSystemAdapter, Editor, MarkdownView, - MarkdownPostProcessorContext + MarkdownPostProcessorContext, + Notice } from "obsidian"; import { DBService } from "./src/DBService"; @@ -24,6 +25,8 @@ import { registerSqlChartRenderer } from "src/webcomponents/SqlChart/registerSql import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types"; import { pluginState } from "src/pluginState"; +import { MoodNoteEntryModal } from "src/components/MoodNoteModal/MoodNoteEntryModal"; +import { registerMoodNoteButtonProcessor } from "src/webcomponents/MoodNote/moodButtonProcessor"; export default class SQLiteDBPlugin extends Plugin { settings: SQLiteDBSettings; @@ -49,6 +52,7 @@ export default class SQLiteDBPlugin extends Plugin { registerSqlChartRenderer(this, this.dbService); registerTimestampUpdaterButton(this); + registerMoodNoteButtonProcessor(this, this.dbService); //? Codeblocks this.registerMarkdownCodeBlockProcessor( diff --git a/src/DBService.ts b/src/DBService.ts index 7eb5395..290617d 100644 --- a/src/DBService.ts +++ b/src/DBService.ts @@ -169,6 +169,14 @@ export class DBService { } } + public generateUuid(): string { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0; + const v = c === 'x' ? r : (r & 0x3 | 0x8); + return v.toString(16); + }); + } + /** * Explicitly closes the database connection. Important for unloading. */ diff --git a/src/components/MoodNoteModal/MoodNoteEntryModal.tsx b/src/components/MoodNoteModal/MoodNoteEntryModal.tsx new file mode 100644 index 0000000..f5b7dfc --- /dev/null +++ b/src/components/MoodNoteModal/MoodNoteEntryModal.tsx @@ -0,0 +1,238 @@ +import { App, Modal, Notice, TextAreaComponent, Setting } from 'obsidian'; +import { DBService } from '../../DBService'; +import { getContrastYIQ } from './helpers/getContrastYIQ'; + +interface MoodTag { + text: string; + type: string; + color: string; +} + +export class MoodNoteEntryModal extends Modal { + private dbService: DBService; + private availableTags: MoodTag[] = []; + private selectedTags: Set = new Set(); // Store names of selected tags + private comment: string = ''; + private commentComponent: TextAreaComponent | null = null; + private tagContainerEl: HTMLElement | null = null; + private rating: number = 5; // Default rating value + private ratingComponent: HTMLElement | null = null; + + private onSaveCallback?: () => void; + + constructor(app: App, dbService: DBService, onSave?: () => void) { + super(app); + this.dbService = dbService; + this.onSaveCallback = onSave; + this.modalEl.addClass('mood-note-entry-modal'); + } + + async onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.addClass('mood-note-modal-content'); + + contentEl.createEl('h2', { text: 'Add Mood Note 💭' }); + + // --- Fetch Available Tags --- + await this.fetchTags(); + + // --- Tag Selection Area --- + contentEl.createEl('h4', { text: 'Select Tags:' }); + this.tagContainerEl = contentEl.createDiv({ cls: 'mood-tag-selection-area' }); + this.renderTags(); + + // --- Comment Area --- + contentEl.createEl('h4', { text: 'Comment' }); + contentEl.createEl('p', { text: 'Add any details about your current mood.', cls: 'setting-item-description' }); + + const commentContainer = contentEl.createDiv({ cls: 'mood-note-comment-container' }); + const textArea = new TextAreaComponent(commentContainer); + this.commentComponent = textArea; + textArea.setPlaceholder("How are you feeling?") + .setValue(this.comment) + .onChange((value) => { + this.comment = value; + }); + textArea.inputEl.rows = 6; + textArea.inputEl.style.width = '100%'; + textArea.inputEl.addClass('mood-note-comment-input'); + + // --- Rating Area --- + contentEl.createEl('h4', { text: 'Rating' }); + contentEl.createEl('p', { text: 'How would you rate your mood from 1 to 10?', cls: 'setting-item-description' }); + + const ratingContainer = contentEl.createDiv({ cls: 'mood-note-rating-container' }); + + // Create slider container + const sliderContainer = ratingContainer.createDiv({ cls: 'mood-note-slider-container' }); + + // Create slider + const slider = sliderContainer.createEl('input', { + type: 'range', + attr: { + min: '1', + max: '10', + value: this.rating.toString(), + step: '1' + }, + cls: 'mood-note-slider' + }); + + // Create value display + const valueDisplay = sliderContainer.createEl('span', { + text: this.rating.toString(), + cls: 'mood-note-rating-value' + }); + + // Update value display when slider changes + slider.addEventListener('input', (e) => { + const value = (e.target as HTMLInputElement).value; + this.rating = parseInt(value); + valueDisplay.setText(value); + }); + + this.ratingComponent = ratingContainer; + + // --- Action Buttons --- + new Setting(contentEl) + .addButton((btn) => + btn + .setButtonText('Save Mood Note') + .setCta() // Makes it visually prominent + .onClick(() => { + this.handleSave(); + }) + ) + .addButton((btn) => + btn + .setButtonText('Cancel') + .onClick(() => { + this.close(); + }) + ); + } + + private async fetchTags() { + try { + // Adjust query based on your schema + const query = `SELECT DISTINCT text, color FROM Tags WHERE type = 'moodTag' ORDER BY text ASC;`; + + const results = await this.dbService.getQuery(query); + + if (results && Array.isArray(results)) { + this.availableTags = results.map(row => ({ text: row.text, type: 'moodTag', color: row.color })); + } else { + console.warn("Could not fetch mood tags or no tags found."); + this.availableTags = []; + } + } catch (error) { + console.error("Error fetching mood tags:", error); + new Notice("Error fetching tags. Check console."); + this.availableTags = []; + } + } + + private renderTags() { + if (!this.tagContainerEl) return; + this.tagContainerEl.empty(); + this.tagContainerEl.addClass('tag-checkbox-container'); + + if (this.availableTags.length === 0) { + this.tagContainerEl.setText('No mood tags found or configured.'); + return; + } + + this.availableTags.forEach(tag => { + const tagId = `mood-tag-${tag.text.replace(/[^a-zA-Z0-9]/g, '-')}`; + const isChecked = this.selectedTags.has(tag.text); + + const label = this.tagContainerEl?.createEl('label', { + cls: `mood-tag-label ${isChecked ? 'is-checked' : ''}`, + attr: { for: tagId } + }); + + if (!label) { + console.error("Failed to create label for tag:", tag); + return; + } + + // --- Apply Color Styling --- + if (tag.color) { + try { + const textColor = getContrastYIQ(tag.color); + label.style.backgroundColor = tag.color; + label.style.color = textColor; + // Make border slightly darker or use the same color initially + label.style.borderColor = tag.color; + + // Ensure checkbox contrast might need specific styling if colors are very light/dark + // but usually browsers handle this okay. + } catch (e) { + console.error(`Failed to apply color ${tag.color} for tag ${tag.text}:`, e); + // Apply default styles if color fails + label.style.backgroundColor = ''; + label.style.color = ''; + label.style.borderColor = ''; + } + } else { + // Reset styles if no color defined for this tag + label.style.backgroundColor = ''; + label.style.color = ''; + label.style.borderColor = ''; + } + + const checkbox = label.createEl('input', { + type: 'checkbox', + attr: { id: tagId } + }); + checkbox.checked = isChecked; + + label.appendText(tag.text); + + checkbox.onchange = (e) => { + if ((e.target as HTMLInputElement).checked) { + this.selectedTags.add(tag.text); + label.addClass('is-checked'); // Update style immediately + } else { + this.selectedTags.delete(tag.text); + label.removeClass('is-checked'); // Update style immediately + } + }; + + }); + } + + private async handleSave() { + const tagsString = Array.from(this.selectedTags).join(','); + const comment = this.comment.trim(); + const timestamp = new Date().toISOString(); + const uuid = this.dbService.generateUuid(); + const createdAt = new Date().toISOString(); + const updatedAt = new Date().toISOString(); + + const sql = ` + INSERT INTO Mood (uuid, date, tag, comment, rating, createdAt, updatedAt) + VALUES (?, ?, ?, ?, ?, ?, ?); + `; + + const params = [uuid, timestamp, tagsString, comment, this.rating, createdAt, updatedAt]; + + try { + await this.dbService.runQuery(sql, params); // Use runQuery or appropriate method for INSERT + new Notice(`Mood Note saved: ${tagsString || 'No tags'} - ${comment.substring(0, 20)}...`); + this.close(); // Close modal on success + if (this.onSaveCallback) { + this.onSaveCallback(); // Trigger refresh if provided + } + } catch (error) { + console.error("Error saving mood note:", error); + new Notice("Failed to save mood note. Check console."); + } + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/components/MoodNoteModal/helpers/getContrastYIQ.ts b/src/components/MoodNoteModal/helpers/getContrastYIQ.ts new file mode 100644 index 0000000..22a3c2b --- /dev/null +++ b/src/components/MoodNoteModal/helpers/getContrastYIQ.ts @@ -0,0 +1,37 @@ +/** + * Calculates whether black or white text provides better contrast against a given hex background color. + * Uses YIQ formula. + * @param hexcolor - The background color in hex format (e.g., "#RRGGBB", "#RGB"). + * @returns '#000000' (black) or '#FFFFFF' (white). + */ +export function getContrastYIQ(hexcolor: string): string { + if (!hexcolor) return '#000000'; // Default to black if no color provided + + // Remove '#' if present + hexcolor = hexcolor.replace("#", ""); + + let r: number, g: number, b: number; + + // Handle short hex codes (#RGB) + if (hexcolor.length === 3) { + r = parseInt(hexcolor.substr(0, 1).repeat(2), 16); + g = parseInt(hexcolor.substr(1, 1).repeat(2), 16); + b = parseInt(hexcolor.substr(2, 1).repeat(2), 16); + } + // Handle full hex codes (#RRGGBB) + else if (hexcolor.length === 6) { + r = parseInt(hexcolor.substr(0, 2), 16); + g = parseInt(hexcolor.substr(2, 2), 16); + b = parseInt(hexcolor.substr(4, 2), 16); + } else { + // Invalid format, default to black + console.warn(`Invalid hex color format for contrast calculation: ${hexcolor}`); + return '#000000'; + } + + // Calculate YIQ (luminance) + const yiq = ((r * 299) + (g * 587) + (b * 114)) / 1000; + + // Threshold (standard value is 128, adjust if needed) + return (yiq >= 128) ? '#000000' : '#FFFFFF'; +} \ No newline at end of file diff --git a/src/webcomponents/MoodNote/moodButtonProcessor.ts b/src/webcomponents/MoodNote/moodButtonProcessor.ts new file mode 100644 index 0000000..e1703ab --- /dev/null +++ b/src/webcomponents/MoodNote/moodButtonProcessor.ts @@ -0,0 +1,54 @@ +import { Plugin, MarkdownPostProcessorContext, ButtonComponent, Notice } from 'obsidian'; +import { MoodNoteEntryModal } from '../../components/MoodNoteModal/MoodNoteEntryModal'; +import { DBService } from '../../DBService'; // Adjust path if needed + +/** + * Registers a MarkdownPostProcessor to create mood note buttons + * AND registers the global event listener to open the modal. + */ +export function registerMoodNoteButtonProcessor(plugin: Plugin, dbService: DBService) { + + // --- Register the Post Processor (finds placeholders, creates buttons) --- + plugin.registerMarkdownPostProcessor((el: HTMLElement, ctx: MarkdownPostProcessorContext) => { + const placeholders = el.querySelectorAll('div.mood-note-button-placeholder'); + placeholders.forEach(placeholder => { + if (placeholder.dataset.processed) return; + placeholder.dataset.processed = 'true'; + + const buttonText = placeholder.dataset.buttonText || 'Add Mood Note 💭'; + placeholder.innerHTML = ''; + placeholder.addClass('mood-note-button-container'); + + const button = new ButtonComponent(placeholder) + .setButtonText(buttonText) + .setClass('mood-note-trigger-button'); + button.buttonEl.style.margin = '0.5em 0'; + + button.onClick(() => { + console.log("Mood Note Button (from div) Clicked - Dispatching Event"); + placeholder.dispatchEvent(new CustomEvent('request-mood-modal', { + bubbles: true, + composed: true + })); + }); + }); + }); + + // --- Register the Global Event Listener (catches event, opens modal) --- + // Use plugin.registerDomEvent for automatic cleanup on unload + plugin.registerDomEvent(document, 'request-mood-modal' as keyof DocumentEventMap, (evt: CustomEvent) => { + if (!dbService) { + new Notice("Database service is not ready."); + console.error("DBService not initialized/ready when trying to open mood modal via custom event."); + return; + } + + // Access app via the plugin instance + const appInstance = plugin.app; + + // Open the modal, passing app and dbService + new MoodNoteEntryModal(appInstance, dbService, () => { + new Notice("Mood note saved."); + }).open(); + }); +} \ No newline at end of file