diff --git a/main.ts b/main.ts index 8df0ad2..3a8211b 100644 --- a/main.ts +++ b/main.ts @@ -3,7 +3,6 @@ import { Plugin, PluginSettingTab, Setting, - Notice, FileSystemAdapter, Editor, MarkdownView, @@ -12,22 +11,23 @@ import { import { DBService } from "./src/dbService"; import { inspectTableStructure, convertEntriesInNotes } from "./src/commands"; -import { processSqlBlock, processSqlChartBlock } from "./src/codeblocks"; +import { processSqlBlock, processSqlChartBlock, renderDatePicker } from "./src/codeblocks"; import { pickTableName } from "./src/helpers"; import { SQLiteDBSettings, DEFAULT_SETTINGS } from "./src/types"; +import { injectDatePickerStyles } from "src/styles/datePickerInject"; export default class SQLiteDBPlugin extends Plugin { settings: SQLiteDBSettings; private dbService: DBService; async onload() { - console.log("Loading SQLiteDBPlugin..."); + // init await this.loadSettings(); - this.dbService = new DBService(this.app); - await this.openDatabase(); + injectDatePickerStyles(); + this.addCommand({ id: "inspect-table-structure", name: "Inspect table structure", @@ -42,7 +42,7 @@ export default class SQLiteDBPlugin extends Plugin { id: "dump-table-to-notes", name: "Dump table to notes", callback: async () => { - await this.openDatabase(); // ensure DB is loaded + await this.openDatabase(); // 1) pick a table const chosenTable = await pickTableName(this.dbService, this.app); @@ -56,7 +56,7 @@ export default class SQLiteDBPlugin extends Plugin { }); this.registerMarkdownCodeBlockProcessor( - "sql", // <-- the name of your code block (```sql) + "sql", async (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext) => { await processSqlBlock(this.dbService, source, el); } @@ -69,6 +69,13 @@ export default class SQLiteDBPlugin extends Plugin { } ); + this.registerMarkdownCodeBlockProcessor( + "date-picker", + async (source: string, el: HTMLElement) => { + renderDatePicker(el, this.app); + } + ); + this.addSettingTab(new SQLiteDBSettingTab(this.app, this)); } diff --git a/src/codeblocks/datePicker/DatePickerModal.ts b/src/codeblocks/datePicker/DatePickerModal.ts new file mode 100644 index 0000000..ab10fef --- /dev/null +++ b/src/codeblocks/datePicker/DatePickerModal.ts @@ -0,0 +1,197 @@ +import { Modal, App, Notice, MarkdownView } from "obsidian"; +import { pluginState } from "../../pluginState"; // Assuming this exists and works + +function getISOWeekNumber(date: Date): number { + const temp = new Date(date.getTime()); + temp.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + temp.setDate(temp.getDate() + 3 - ((temp.getDay() + 6) % 7)); + const week1 = new Date(temp.getFullYear(), 0, 4); + return ( + 1 + + Math.round( + ((temp.getTime() - week1.getTime()) / 86400000 - ((week1.getDay() + 6) % 7)) / + 7 + ) + ); +} + +export class DatePickerModal extends Modal { + private selectedDate: Date; + private currentMonth: Date; + private today: Date = new Date(); + + constructor(app: App) { + super(app); + // Ensure selectedDate is always a Date object + const initialDate = pluginState.selectedDate ? new Date(pluginState.selectedDate) : new Date(); + // Validate if the parsed date is valid, otherwise default to today + this.selectedDate = isNaN(initialDate.getTime()) ? new Date() : initialDate; + // Set time to 00:00:00 to compare dates easily + this.selectedDate.setHours(0, 0, 0, 0); + this.today.setHours(0, 0, 0, 0); + + this.currentMonth = new Date(this.selectedDate.getFullYear(), this.selectedDate.getMonth(), 1); + } + + onOpen() { + // Add a class to the modal content for easier styling + this.contentEl.addClass("custom-datepicker-modal-content"); + this.render(); + } + + private render() { + const { contentEl } = this; + contentEl.empty(); // Clear previous content + + // --- Header --- + const header = contentEl.createEl("div", { cls: "calendar-header" }); + const prevBtn = header.createEl("button", { text: "←", cls: "clickable-icon" }); + prevBtn.setAttribute("aria-label", "Previous month"); + prevBtn.onclick = () => { + this.currentMonth.setMonth(this.currentMonth.getMonth() - 1); + this.render(); + }; + + const title = header.createEl("span", { + cls: "calendar-title", + text: this.currentMonth.toLocaleString("default", { month: "long", year: "numeric" }), + }); + + const nextBtn = header.createEl("button", { text: "→", cls: "clickable-icon" }); + nextBtn.setAttribute("aria-label", "Next month"); + nextBtn.onclick = () => { + this.currentMonth.setMonth(this.currentMonth.getMonth() + 1); + this.render(); + }; + + // --- Weekday Headers --- + const weekdaysContainer = contentEl.createDiv({ cls: "calendar-weekdays" }); + const weekdays = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"]; + weekdaysContainer.createDiv({ cls: "week-number-header", text: "W#" }); // Header for Week# + weekdays.forEach(day => weekdaysContainer.createDiv({ cls: "calendar-weekday", text: day })); + + // --- Calendar Grid --- + // buildCalendar now returns the grid itself + const calendarGrid = this.buildCalendar(); + contentEl.appendChild(calendarGrid); + + // --- Footer --- + const footer = contentEl.createDiv({ cls: "calendar-footer" }); + // Display currently selected date + const selectedDateDisplay = footer.createEl("span", { cls: "selected-date-display" }); + this.updateSelectedDateDisplay(selectedDateDisplay); // Initial display + + const confirmBtn = footer.createEl("button", { text: "Select", cls: "mod-cta" }); // Use Obsidian's primary button style + confirmBtn.onclick = () => { + const iso = this.selectedDate.toISOString().split("T")[0]; + pluginState.selectedDate = iso; // Update global state + new Notice(`Date set to ${iso}`); + // Optional: Force re-render if your plugin uses the date reactively in preview + this.app.workspace.getActiveViewOfType(MarkdownView)?.previewMode?.rerender(true); + this.close(); + }; + } + + private buildCalendar(): HTMLElement { + const grid = document.createElement("div"); + grid.className = "calendar-grid"; // This will be the actual grid container + + const year = this.currentMonth.getFullYear(); + const month = this.currentMonth.getMonth(); + const firstDayOfMonth = new Date(year, month, 1); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Calculate offset for Monday start (0=Mon, 6=Sun) + const startOffset = (firstDayOfMonth.getDay() + 6) % 7; + + // --- Add Week Numbers and Empty Cells before the 1st --- + let day = 1 - startOffset; // Start day counter considering offset + let currentWeekNumber = -1; + + while (day <= daysInMonth) { + // Calculate and add week number cell for the current row + const dateForWeekNum = new Date(year, month, day > 0 ? day : 1); // Use day 1 if current day is placeholder + const weekNumber = getISOWeekNumber(dateForWeekNum); + // Only add week number if it changed (start of a new row essentially) + if (weekNumber !== currentWeekNumber) { + const weekEl = document.createElement("div"); + weekEl.className = "week-number"; + weekEl.textContent = `${String(weekNumber).padStart(2, "0")}`; + grid.appendChild(weekEl); + currentWeekNumber = weekNumber; + } else if (grid.children.length % 8 === 0) { + // If week number didn't change but we are at start of grid row, add it + // This handles month starts that continue week number from previous month correctly + const weekEl = document.createElement("div"); + weekEl.className = "week-number"; + weekEl.textContent = `${String(weekNumber).padStart(2, "0")}`; + grid.appendChild(weekEl); + } + + + // Add the 7 day cells for the week + for (let i = 0; i < 7; i++) { + const cell = document.createElement("div"); // Use a div as container for button + cell.className = "calendar-day-cell"; + + if (day < 1 || day > daysInMonth) { + // Empty cell (before start or after end of month) + cell.classList.add("empty"); + } else { + const currentDate = new Date(year, month, day); + currentDate.setHours(0, 0, 0, 0); // Normalize time for comparison + + const btn = document.createElement("button"); + btn.textContent = String(day); + btn.className = "calendar-day"; + btn.setAttribute("aria-label", currentDate.toDateString()); + + // Check if this is the currently selected date + if (currentDate.getTime() === this.selectedDate.getTime()) { + btn.classList.add("selected"); + btn.setAttribute("aria-selected", "true"); + } else { + btn.setAttribute("aria-selected", "false"); + } + + // Check if this is today's date + if (currentDate.getTime() === this.today.getTime()) { + btn.classList.add("today"); + } + + btn.onclick = () => { + // Update selected date state + this.selectedDate = currentDate; + + const previouslySelected = grid.querySelector(".calendar-day.selected"); + if (previouslySelected) { + previouslySelected.classList.remove("selected"); + previouslySelected.setAttribute("aria-selected", "false"); + } + btn.classList.add("selected"); + btn.setAttribute("aria-selected", "true"); + + this.updateSelectedDateDisplay(); + }; + cell.appendChild(btn); + } + grid.appendChild(cell); + day++; + } + } + return grid; + } + + private updateSelectedDateDisplay(element?: HTMLElement) { + const displayEl = element ?? this.contentEl.querySelector(".selected-date-display"); + if (displayEl) { + displayEl.textContent = `Selected: ${this.selectedDate.toLocaleDateString()}`; + } + } + + + onClose() { + this.contentEl.empty(); + } +} \ No newline at end of file diff --git a/src/codeblocks/datePicker/index.ts b/src/codeblocks/datePicker/index.ts new file mode 100644 index 0000000..e4c5b69 --- /dev/null +++ b/src/codeblocks/datePicker/index.ts @@ -0,0 +1,11 @@ +import { App } from "obsidian"; +import { DatePickerModal } from "./DatePickerModal"; + +export function renderDatePicker(container: HTMLElement, app: App) { + const wrapper = container.createDiv({ cls: "datepicker" }); + + const button = wrapper.createEl("button", { text: "Select Date" }); + button.onclick = () => { + new DatePickerModal(app).open(); + }; +} diff --git a/src/codeblocks/index.ts b/src/codeblocks/index.ts index 38c67b6..d771e66 100644 --- a/src/codeblocks/index.ts +++ b/src/codeblocks/index.ts @@ -1,4 +1,5 @@ import { processSqlBlock } from "./processSqlBlock"; import { processSqlChartBlock } from "./processSqlChartBlock"; +import { renderDatePicker } from "./datePicker"; -export { processSqlBlock, processSqlChartBlock }; \ No newline at end of file +export { processSqlBlock, processSqlChartBlock, renderDatePicker }; \ No newline at end of file diff --git a/src/codeblocks/processSqlBlock/index.ts b/src/codeblocks/processSqlBlock/index.ts index 6bd1828..d29a21a 100644 --- a/src/codeblocks/processSqlBlock/index.ts +++ b/src/codeblocks/processSqlBlock/index.ts @@ -1,6 +1,7 @@ import { Notice } from "obsidian"; import { DBService } from "../../dbService"; import { parseSqlParams, validateTable, buildSqlQuery, renderResults } from "./helpers"; +import { pluginState } from "../../pluginState"; export async function processSqlBlock(dbService: DBService, source: string, el: HTMLElement) { const db = dbService.getDB(); @@ -10,7 +11,10 @@ export async function processSqlBlock(dbService: DBService, source: string, el: return; } - const params = parseSqlParams(source); + // replace @date with the selected date + const injectedSource = source.replace(/@date/g, pluginState.selectedDate); + const params = parseSqlParams(injectedSource); + if (!params) { el.createEl("p", { text: "Missing required parameter: table" }); el.createEl("p", { text: "Example usage:" }); diff --git a/src/codeblocks/processSqlChartBlock/index.ts b/src/codeblocks/processSqlChartBlock/index.ts index f5487ee..3e131f6 100644 --- a/src/codeblocks/processSqlChartBlock/index.ts +++ b/src/codeblocks/processSqlChartBlock/index.ts @@ -2,6 +2,7 @@ import { Notice } from "obsidian"; import { DBService } from "../../dbService"; import { parseChartParams, validateColumns, buildSqlQuery, processChartData } from "./helpers"; import { ChartType, createChart } from "./charts"; +import { pluginState } from "../../pluginState"; export async function processSqlChartBlock(dbService: DBService, source: string, el: HTMLElement) { const db = dbService.getDB(); @@ -11,7 +12,10 @@ export async function processSqlChartBlock(dbService: DBService, source: string, return; } - const config = parseChartParams(source); + // replace @date with the selected date + const injectedSource = source.replace(/@date/g, pluginState.selectedDate); + const config = parseChartParams(injectedSource); + if (!config) { // @ts-ignore const message = config?.chartType === 'pie' diff --git a/src/pluginState.ts b/src/pluginState.ts new file mode 100644 index 0000000..dc106c5 --- /dev/null +++ b/src/pluginState.ts @@ -0,0 +1,17 @@ +export class PluginState { + private _selectedDate: string; + + constructor() { + this._selectedDate = new Date().toISOString().split("T")[0]; + } + + get selectedDate(): string { + return this._selectedDate; + } + + set selectedDate(newDate: string) { + this._selectedDate = newDate; + } +} + +export const pluginState = new PluginState(); diff --git a/src/styles/datePickerInject.ts b/src/styles/datePickerInject.ts new file mode 100644 index 0000000..d022ec0 --- /dev/null +++ b/src/styles/datePickerInject.ts @@ -0,0 +1,163 @@ +export function injectDatePickerStyles() { + const styleId = "custom-datepicker-styles"; + // Avoid injecting multiple times + if (document.getElementById(styleId)) { + return; + } + + const style = document.createElement("style"); + style.id = styleId; + style.textContent = ` + /* Style the modal content area */ + .custom-datepicker-modal-content { + padding: var(--size-4-4) var(--size-4-6); /* Consistent padding */ + } + + /* Header: Title and Navigation Buttons */ + .calendar-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: var(--size-4-3); /* Space below header */ + } + .calendar-title { + font-weight: var(--font-bold); + font-size: var(--font-ui-large); + } + .calendar-header button.clickable-icon { + /* Use Obsidian's icon button style */ + padding: var(--size-4-1) var(--size-4-2); + font-size: var(--font-ui-large); /* Make arrows bigger */ + line-height: 1; + } + + /* Weekday Headers Row */ + .calendar-weekdays { + display: grid; + grid-template-columns: 36px repeat(7, 1fr); /* Week# + 7 days */ + gap: var(--size-4-1); + padding-bottom: var(--size-4-1); + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: var(--size-4-2); + } + .calendar-weekday, + .week-number-header { + text-align: center; + font-weight: var(--font-semibold); + font-size: var(--font-ui-small); + color: var(--text-muted); + line-height: 1; /* Ensure consistent height */ + } + .week-number-header { + font-style: italic; + } + + /* Calendar Grid: Week Numbers and Day Cells */ + .calendar-grid { + display: grid; + grid-template-columns: 36px repeat(7, 1fr); /* Week# + 7 days */ + gap: var(--size-4-1); /* Gap between cells */ + align-items: center; /* Vertically center content in grid rows */ + } + + /* Individual cell container (holds button or is empty) */ + .calendar-day-cell { + display: flex; /* Center button inside */ + justify-content: center; + align-items: center; + aspect-ratio: 1 / 1; /* Make cells squareish */ + min-height: 30px; /* Ensure minimum height */ + } + .calendar-day-cell.empty { + /* Style for empty cells (before/after month) */ + /* background-color: var(--background-secondary); */ /* Subtle background */ + /* opacity: 0.5; */ + /* Or just leave them blank */ + } + + + /* Week Number Cells in the main grid */ + .week-number { + text-align: center; + font-size: var(--font-ui-smaller); + color: var(--text-faint); + font-style: italic; + align-self: center; /* Align vertically in grid row */ + padding: 0 var(--size-4-1); /* Add some horizontal padding */ + line-height: var(--line-height-normal); + } + + /* Day Buttons */ + .calendar-day { + display: flex; + align-items: center; + justify-content: center; + width: 100%; /* Fill cell width */ + height: 100%; /* Fill cell height */ + padding: var(--size-4-1); + border: 1px solid transparent; /* Reserve space for border */ + border-radius: var(--radius-s); + background-color: var(--background-secondary); + color: var(--text-normal); + font-size: var(--font-ui-small); + cursor: pointer; + transition: all 0.1s ease-in-out; + line-height: 1; /* Prevent text pushing button size */ + } + + .calendar-day:hover { + background-color: var(--background-modifier-hover); + color: var(--text-normal); /* Keep text readable on hover */ + border-color: var(--background-modifier-border-hover); + } + + /* Today's Date Indicator */ + .calendar-day.today { + border-color: var(--text-accent); /* Use accent color for border */ + /* Optional: Add a subtle background or different font weight */ + /* background-color: var(--background-primary-alt); */ + /* font-weight: var(--font-bold); */ + } + + /* Selected Date Styling */ + .calendar-day.selected { + background-color: var(--interactive-accent); /* Use theme's accent color */ + color: var(--text-on-accent); + font-weight: var(--font-bold); + border-color: var(--interactive-accent-hover); /* Slightly darker border */ + } + .calendar-day.selected:hover { + background-color: var(--interactive-accent-hover); /* Darken on hover */ + color: var(--text-on-accent); + } + + /* Today and Selected */ + .calendar-day.today.selected { + /* Optional: Make combo distinct if needed, e.g., thicker border */ + /* border-width: 2px; */ + } + + + /* Footer Area */ + .calendar-footer { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: var(--size-4-3); /* Space above footer */ + padding-top: var(--size-4-2); + border-top: 1px solid var(--background-modifier-border); + } + .selected-date-display { + font-size: var(--font-ui-small); + color: var(--text-muted); + } + + .calendar-footer button { + /* Let .mod-cta handle primary button style */ + /* Add margin if needed */ + /* margin-left: var(--size-4-2); */ + } + + `; + document.head.appendChild(style); +} \ No newline at end of file