mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(calendar): add custom calendar views with configurable options
This commit is contained in:
parent
6987fdd2ba
commit
e8c1dd1ec4
21 changed files with 4430 additions and 204 deletions
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "packages/calendar"]
|
||||
path = packages/calendar
|
||||
url = https://github.com/taskgenius/calendar.git
|
||||
1
packages/calendar
Submodule
1
packages/calendar
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 14fc6c499353befebf7748172497d068ac699a5e
|
||||
|
|
@ -125,6 +125,89 @@ export interface CalendarSpecificConfig {
|
|||
workingHoursEnd?: number; // End hour (0-23), default: 18
|
||||
}
|
||||
|
||||
/**
|
||||
* Day filter configuration for custom calendar views
|
||||
* Supports preset filters and custom day selection
|
||||
*/
|
||||
export interface DayFilterConfig {
|
||||
/** Filter type: none, preset, or custom */
|
||||
type: "none" | "hideWeekends" | "hideWeekdays" | "customDays";
|
||||
/** Custom hidden days (0=Sunday, 1=Monday, ..., 6=Saturday) */
|
||||
hiddenDays?: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Time filter configuration for week/day views
|
||||
* Controls which time slots are displayed
|
||||
*/
|
||||
export interface TimeFilterConfig {
|
||||
/** Whether time filtering is enabled */
|
||||
enabled: boolean;
|
||||
/** Filter type: working hours preset or custom range */
|
||||
type: "workingHours" | "custom";
|
||||
/** Start hour (0-23), default: 9 */
|
||||
startHour: number;
|
||||
/** End hour (0-23), default: 18 */
|
||||
endHour: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom calendar view configuration
|
||||
* Allows users to create customized calendar views based on Month/Week/Day
|
||||
*/
|
||||
export interface CustomCalendarViewConfig {
|
||||
/** Unique identifier for this custom view */
|
||||
id: string;
|
||||
/** Display name shown in UI */
|
||||
name: string;
|
||||
/** Icon identifier (Lucide icon name) */
|
||||
icon: string;
|
||||
/** Base view type to inherit from */
|
||||
baseViewType: "month" | "week" | "day" | "agenda" | "year";
|
||||
/** Whether this view is enabled and visible */
|
||||
enabled: boolean;
|
||||
/** Display order in the view switcher */
|
||||
order: number;
|
||||
|
||||
/** Calendar-specific configuration */
|
||||
calendarConfig: CustomCalendarConfig;
|
||||
|
||||
/** Creation timestamp */
|
||||
createdAt: number;
|
||||
/** Last update timestamp */
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calendar configuration options for custom views
|
||||
* Maps to @taskgenius/calendar CalendarConfig options
|
||||
*/
|
||||
export interface CustomCalendarConfig {
|
||||
// Basic configuration
|
||||
/** First day of week: 0=Sunday, 1=Monday, ..., 6=Saturday */
|
||||
firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
/** Show week numbers in month view */
|
||||
showWeekNumbers?: boolean;
|
||||
/** Show event count badges on date cells */
|
||||
showEventCounts?: boolean;
|
||||
/** Maximum events to show per row before "+N more" indicator (month view) */
|
||||
maxEventsPerRow?: number;
|
||||
|
||||
/** Day filter configuration */
|
||||
dayFilter?: DayFilterConfig;
|
||||
|
||||
/** Time filter configuration (week/day views only) */
|
||||
timeFilter?: TimeFilterConfig;
|
||||
|
||||
/** Custom date format configuration */
|
||||
dateFormats?: {
|
||||
/** Month header format (e.g., "yyyy M") */
|
||||
monthHeader?: string;
|
||||
/** Day header format (e.g., "yyyy M d") */
|
||||
dayHeader?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GanttSpecificConfig {
|
||||
viewType: "gantt"; // Discriminator
|
||||
showTaskLabels: boolean;
|
||||
|
|
@ -900,6 +983,9 @@ export interface TaskProgressBarSettings {
|
|||
|
||||
// Workspace Settings
|
||||
workspaces?: WorkspacesConfig;
|
||||
|
||||
// Custom Calendar Views Settings
|
||||
customCalendarViews?: CustomCalendarViewConfig[];
|
||||
}
|
||||
|
||||
/** Define the default settings */
|
||||
|
|
@ -1811,6 +1897,9 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
|
|||
},
|
||||
useWorkspaceSideLeaves: false,
|
||||
},
|
||||
|
||||
// Custom Calendar Views Defaults
|
||||
customCalendarViews: [],
|
||||
};
|
||||
|
||||
// Helper function to get view settings safely
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -58,8 +58,13 @@ export class CalendarEventComponent extends Component {
|
|||
this.app = params.app;
|
||||
|
||||
// Create the main element
|
||||
// Include tg-event-block for @taskgenius/calendar InteractionController compatibility
|
||||
this.eventEl = createEl("div", {
|
||||
cls: ["calendar-event", `calendar-event-${this.viewType}`],
|
||||
cls: [
|
||||
"calendar-event",
|
||||
`calendar-event-${this.viewType}`,
|
||||
"tg-event-block",
|
||||
],
|
||||
});
|
||||
|
||||
if (this.event.metadata.project) {
|
||||
|
|
@ -79,6 +84,8 @@ export class CalendarEventComponent extends Component {
|
|||
this.eventEl.dataset.filePath = this.event.filePath;
|
||||
}
|
||||
this.eventEl.dataset.eventId = this.event.id;
|
||||
// data-eid is used by @taskgenius/calendar InteractionController for event lookup
|
||||
this.eventEl.dataset.eid = this.event.id;
|
||||
}
|
||||
|
||||
override onload(): void {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
/**
|
||||
* @deprecated This file is deprecated. Use `tg-agenda-view.ts` instead.
|
||||
*
|
||||
* The new TGAgendaView extends @taskgenius/calendar's BaseView for better
|
||||
* integration with the calendar system. This legacy implementation is kept
|
||||
* for backward compatibility but should not be used for new development.
|
||||
*
|
||||
* @see {@link ./tg-agenda-view.ts} for the new implementation
|
||||
*/
|
||||
import { App, Component, moment } from "obsidian";
|
||||
import { CalendarEvent } from '@/components/features/calendar/index';
|
||||
import { CalendarEvent } from "@/components/features/calendar/index";
|
||||
import { renderCalendarEvent } from "../rendering/event-renderer"; // Use new renderer
|
||||
import { CalendarViewComponent, CalendarViewOptions } from "./base-view"; // Import base class
|
||||
import TaskProgressBarPlugin from "@/index"; // Import plugin type
|
||||
|
||||
/**
|
||||
* @deprecated Use TGAgendaView from `tg-agenda-view.ts` instead.
|
||||
* This class extends the legacy CalendarViewComponent (Obsidian Component).
|
||||
* The new TGAgendaView extends @taskgenius/calendar's BaseView for unified view management.
|
||||
*/
|
||||
export class AgendaView extends CalendarViewComponent {
|
||||
// Extend base class
|
||||
// private containerEl: HTMLElement; // Inherited
|
||||
|
|
@ -18,7 +32,7 @@ export class AgendaView extends CalendarViewComponent {
|
|||
containerEl: HTMLElement,
|
||||
currentDate: moment.Moment,
|
||||
events: CalendarEvent[],
|
||||
options: CalendarViewOptions = {} // Use base options, default to empty
|
||||
options: CalendarViewOptions = {}, // Use base options, default to empty
|
||||
) {
|
||||
super(plugin, app, containerEl, events, options); // Call base constructor
|
||||
this.app = app;
|
||||
|
|
@ -43,11 +57,11 @@ export class AgendaView extends CalendarViewComponent {
|
|||
rangeStart,
|
||||
rangeEnd,
|
||||
undefined,
|
||||
"[]"
|
||||
"[]",
|
||||
);
|
||||
})
|
||||
.sort(
|
||||
(a, b) => moment(a.start).valueOf() - moment(b.start).valueOf()
|
||||
(a, b) => moment(a.start).valueOf() - moment(b.start).valueOf(),
|
||||
); // Ensure sorting by start time
|
||||
|
||||
// 3. Group events by their start day
|
||||
|
|
@ -67,8 +81,8 @@ export class AgendaView extends CalendarViewComponent {
|
|||
if (Object.keys(eventsByDay).length === 0) {
|
||||
this.containerEl.setText(
|
||||
`No upcoming events from ${rangeStart.format(
|
||||
"MMM D"
|
||||
)} to ${rangeEnd.format("MMM D, YYYY")}.`
|
||||
"MMM D",
|
||||
)} to ${rangeEnd.format("MMM D, YYYY")}.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -83,7 +97,7 @@ export class AgendaView extends CalendarViewComponent {
|
|||
|
||||
// Left column for the date
|
||||
const dateColumn = daySection.createDiv(
|
||||
"agenda-day-date-column"
|
||||
"agenda-day-date-column",
|
||||
);
|
||||
const dayHeader = dateColumn.createDiv("agenda-day-header");
|
||||
dayHeader.textContent = currentDayIter.format("dddd, MMMM D");
|
||||
|
|
@ -93,7 +107,7 @@ export class AgendaView extends CalendarViewComponent {
|
|||
|
||||
// Right column for the events
|
||||
const eventsColumn = daySection.createDiv(
|
||||
"agenda-day-events-column"
|
||||
"agenda-day-events-column",
|
||||
);
|
||||
const eventsList = eventsColumn.createDiv("agenda-events-list"); // Keep the original list class if needed
|
||||
|
||||
|
|
@ -124,8 +138,8 @@ export class AgendaView extends CalendarViewComponent {
|
|||
|
||||
console.log(
|
||||
`Rendered Agenda View component from ${rangeStart.format(
|
||||
"YYYY-MM-DD"
|
||||
)} to ${rangeEnd.format("YYYY-MM-DD")}`
|
||||
"YYYY-MM-DD",
|
||||
)} to ${rangeEnd.format("YYYY-MM-DD")}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,27 @@
|
|||
/**
|
||||
* @deprecated This file is deprecated. Use @taskgenius/calendar's BaseView instead.
|
||||
*
|
||||
* For new calendar views, extend BaseView from @taskgenius/calendar:
|
||||
*
|
||||
* ```typescript
|
||||
* import { BaseView, type ViewMeta } from "@taskgenius/calendar";
|
||||
*
|
||||
* class MyView extends BaseView {
|
||||
* static meta: ViewMeta = { type: "my-view", label: "My View", order: 50 };
|
||||
* render(container: HTMLElement, events: CalendarEvent[]): void { ... }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @see {@link ./tg-agenda-view.ts} for an example of the new pattern
|
||||
* @see {@link ./tg-year-view.ts} for another example
|
||||
*/
|
||||
import { App, Component } from "obsidian";
|
||||
import { CalendarEvent } from '@/components/features/calendar/index';
|
||||
import { CalendarEvent } from "@/components/features/calendar/index";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
|
||||
/**
|
||||
* @deprecated Use @taskgenius/calendar's BaseView instead.
|
||||
*/
|
||||
interface EventMap {
|
||||
onEventClick: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
onEventHover: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
|
|
@ -10,7 +30,7 @@ interface EventMap {
|
|||
day: number,
|
||||
options: {
|
||||
behavior: "open-quick-capture" | "open-task-view";
|
||||
}
|
||||
},
|
||||
) => void;
|
||||
onDayHover: (ev: MouseEvent, day: number) => void;
|
||||
onMonthClick: (ev: MouseEvent, month: number) => void;
|
||||
|
|
@ -21,12 +41,20 @@ interface EventMap {
|
|||
onEventComplete: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use @taskgenius/calendar's BaseView options instead.
|
||||
*/
|
||||
// Combine event handlers into a single options object, making them optional
|
||||
export interface CalendarViewOptions extends Partial<EventMap> {
|
||||
// Add other common view options here if needed
|
||||
getBadgeEventsForDate?: (date: Date) => CalendarEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use @taskgenius/calendar's BaseView instead.
|
||||
* This class extends Obsidian's Component for lifecycle management.
|
||||
* The new BaseView from @taskgenius/calendar provides unified view management.
|
||||
*/
|
||||
export abstract class CalendarViewComponent extends Component {
|
||||
protected containerEl: HTMLElement;
|
||||
protected events: CalendarEvent[];
|
||||
|
|
@ -37,7 +65,7 @@ export abstract class CalendarViewComponent extends Component {
|
|||
app: App,
|
||||
containerEl: HTMLElement,
|
||||
events: CalendarEvent[],
|
||||
options: CalendarViewOptions = {} // Provide default empty options
|
||||
options: CalendarViewOptions = {}, // Provide default empty options
|
||||
) {
|
||||
super(); // Call the base class constructor
|
||||
this.containerEl = containerEl;
|
||||
|
|
|
|||
380
src/components/features/calendar/views/tg-agenda-view.ts
Normal file
380
src/components/features/calendar/views/tg-agenda-view.ts
Normal file
|
|
@ -0,0 +1,380 @@
|
|||
/**
|
||||
* TGAgendaView - Agenda view extending @taskgenius/calendar BaseView
|
||||
*
|
||||
* This view displays upcoming events in a list format grouped by day.
|
||||
* Users can copy this view to create their own custom agenda-style views.
|
||||
*/
|
||||
import { App, moment } from "obsidian";
|
||||
import {
|
||||
BaseView,
|
||||
type ViewMeta,
|
||||
type ViewRenderOptions,
|
||||
type CalendarEvent as TGCalendarEvent,
|
||||
} from "@taskgenius/calendar";
|
||||
import { CalendarEvent } from "@/components/features/calendar/index";
|
||||
import { renderCalendarEvent } from "../rendering/event-renderer";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
|
||||
/**
|
||||
* Options for AgendaView customization
|
||||
*/
|
||||
export interface AgendaViewOptions {
|
||||
/** Number of days to show in the agenda (default: 7) */
|
||||
daysToShow?: number;
|
||||
/** Whether to show empty days (default: false) */
|
||||
showEmptyDays?: boolean;
|
||||
/** Date format for day headers (default: "dddd, MMMM D") */
|
||||
dayHeaderFormat?: string;
|
||||
/** Event click callback */
|
||||
onEventClick?: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
/** Event hover callback */
|
||||
onEventHover?: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
/** Event context menu callback */
|
||||
onEventContextMenu?: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
/** Event complete callback */
|
||||
onEventComplete?: (ev: MouseEvent, event: CalendarEvent) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* AgendaView - Displays events in a list format grouped by day
|
||||
*
|
||||
* Extends BaseView from @taskgenius/calendar for integration with the calendar system.
|
||||
*
|
||||
* @example Creating a custom agenda view
|
||||
* ```typescript
|
||||
* class MyCustomAgendaView extends TGAgendaView {
|
||||
* static override meta: ViewMeta = {
|
||||
* type: 'my-agenda',
|
||||
* label: 'My Agenda',
|
||||
* shortLabel: 'MA',
|
||||
* order: 50
|
||||
* };
|
||||
*
|
||||
* protected override daysToShow = 14; // Show 2 weeks
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class TGAgendaView<T = unknown> extends BaseView<T> {
|
||||
static meta: ViewMeta = {
|
||||
type: "agenda",
|
||||
label: "Agenda",
|
||||
shortLabel: "A",
|
||||
order: 40,
|
||||
};
|
||||
|
||||
/** Plugin instance for accessing settings and services */
|
||||
protected plugin: TaskProgressBarPlugin | null = null;
|
||||
|
||||
/** App instance for Obsidian integration */
|
||||
protected app: App | null = null;
|
||||
|
||||
/** Custom options for this view */
|
||||
protected viewOptions: AgendaViewOptions = {};
|
||||
|
||||
/** Number of days to display (can be overridden by subclasses) */
|
||||
protected daysToShow = 7;
|
||||
|
||||
/** Whether to show days with no events */
|
||||
protected showEmptyDays = false;
|
||||
|
||||
/** Format string for day headers */
|
||||
protected dayHeaderFormat = "dddd, MMMM D";
|
||||
|
||||
/** Child components for cleanup */
|
||||
private childComponents: { unload: () => void }[] = [];
|
||||
|
||||
/**
|
||||
* Set plugin and app references (called after construction)
|
||||
*/
|
||||
setPluginContext(plugin: TaskProgressBarPlugin, app: App): void {
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom options for this view
|
||||
*/
|
||||
setOptions(options: AgendaViewOptions): void {
|
||||
this.viewOptions = options;
|
||||
if (options.daysToShow !== undefined) {
|
||||
this.daysToShow = options.daysToShow;
|
||||
}
|
||||
if (options.showEmptyDays !== undefined) {
|
||||
this.showEmptyDays = options.showEmptyDays;
|
||||
}
|
||||
if (options.dayHeaderFormat !== undefined) {
|
||||
this.dayHeaderFormat = options.dayHeaderFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the agenda view
|
||||
*/
|
||||
render(
|
||||
container: HTMLElement,
|
||||
events: TGCalendarEvent[],
|
||||
_options?: ViewRenderOptions,
|
||||
): void {
|
||||
// Cleanup previous child components
|
||||
this.cleanupChildren();
|
||||
|
||||
container.empty();
|
||||
container.addClass("view-agenda", "tg-agenda-view");
|
||||
|
||||
// Convert TGCalendarEvent[] to CalendarEvent[] for rendering
|
||||
const calendarEvents = this.convertEvents(events);
|
||||
|
||||
// Calculate date range
|
||||
const currentDate = this.context.adapter.create(
|
||||
this.context.currentDate as unknown as Date,
|
||||
);
|
||||
const rangeStart = moment(currentDate as unknown as Date).startOf(
|
||||
"day",
|
||||
);
|
||||
const rangeEnd = rangeStart
|
||||
.clone()
|
||||
.add(this.daysToShow - 1, "days")
|
||||
.endOf("day");
|
||||
|
||||
// Filter and sort events
|
||||
const agendaEvents = this.filterAndSortEvents(
|
||||
calendarEvents,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
);
|
||||
|
||||
// Group events by day
|
||||
const eventsByDay = this.groupEventsByDay(agendaEvents);
|
||||
|
||||
// Render empty state if no events
|
||||
if (Object.keys(eventsByDay).length === 0 && !this.showEmptyDays) {
|
||||
this.renderEmptyState(container, rangeStart, rangeEnd);
|
||||
return;
|
||||
}
|
||||
|
||||
// Render each day
|
||||
this.renderDays(container, eventsByDay, rangeStart, rangeEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TGCalendarEvent to CalendarEvent
|
||||
*/
|
||||
protected convertEvents(events: TGCalendarEvent[]): CalendarEvent[] {
|
||||
return events.map((e) => ({
|
||||
...e,
|
||||
start: new Date(e.start),
|
||||
end: e.end ? new Date(e.end) : undefined,
|
||||
allDay: this.isAllDayEvent(e),
|
||||
metadata: e.metadata as CalendarEvent["metadata"],
|
||||
})) as unknown as CalendarEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event is all-day
|
||||
*/
|
||||
protected isAllDayEvent(event: TGCalendarEvent): boolean {
|
||||
const start = new Date(event.start);
|
||||
const end = event.end ? new Date(event.end) : start;
|
||||
return (
|
||||
start.getHours() === 0 &&
|
||||
start.getMinutes() === 0 &&
|
||||
(end.getHours() === 0 ||
|
||||
(end.getHours() === 23 && end.getMinutes() === 59))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events within the date range and sort by start time
|
||||
*/
|
||||
protected filterAndSortEvents(
|
||||
events: CalendarEvent[],
|
||||
rangeStart: moment.Moment,
|
||||
rangeEnd: moment.Moment,
|
||||
): CalendarEvent[] {
|
||||
return events
|
||||
.filter((event) => {
|
||||
const eventStart = moment(event.start);
|
||||
return eventStart.isBetween(
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
undefined,
|
||||
"[]",
|
||||
);
|
||||
})
|
||||
.sort(
|
||||
(a, b) => moment(a.start).valueOf() - moment(b.start).valueOf(),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Group events by their start day
|
||||
*/
|
||||
protected groupEventsByDay(
|
||||
events: CalendarEvent[],
|
||||
): Record<string, CalendarEvent[]> {
|
||||
const eventsByDay: Record<string, CalendarEvent[]> = {};
|
||||
|
||||
events.forEach((event) => {
|
||||
const dateStr = moment(event.start).format("YYYY-MM-DD");
|
||||
if (!eventsByDay[dateStr]) {
|
||||
eventsByDay[dateStr] = [];
|
||||
}
|
||||
eventsByDay[dateStr].push(event);
|
||||
});
|
||||
|
||||
return eventsByDay;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render empty state when no events
|
||||
*/
|
||||
protected renderEmptyState(
|
||||
container: HTMLElement,
|
||||
rangeStart: moment.Moment,
|
||||
rangeEnd: moment.Moment,
|
||||
): void {
|
||||
const emptyEl = container.createDiv("agenda-empty-state");
|
||||
emptyEl.setText(
|
||||
`No upcoming events from ${rangeStart.format(
|
||||
"MMM D",
|
||||
)} to ${rangeEnd.format("MMM D, YYYY")}.`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all days in the range
|
||||
*/
|
||||
protected renderDays(
|
||||
container: HTMLElement,
|
||||
eventsByDay: Record<string, CalendarEvent[]>,
|
||||
rangeStart: moment.Moment,
|
||||
rangeEnd: moment.Moment,
|
||||
): void {
|
||||
const currentDayIter = rangeStart.clone();
|
||||
|
||||
while (currentDayIter.isSameOrBefore(rangeEnd, "day")) {
|
||||
const dateStr = currentDayIter.format("YYYY-MM-DD");
|
||||
const dayEvents = eventsByDay[dateStr] || [];
|
||||
|
||||
if (dayEvents.length > 0 || this.showEmptyDays) {
|
||||
this.renderDaySection(
|
||||
container,
|
||||
currentDayIter.clone(),
|
||||
dayEvents,
|
||||
);
|
||||
}
|
||||
|
||||
currentDayIter.add(1, "day");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single day section
|
||||
*/
|
||||
protected renderDaySection(
|
||||
container: HTMLElement,
|
||||
date: moment.Moment,
|
||||
events: CalendarEvent[],
|
||||
): void {
|
||||
const daySection = container.createDiv("agenda-day-section");
|
||||
|
||||
// Date column
|
||||
const dateColumn = daySection.createDiv("agenda-day-date-column");
|
||||
const dayHeader = dateColumn.createDiv("agenda-day-header");
|
||||
dayHeader.textContent = date.format(this.dayHeaderFormat);
|
||||
|
||||
if (date.isSame(moment(), "day")) {
|
||||
dayHeader.addClass("is-today");
|
||||
}
|
||||
|
||||
// Events column
|
||||
const eventsColumn = daySection.createDiv("agenda-day-events-column");
|
||||
|
||||
if (events.length === 0) {
|
||||
const emptyDay = eventsColumn.createDiv("agenda-empty-day");
|
||||
emptyDay.setText("No events");
|
||||
} else {
|
||||
const eventsList = eventsColumn.createDiv("agenda-events-list");
|
||||
this.renderEvents(eventsList, events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render events for a day
|
||||
*/
|
||||
protected renderEvents(
|
||||
container: HTMLElement,
|
||||
events: CalendarEvent[],
|
||||
): void {
|
||||
// Sort events by time
|
||||
const sortedEvents = [...events].sort((a, b) => {
|
||||
const timeA = a.start ? moment(a.start).valueOf() : 0;
|
||||
const timeB = b.start ? moment(b.start).valueOf() : 0;
|
||||
return timeA - timeB;
|
||||
});
|
||||
|
||||
sortedEvents.forEach((event) => {
|
||||
const eventItem = container.createDiv("agenda-event-item");
|
||||
|
||||
if (this.app) {
|
||||
const { eventEl, component } = renderCalendarEvent({
|
||||
event,
|
||||
viewType: "agenda",
|
||||
app: this.app,
|
||||
onEventClick: this.viewOptions.onEventClick,
|
||||
onEventHover: this.viewOptions.onEventHover,
|
||||
onEventContextMenu: this.viewOptions.onEventContextMenu,
|
||||
onEventComplete: this.viewOptions.onEventComplete,
|
||||
});
|
||||
|
||||
this.childComponents.push(component);
|
||||
eventItem.appendChild(eventEl);
|
||||
} else {
|
||||
// Fallback rendering without app context
|
||||
eventItem.textContent = event.title;
|
||||
if (event.color) {
|
||||
eventItem.style.borderLeftColor = event.color;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup child components
|
||||
*/
|
||||
protected cleanupChildren(): void {
|
||||
this.childComponents.forEach((c) => c.unload());
|
||||
this.childComponents = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation unit for prev/next buttons
|
||||
*/
|
||||
override getNavigationUnit(): "week" {
|
||||
return "week";
|
||||
}
|
||||
|
||||
/**
|
||||
* Header title for this view
|
||||
*/
|
||||
override getHeaderTitle(): string {
|
||||
const currentDate = this.context.adapter.create(
|
||||
this.context.currentDate as unknown as Date,
|
||||
);
|
||||
const start = moment(currentDate as unknown as Date);
|
||||
const end = start.clone().add(this.daysToShow - 1, "days");
|
||||
|
||||
if (start.isSame(end, "month")) {
|
||||
return `${start.format("MMM D")} - ${end.format("D, YYYY")}`;
|
||||
}
|
||||
return `${start.format("MMM D")} - ${end.format("MMM D, YYYY")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when view is unmounted
|
||||
*/
|
||||
override onUnmount(): void {
|
||||
this.cleanupChildren();
|
||||
super.onUnmount();
|
||||
}
|
||||
}
|
||||
575
src/components/features/calendar/views/tg-year-view.ts
Normal file
575
src/components/features/calendar/views/tg-year-view.ts
Normal file
|
|
@ -0,0 +1,575 @@
|
|||
/**
|
||||
* TGYearView - Year view extending @taskgenius/calendar BaseView
|
||||
*
|
||||
* This view displays a full year with 12 mini-month calendars.
|
||||
* Users can copy this view to create their own custom year-style views.
|
||||
*/
|
||||
import { App, debounce, moment } from "obsidian";
|
||||
import {
|
||||
BaseView,
|
||||
type ViewMeta,
|
||||
type ViewRenderOptions,
|
||||
type CalendarEvent as TGCalendarEvent,
|
||||
} from "@taskgenius/calendar";
|
||||
import { CalendarEvent } from "@/components/features/calendar/index";
|
||||
import {
|
||||
CalendarSpecificConfig,
|
||||
getViewSettingOrDefault,
|
||||
} from "@/common/setting-definition";
|
||||
import TaskProgressBarPlugin from "@/index";
|
||||
|
||||
/**
|
||||
* Options for YearView customization
|
||||
*/
|
||||
export interface YearViewOptions {
|
||||
/** First day of week (0=Sunday, 1=Monday, etc.) */
|
||||
firstDayOfWeek?: 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
||||
/** Whether to hide weekends */
|
||||
hideWeekends?: boolean;
|
||||
/** Day click callback */
|
||||
onDayClick?: (
|
||||
ev: MouseEvent,
|
||||
timestamp: number,
|
||||
options: { behavior: "open-quick-capture" | "open-task-view" },
|
||||
) => void;
|
||||
/** Day hover callback */
|
||||
onDayHover?: (ev: MouseEvent, timestamp: number) => void;
|
||||
/** Month click callback */
|
||||
onMonthClick?: (ev: MouseEvent, timestamp: number) => void;
|
||||
/** Month hover callback */
|
||||
onMonthHover?: (ev: MouseEvent, timestamp: number) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* YearView - Displays a full year with 12 mini-month calendars
|
||||
*
|
||||
* Extends BaseView from @taskgenius/calendar for integration with the calendar system.
|
||||
*
|
||||
* @example Creating a custom year view
|
||||
* ```typescript
|
||||
* class MyCustomYearView extends TGYearView {
|
||||
* static override meta: ViewMeta = {
|
||||
* type: 'my-year',
|
||||
* label: 'My Year',
|
||||
* shortLabel: 'MY',
|
||||
* order: 60
|
||||
* };
|
||||
*
|
||||
* // Override to customize mini-month rendering
|
||||
* protected override renderMiniMonth(container: HTMLElement, month: number, year: number) {
|
||||
* // Custom rendering logic
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export class TGYearView<T = unknown> extends BaseView<T> {
|
||||
static meta: ViewMeta = {
|
||||
type: "year",
|
||||
label: "Year",
|
||||
shortLabel: "Y",
|
||||
order: 50,
|
||||
};
|
||||
|
||||
/** Plugin instance for accessing settings */
|
||||
protected plugin: TaskProgressBarPlugin | null = null;
|
||||
|
||||
/** App instance for Obsidian integration */
|
||||
protected app: App | null = null;
|
||||
|
||||
/** Custom options for this view */
|
||||
protected viewOptions: YearViewOptions = {};
|
||||
|
||||
/** Override config from settings */
|
||||
protected overrideConfig?: Partial<CalendarSpecificConfig>;
|
||||
|
||||
/** DOM event cleanup functions */
|
||||
private cleanupFns: (() => void)[] = [];
|
||||
|
||||
/**
|
||||
* Set plugin and app references (called after construction)
|
||||
*/
|
||||
setPluginContext(plugin: TaskProgressBarPlugin, app: App): void {
|
||||
this.plugin = plugin;
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set custom options for this view
|
||||
*/
|
||||
setOptions(options: YearViewOptions): void {
|
||||
this.viewOptions = options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set override config
|
||||
*/
|
||||
setOverrideConfig(config?: Partial<CalendarSpecificConfig>): void {
|
||||
this.overrideConfig = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the year view
|
||||
*/
|
||||
render(
|
||||
container: HTMLElement,
|
||||
events: TGCalendarEvent[],
|
||||
_options?: ViewRenderOptions,
|
||||
): void {
|
||||
// Cleanup previous event listeners
|
||||
this.cleanupEventListeners();
|
||||
|
||||
const currentDate = this.context.adapter.create(
|
||||
this.context.currentDate as unknown as Date,
|
||||
);
|
||||
const year = moment(currentDate as unknown as Date).year();
|
||||
|
||||
container.empty();
|
||||
container.addClass("view-year", "tg-year-view");
|
||||
|
||||
// Convert events
|
||||
const calendarEvents = this.convertEvents(events);
|
||||
|
||||
// Create year grid
|
||||
const yearGrid = container.createDiv("calendar-year-grid");
|
||||
|
||||
// Filter events for this year
|
||||
const yearStart = moment({ year, month: 0, day: 1 });
|
||||
const yearEnd = moment({ year, month: 11, day: 31 });
|
||||
const yearEvents = this.filterEventsForYear(
|
||||
calendarEvents,
|
||||
yearStart,
|
||||
yearEnd,
|
||||
);
|
||||
|
||||
// Get settings
|
||||
const { firstDayOfWeek, hideWeekends } = this.getEffectiveSettings();
|
||||
|
||||
// Apply weekend hiding class
|
||||
if (hideWeekends) {
|
||||
container.addClass("hide-weekends");
|
||||
} else {
|
||||
container.removeClass("hide-weekends");
|
||||
}
|
||||
|
||||
// Render 12 months
|
||||
for (let month = 0; month < 12; month++) {
|
||||
this.renderMiniMonth(
|
||||
yearGrid,
|
||||
year,
|
||||
month,
|
||||
yearEvents,
|
||||
firstDayOfWeek,
|
||||
hideWeekends,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert TGCalendarEvent to CalendarEvent
|
||||
*/
|
||||
protected convertEvents(events: TGCalendarEvent[]): CalendarEvent[] {
|
||||
return events.map((e) => ({
|
||||
...e,
|
||||
start: new Date(e.start),
|
||||
end: e.end ? new Date(e.end) : undefined,
|
||||
allDay: this.isAllDayEvent(e),
|
||||
metadata: e.metadata as CalendarEvent["metadata"],
|
||||
})) as unknown as CalendarEvent[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if event is all-day
|
||||
*/
|
||||
protected isAllDayEvent(event: TGCalendarEvent): boolean {
|
||||
const start = new Date(event.start);
|
||||
const end = event.end ? new Date(event.end) : start;
|
||||
return (
|
||||
start.getHours() === 0 &&
|
||||
start.getMinutes() === 0 &&
|
||||
(end.getHours() === 0 ||
|
||||
(end.getHours() === 23 && end.getMinutes() === 59))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter events for the year
|
||||
*/
|
||||
protected filterEventsForYear(
|
||||
events: CalendarEvent[],
|
||||
yearStart: moment.Moment,
|
||||
yearEnd: moment.Moment,
|
||||
): CalendarEvent[] {
|
||||
return events.filter((e) => {
|
||||
const start = moment(e.start);
|
||||
const end = e.end ? moment(e.end) : start;
|
||||
return (
|
||||
start.isSameOrBefore(yearEnd.endOf("day")) &&
|
||||
end.isSameOrAfter(yearStart.startOf("day"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective settings from plugin config and overrides
|
||||
*/
|
||||
protected getEffectiveSettings(): {
|
||||
firstDayOfWeek: number;
|
||||
hideWeekends: boolean;
|
||||
} {
|
||||
let firstDayOfWeek = 0;
|
||||
let hideWeekends = false;
|
||||
|
||||
if (this.plugin) {
|
||||
const viewConfig = getViewSettingOrDefault(this.plugin, "calendar");
|
||||
const specificConfig =
|
||||
viewConfig.specificConfig as CalendarSpecificConfig;
|
||||
firstDayOfWeek =
|
||||
this.overrideConfig?.firstDayOfWeek ??
|
||||
this.viewOptions.firstDayOfWeek ??
|
||||
specificConfig.firstDayOfWeek ??
|
||||
0;
|
||||
hideWeekends =
|
||||
this.overrideConfig?.hideWeekends ??
|
||||
this.viewOptions.hideWeekends ??
|
||||
specificConfig?.hideWeekends ??
|
||||
false;
|
||||
} else {
|
||||
firstDayOfWeek = this.viewOptions.firstDayOfWeek ?? 0;
|
||||
hideWeekends = this.viewOptions.hideWeekends ?? false;
|
||||
}
|
||||
|
||||
return { firstDayOfWeek, hideWeekends };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single mini-month
|
||||
*/
|
||||
protected renderMiniMonth(
|
||||
container: HTMLElement,
|
||||
year: number,
|
||||
month: number,
|
||||
yearEvents: CalendarEvent[],
|
||||
firstDayOfWeek: number,
|
||||
hideWeekends: boolean,
|
||||
): void {
|
||||
const monthContainer = container.createDiv("calendar-mini-month");
|
||||
const monthMoment = moment({ year, month, day: 1 });
|
||||
|
||||
// Month header
|
||||
const monthHeader = monthContainer.createDiv("mini-month-header");
|
||||
monthHeader.textContent = monthMoment.format("MMMM");
|
||||
monthHeader.style.cursor = "pointer";
|
||||
|
||||
// Month header click
|
||||
const monthClickHandler = (ev: MouseEvent) => {
|
||||
if (this.viewOptions.onMonthClick) {
|
||||
this.viewOptions.onMonthClick(ev, monthMoment.valueOf());
|
||||
}
|
||||
};
|
||||
monthHeader.addEventListener("click", monthClickHandler);
|
||||
this.cleanupFns.push(() =>
|
||||
monthHeader.removeEventListener("click", monthClickHandler),
|
||||
);
|
||||
|
||||
// Month header hover
|
||||
const monthHoverHandler = (ev: MouseEvent) => {
|
||||
if (this.viewOptions.onMonthHover) {
|
||||
this.viewOptions.onMonthHover(ev, monthMoment.valueOf());
|
||||
}
|
||||
};
|
||||
monthHeader.addEventListener("mouseenter", monthHoverHandler);
|
||||
this.cleanupFns.push(() =>
|
||||
monthHeader.removeEventListener("mouseenter", monthHoverHandler),
|
||||
);
|
||||
|
||||
// Month body
|
||||
const monthBody = monthContainer.createDiv("mini-month-body");
|
||||
const daysWithEvents = this.calculateDaysWithEvents(
|
||||
monthMoment,
|
||||
yearEvents,
|
||||
);
|
||||
|
||||
this.renderMiniMonthGrid(
|
||||
monthBody,
|
||||
monthMoment,
|
||||
daysWithEvents,
|
||||
firstDayOfWeek,
|
||||
hideWeekends,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate which days have events
|
||||
*/
|
||||
protected calculateDaysWithEvents(
|
||||
monthMoment: moment.Moment,
|
||||
events: CalendarEvent[],
|
||||
): Set<number> {
|
||||
const days = new Set<number>();
|
||||
const monthStart = monthMoment.clone().startOf("month");
|
||||
const monthEnd = monthMoment.clone().endOf("month");
|
||||
|
||||
events.forEach((event) => {
|
||||
const datesToCheck = [
|
||||
event.start,
|
||||
event.metadata?.scheduledDate,
|
||||
event.metadata?.dueDate,
|
||||
];
|
||||
|
||||
datesToCheck.forEach((dateInput) => {
|
||||
if (dateInput) {
|
||||
const dateMoment = moment(dateInput);
|
||||
if (
|
||||
dateMoment.isBetween(monthStart, monthEnd, "day", "[]")
|
||||
) {
|
||||
days.add(dateMoment.date());
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the mini-month grid
|
||||
*/
|
||||
protected renderMiniMonthGrid(
|
||||
container: HTMLElement,
|
||||
monthMoment: moment.Moment,
|
||||
daysWithEvents: Set<number>,
|
||||
firstDayOfWeek: number,
|
||||
hideWeekends: boolean,
|
||||
): void {
|
||||
container.empty();
|
||||
container.addClass("mini-month-grid");
|
||||
|
||||
// Weekday headers
|
||||
this.renderWeekdayHeaders(container, firstDayOfWeek, hideWeekends);
|
||||
|
||||
// Calculate grid boundaries
|
||||
const { gridStart, gridEnd } = this.calculateGridBoundaries(
|
||||
monthMoment,
|
||||
firstDayOfWeek,
|
||||
hideWeekends,
|
||||
);
|
||||
|
||||
// Render day cells
|
||||
this.renderDayCells(
|
||||
container,
|
||||
monthMoment,
|
||||
gridStart,
|
||||
gridEnd,
|
||||
daysWithEvents,
|
||||
hideWeekends,
|
||||
);
|
||||
|
||||
// Setup event listeners for the grid
|
||||
this.setupGridEventListeners(container);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render weekday headers
|
||||
*/
|
||||
protected renderWeekdayHeaders(
|
||||
container: HTMLElement,
|
||||
firstDayOfWeek: number,
|
||||
hideWeekends: boolean,
|
||||
): void {
|
||||
const headerRow = container.createDiv("mini-weekday-header");
|
||||
const weekdays = moment.weekdaysMin(true);
|
||||
const rotatedWeekdays = [
|
||||
...weekdays.slice(firstDayOfWeek),
|
||||
...weekdays.slice(0, firstDayOfWeek),
|
||||
];
|
||||
|
||||
const filteredWeekdays = hideWeekends
|
||||
? rotatedWeekdays.filter((_, index) => {
|
||||
const dayOfWeek = (firstDayOfWeek + index) % 7;
|
||||
return dayOfWeek !== 0 && dayOfWeek !== 6;
|
||||
})
|
||||
: rotatedWeekdays;
|
||||
|
||||
filteredWeekdays.forEach((day) => {
|
||||
headerRow.createDiv("mini-weekday").textContent = day;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate grid start and end dates
|
||||
*/
|
||||
protected calculateGridBoundaries(
|
||||
monthMoment: moment.Moment,
|
||||
firstDayOfWeek: number,
|
||||
hideWeekends: boolean,
|
||||
): { gridStart: moment.Moment; gridEnd: moment.Moment } {
|
||||
const monthStart = monthMoment.clone().startOf("month");
|
||||
const monthEnd = monthMoment.clone().endOf("month");
|
||||
|
||||
let gridStart: moment.Moment;
|
||||
let gridEnd: moment.Moment;
|
||||
|
||||
if (hideWeekends) {
|
||||
gridStart = monthStart.clone();
|
||||
const daysToSubtractStart =
|
||||
(monthStart.weekday() - firstDayOfWeek + 7) % 7;
|
||||
gridStart.subtract(daysToSubtractStart, "days");
|
||||
|
||||
while (gridStart.day() === 0 || gridStart.day() === 6) {
|
||||
gridStart.add(1, "day");
|
||||
}
|
||||
|
||||
gridEnd = monthEnd.clone();
|
||||
const daysToAddEnd =
|
||||
(firstDayOfWeek + 4 - monthEnd.weekday() + 7) % 7;
|
||||
gridEnd.add(daysToAddEnd, "days");
|
||||
|
||||
while (gridEnd.day() === 0 || gridEnd.day() === 6) {
|
||||
gridEnd.subtract(1, "day");
|
||||
}
|
||||
} else {
|
||||
const daysToSubtractStart =
|
||||
(monthStart.weekday() - firstDayOfWeek + 7) % 7;
|
||||
gridStart = monthStart
|
||||
.clone()
|
||||
.subtract(daysToSubtractStart, "days");
|
||||
|
||||
const daysToAddEnd =
|
||||
(firstDayOfWeek + 6 - monthEnd.weekday() + 7) % 7;
|
||||
gridEnd = monthEnd.clone().add(daysToAddEnd, "days");
|
||||
}
|
||||
|
||||
return { gridStart, gridEnd };
|
||||
}
|
||||
|
||||
/**
|
||||
* Render day cells
|
||||
*/
|
||||
protected renderDayCells(
|
||||
container: HTMLElement,
|
||||
monthMoment: moment.Moment,
|
||||
gridStart: moment.Moment,
|
||||
gridEnd: moment.Moment,
|
||||
daysWithEvents: Set<number>,
|
||||
hideWeekends: boolean,
|
||||
): void {
|
||||
const currentDayIter = gridStart.clone();
|
||||
|
||||
while (currentDayIter.isSameOrBefore(gridEnd, "day")) {
|
||||
const isWeekend =
|
||||
currentDayIter.day() === 0 || currentDayIter.day() === 6;
|
||||
|
||||
if (hideWeekends && isWeekend) {
|
||||
currentDayIter.add(1, "day");
|
||||
continue;
|
||||
}
|
||||
|
||||
const cell = container.createEl("div", {
|
||||
cls: "mini-day-cell",
|
||||
attr: {
|
||||
"data-date": currentDayIter.format("YYYY-MM-DD"),
|
||||
},
|
||||
});
|
||||
|
||||
const dayNumber = currentDayIter.date();
|
||||
const isCurrentMonth = currentDayIter.isSame(monthMoment, "month");
|
||||
|
||||
cell.textContent = String(dayNumber);
|
||||
|
||||
if (!isCurrentMonth) {
|
||||
cell.addClass("is-other-month");
|
||||
}
|
||||
|
||||
if (currentDayIter.isSame(moment(), "day")) {
|
||||
cell.addClass("is-today");
|
||||
}
|
||||
|
||||
if (isCurrentMonth && daysWithEvents.has(dayNumber)) {
|
||||
cell.addClass("has-events");
|
||||
}
|
||||
|
||||
cell.style.cursor = isCurrentMonth ? "pointer" : "default";
|
||||
|
||||
currentDayIter.add(1, "day");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup event listeners for the grid
|
||||
*/
|
||||
protected setupGridEventListeners(container: HTMLElement): void {
|
||||
// Click handler
|
||||
const clickHandler = (ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement;
|
||||
const cell = target.closest(".mini-day-cell");
|
||||
if (cell) {
|
||||
const dateStr = cell.getAttribute("data-date");
|
||||
if (dateStr && this.viewOptions.onDayClick) {
|
||||
this.viewOptions.onDayClick(ev, moment(dateStr).valueOf(), {
|
||||
behavior: "open-task-view",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
container.addEventListener("click", clickHandler);
|
||||
this.cleanupFns.push(() =>
|
||||
container.removeEventListener("click", clickHandler),
|
||||
);
|
||||
|
||||
// Hover handler (debounced)
|
||||
const hoverHandler = this.createDebouncedHoverHandler();
|
||||
container.addEventListener("mouseover", hoverHandler);
|
||||
this.cleanupFns.push(() =>
|
||||
container.removeEventListener("mouseover", hoverHandler),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create debounced hover handler
|
||||
*/
|
||||
protected createDebouncedHoverHandler(): (ev: MouseEvent) => void {
|
||||
return debounce((ev: MouseEvent) => {
|
||||
const target = ev.target as HTMLElement;
|
||||
const cell = target.closest(".mini-day-cell");
|
||||
if (cell) {
|
||||
const dateStr = cell.getAttribute("data-date");
|
||||
if (dateStr && this.viewOptions.onDayHover) {
|
||||
this.viewOptions.onDayHover(ev, moment(dateStr).valueOf());
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup event listeners
|
||||
*/
|
||||
protected cleanupEventListeners(): void {
|
||||
this.cleanupFns.forEach((fn) => fn());
|
||||
this.cleanupFns = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigation unit for prev/next buttons
|
||||
*/
|
||||
override getNavigationUnit(): "year" {
|
||||
return "year";
|
||||
}
|
||||
|
||||
/**
|
||||
* Header title for this view
|
||||
*/
|
||||
override getHeaderTitle(): string {
|
||||
const currentDate = this.context.adapter.create(
|
||||
this.context.currentDate as unknown as Date,
|
||||
);
|
||||
return moment(currentDate as unknown as Date).format("YYYY");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup when view is unmounted
|
||||
*/
|
||||
override onUnmount(): void {
|
||||
this.cleanupEventListeners();
|
||||
super.onUnmount();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,14 @@
|
|||
/**
|
||||
* @deprecated This file is deprecated. Use `tg-year-view.ts` instead.
|
||||
*
|
||||
* The new TGYearView extends @taskgenius/calendar's BaseView for better
|
||||
* integration with the calendar system. This legacy implementation is kept
|
||||
* for backward compatibility but should not be used for new development.
|
||||
*
|
||||
* @see {@link ./tg-year-view.ts} for the new implementation
|
||||
*/
|
||||
import { App, Component, debounce, moment } from "obsidian";
|
||||
import { CalendarEvent } from '@/components/features/calendar/index';
|
||||
import { CalendarEvent } from "@/components/features/calendar/index";
|
||||
import {
|
||||
CalendarSpecificConfig,
|
||||
getViewSettingOrDefault,
|
||||
|
|
@ -8,7 +17,9 @@ import TaskProgressBarPlugin from "@/index"; // Import plugin type for settings
|
|||
import { CalendarViewComponent, CalendarViewOptions } from "./base-view"; // Import base class
|
||||
|
||||
/**
|
||||
* Renders the year view grid as a component.
|
||||
* @deprecated Use TGYearView from `tg-year-view.ts` instead.
|
||||
* This class extends the legacy CalendarViewComponent (Obsidian Component).
|
||||
* The new TGYearView extends @taskgenius/calendar's BaseView for unified view management.
|
||||
*/
|
||||
export class YearView extends CalendarViewComponent {
|
||||
// Extend base class
|
||||
|
|
@ -18,8 +29,7 @@ export class YearView extends CalendarViewComponent {
|
|||
private app: App; // Keep app reference
|
||||
private plugin: TaskProgressBarPlugin; // Keep plugin reference
|
||||
// Removed specific click/hover properties, use this.options
|
||||
private overrideConfig?: Partial<CalendarSpecificConfig>;
|
||||
|
||||
private overrideConfig?: Partial<CalendarSpecificConfig>;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
|
|
@ -28,7 +38,7 @@ export class YearView extends CalendarViewComponent {
|
|||
currentDate: moment.Moment,
|
||||
events: CalendarEvent[],
|
||||
options: CalendarViewOptions, // Use base options type
|
||||
overrideConfig?: Partial<CalendarSpecificConfig>
|
||||
overrideConfig?: Partial<CalendarSpecificConfig>,
|
||||
) {
|
||||
super(plugin, app, containerEl, events, options); // Call base constructor
|
||||
this.app = app;
|
||||
|
|
@ -43,7 +53,7 @@ export class YearView extends CalendarViewComponent {
|
|||
this.containerEl.addClass("view-year");
|
||||
|
||||
console.log(
|
||||
`YearView: Rendering year ${year}. Total events received: ${this.events.length}`
|
||||
`YearView: Rendering year ${year}. Total events received: ${this.events.length}`,
|
||||
); // Log total events
|
||||
|
||||
// Create a grid container for the 12 months (e.g., 4x3)
|
||||
|
|
@ -65,13 +75,20 @@ export class YearView extends CalendarViewComponent {
|
|||
console.log(
|
||||
`YearView: Filtered ${
|
||||
yearEvents.length
|
||||
} events for year ${year} in ${endTimeFilter - startTimeFilter}ms`
|
||||
} events for year ${year} in ${endTimeFilter - startTimeFilter}ms`,
|
||||
); // Log filtering time
|
||||
|
||||
// Get view settings (prefer override when provided)
|
||||
const viewConfig = getViewSettingOrDefault(this.plugin, "calendar"); // Adjust if needed
|
||||
const firstDayOfWeekSetting = (this.overrideConfig?.firstDayOfWeek ?? (viewConfig.specificConfig as CalendarSpecificConfig).firstDayOfWeek);
|
||||
const hideWeekends = (this.overrideConfig?.hideWeekends ?? (viewConfig.specificConfig as CalendarSpecificConfig)?.hideWeekends) ?? false;
|
||||
const firstDayOfWeekSetting =
|
||||
this.overrideConfig?.firstDayOfWeek ??
|
||||
(viewConfig.specificConfig as CalendarSpecificConfig)
|
||||
.firstDayOfWeek;
|
||||
const hideWeekends =
|
||||
this.overrideConfig?.hideWeekends ??
|
||||
(viewConfig.specificConfig as CalendarSpecificConfig)
|
||||
?.hideWeekends ??
|
||||
false;
|
||||
|
||||
// Add hide-weekends class if weekend hiding is enabled
|
||||
if (hideWeekends) {
|
||||
|
|
@ -115,7 +132,7 @@ export class YearView extends CalendarViewComponent {
|
|||
const monthBody = monthContainer.createDiv("mini-month-body");
|
||||
const daysWithEvents = this.calculateDaysWithEvents(
|
||||
monthMoment,
|
||||
yearEvents // Pass already filtered year events
|
||||
yearEvents, // Pass already filtered year events
|
||||
);
|
||||
|
||||
this.renderMiniMonthGrid(
|
||||
|
|
@ -123,7 +140,7 @@ export class YearView extends CalendarViewComponent {
|
|||
monthMoment,
|
||||
daysWithEvents,
|
||||
effectiveFirstDay,
|
||||
hideWeekends
|
||||
hideWeekends,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -131,14 +148,14 @@ export class YearView extends CalendarViewComponent {
|
|||
console.log(
|
||||
`YearView: Finished rendering year ${year} in ${
|
||||
totalRenderEndTime - totalRenderStartTime
|
||||
}ms. (First day: ${effectiveFirstDay})`
|
||||
}ms. (First day: ${effectiveFirstDay})`,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper function to calculate which days in a month have events
|
||||
private calculateDaysWithEvents(
|
||||
monthMoment: moment.Moment,
|
||||
relevantEvents: CalendarEvent[] // Use the pre-filtered events
|
||||
relevantEvents: CalendarEvent[], // Use the pre-filtered events
|
||||
): Set<number> {
|
||||
const days = new Set<number>();
|
||||
const monthStart = monthMoment.clone().startOf("month");
|
||||
|
|
@ -182,7 +199,7 @@ export class YearView extends CalendarViewComponent {
|
|||
monthMoment: moment.Moment,
|
||||
daysWithEvents: Set<number>,
|
||||
effectiveFirstDay: number, // Pass the effective first day
|
||||
hideWeekends: boolean // Pass the weekend hiding setting
|
||||
hideWeekends: boolean, // Pass the weekend hiding setting
|
||||
) {
|
||||
container.empty(); // Clear placeholder
|
||||
container.addClass("mini-month-grid");
|
||||
|
|
@ -198,10 +215,10 @@ export class YearView extends CalendarViewComponent {
|
|||
// Filter out weekends if hideWeekends is enabled
|
||||
const filteredWeekdays = hideWeekends
|
||||
? rotatedWeekdays.filter((_, index) => {
|
||||
// Calculate the actual day of week for this header position
|
||||
const dayOfWeek = (effectiveFirstDay + index) % 7;
|
||||
return dayOfWeek !== 0 && dayOfWeek !== 6; // Exclude Sunday (0) and Saturday (6)
|
||||
})
|
||||
// Calculate the actual day of week for this header position
|
||||
const dayOfWeek = (effectiveFirstDay + index) % 7;
|
||||
return dayOfWeek !== 0 && dayOfWeek !== 6; // Exclude Sunday (0) and Saturday (6)
|
||||
})
|
||||
: rotatedWeekdays;
|
||||
|
||||
filteredWeekdays.forEach((day) => {
|
||||
|
|
@ -219,7 +236,8 @@ export class YearView extends CalendarViewComponent {
|
|||
// When weekends are hidden, adjust grid to start and end on work days
|
||||
// Find the first work day of the week containing the start of month
|
||||
gridStart = monthStart.clone();
|
||||
const daysToSubtractStart = (monthStart.weekday() - effectiveFirstDay + 7) % 7;
|
||||
const daysToSubtractStart =
|
||||
(monthStart.weekday() - effectiveFirstDay + 7) % 7;
|
||||
gridStart.subtract(daysToSubtractStart, "days");
|
||||
|
||||
// Ensure gridStart is not a weekend
|
||||
|
|
@ -229,7 +247,8 @@ export class YearView extends CalendarViewComponent {
|
|||
|
||||
// Find the last work day of the week containing the end of month
|
||||
gridEnd = monthEnd.clone();
|
||||
const daysToAddEnd = (effectiveFirstDay + 4 - monthEnd.weekday() + 7) % 7; // 4 = Friday in work week
|
||||
const daysToAddEnd =
|
||||
(effectiveFirstDay + 4 - monthEnd.weekday() + 7) % 7; // 4 = Friday in work week
|
||||
gridEnd.add(daysToAddEnd, "days");
|
||||
|
||||
// Ensure gridEnd is not a weekend
|
||||
|
|
@ -238,16 +257,21 @@ export class YearView extends CalendarViewComponent {
|
|||
}
|
||||
} else {
|
||||
// Original logic for when weekends are shown
|
||||
const daysToSubtractStart = (monthStart.weekday() - effectiveFirstDay + 7) % 7;
|
||||
gridStart = monthStart.clone().subtract(daysToSubtractStart, "days");
|
||||
const daysToSubtractStart =
|
||||
(monthStart.weekday() - effectiveFirstDay + 7) % 7;
|
||||
gridStart = monthStart
|
||||
.clone()
|
||||
.subtract(daysToSubtractStart, "days");
|
||||
|
||||
const daysToAddEnd = (effectiveFirstDay + 6 - monthEnd.weekday() + 7) % 7;
|
||||
const daysToAddEnd =
|
||||
(effectiveFirstDay + 6 - monthEnd.weekday() + 7) % 7;
|
||||
gridEnd = monthEnd.clone().add(daysToAddEnd, "days");
|
||||
}
|
||||
|
||||
let currentDayIter = gridStart.clone();
|
||||
while (currentDayIter.isSameOrBefore(gridEnd, "day")) {
|
||||
const isWeekend = currentDayIter.day() === 0 || currentDayIter.day() === 6; // Sunday or Saturday
|
||||
const isWeekend =
|
||||
currentDayIter.day() === 0 || currentDayIter.day() === 6; // Sunday or Saturday
|
||||
|
||||
// Skip weekend days if hideWeekends is enabled
|
||||
if (hideWeekends && isWeekend) {
|
||||
|
|
|
|||
|
|
@ -467,7 +467,7 @@ export class WorkspaceManager {
|
|||
if (this.isSaving) {
|
||||
console.warn(
|
||||
"[TG-WORKSPACE] Recursive saveOverrides detected, skipping to prevent loop",
|
||||
{ workspaceId }
|
||||
{ workspaceId },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -783,6 +783,7 @@ export class WorkspaceManager {
|
|||
views: [],
|
||||
sidebarComponents: [],
|
||||
features: [],
|
||||
calendarViews: [],
|
||||
};
|
||||
}
|
||||
if (!workspace.settings.hiddenModules.views) {
|
||||
|
|
@ -797,6 +798,9 @@ export class WorkspaceManager {
|
|||
// Features can no longer be hidden; normalize to empty
|
||||
workspace.settings.hiddenModules.features = [];
|
||||
}
|
||||
if (!workspace.settings.hiddenModules.calendarViews) {
|
||||
workspace.settings.hiddenModules.calendarViews = [];
|
||||
}
|
||||
return workspace.settings
|
||||
.hiddenModules as Required<HiddenModulesConfig>;
|
||||
}
|
||||
|
|
@ -999,6 +1003,32 @@ export class WorkspaceManager {
|
|||
emitWorkspaceOverridesSaved(this.app, workspace.id, ["hiddenModules"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hidden custom calendar views for a workspace
|
||||
* @param calendarViewIds - Array of custom calendar view IDs to hide
|
||||
* @param workspaceId - Optional workspace ID, defaults to active workspace
|
||||
*/
|
||||
public async setHiddenCalendarViews(
|
||||
calendarViewIds: string[],
|
||||
workspaceId?: string,
|
||||
): Promise<void> {
|
||||
const workspace = workspaceId
|
||||
? this.getWorkspace(workspaceId)
|
||||
: this.getActiveWorkspace();
|
||||
|
||||
if (!workspace) return;
|
||||
|
||||
// Ensure complete initialization and get the initialized object
|
||||
const hiddenModules = this.ensureHiddenModulesInitialized(workspace);
|
||||
hiddenModules.calendarViews = [...calendarViewIds];
|
||||
|
||||
workspace.updatedAt = Date.now();
|
||||
this.clearCache();
|
||||
await this.plugin.saveSettings();
|
||||
|
||||
emitWorkspaceOverridesSaved(this.app, workspace.id, ["hiddenModules"]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set hidden features for a workspace
|
||||
* @param featureIds - Array of feature IDs to hide
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ export { renderTimelineSidebarSettingsTab } from "./tabs/TimelineSidebarSettings
|
|||
export { renderIndexSettingsTab } from "./tabs/IndexSettingsTab";
|
||||
export { createFileSourceSettings } from "./components/FileSourceSettingsSection";
|
||||
export { renderDesktopIntegrationSettingsTab } from "./tabs/DesktopIntegrationSettingsTab";
|
||||
export { renderCalendarViewSettingsTab } from "./tabs/CalendarViewSettingTab";
|
||||
|
|
|
|||
477
src/components/features/settings/tabs/CalendarViewSettingTab.ts
Normal file
477
src/components/features/settings/tabs/CalendarViewSettingTab.ts
Normal file
|
|
@ -0,0 +1,477 @@
|
|||
import { Setting, Notice, setIcon, ButtonComponent } from "obsidian";
|
||||
import { TaskProgressBarSettingTab } from "@/setting";
|
||||
import { CustomCalendarViewConfig } from "@/common/setting-definition";
|
||||
import { t } from "@/translations/helper";
|
||||
import { CalendarViewConfigModal } from "@/components/ui/modals/CalendarViewConfigModal";
|
||||
import Sortable from "sortablejs";
|
||||
import "@/styles/calendar-view-settings.css";
|
||||
|
||||
/**
|
||||
* Built-in calendar view templates that users can use as base
|
||||
*/
|
||||
const BUILTIN_VIEW_TEMPLATES = [
|
||||
{
|
||||
type: "month" as const,
|
||||
name: t("Month View"),
|
||||
icon: "calendar",
|
||||
description: t("Display tasks in a monthly calendar grid"),
|
||||
},
|
||||
{
|
||||
type: "week" as const,
|
||||
name: t("Week View"),
|
||||
icon: "calendar-range",
|
||||
description: t("Display tasks in a weekly time grid with hourly slots"),
|
||||
},
|
||||
{
|
||||
type: "day" as const,
|
||||
name: t("Day View"),
|
||||
icon: "calendar-days",
|
||||
description: t(
|
||||
"Display tasks for a single day with detailed time slots",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "agenda" as const,
|
||||
name: t("Agenda View"),
|
||||
icon: "list",
|
||||
description: t(
|
||||
"Display upcoming tasks in a list format grouped by day",
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "year" as const,
|
||||
name: t("Year View"),
|
||||
icon: "calendar-clock",
|
||||
description: t("Display a full year with 12 mini-month calendars"),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Render the Calendar View Settings Tab
|
||||
*/
|
||||
export function renderCalendarViewSettingsTab(
|
||||
settingTab: TaskProgressBarSettingTab,
|
||||
containerEl: HTMLElement,
|
||||
) {
|
||||
// Header
|
||||
new Setting(containerEl)
|
||||
.setName(t("Calendar View Configuration"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Create custom calendar views based on Month, Week, or Day views. Each custom view can have its own display settings.",
|
||||
),
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
// Built-in Templates Section
|
||||
new Setting(containerEl)
|
||||
.setName(t("Built-in View Templates"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Use these templates as a starting point to create your own custom calendar views.",
|
||||
),
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
const templatesContainer = containerEl.createDiv({
|
||||
cls: "calendar-templates-container",
|
||||
});
|
||||
|
||||
BUILTIN_VIEW_TEMPLATES.forEach((template) => {
|
||||
const templateCard = templatesContainer.createDiv({
|
||||
cls: "calendar-template-card",
|
||||
});
|
||||
|
||||
// Icon
|
||||
const iconEl = templateCard.createDiv({
|
||||
cls: "calendar-template-icon",
|
||||
});
|
||||
setIcon(iconEl, template.icon);
|
||||
|
||||
// Info
|
||||
const infoEl = templateCard.createDiv({
|
||||
cls: "calendar-template-info",
|
||||
});
|
||||
infoEl.createDiv({
|
||||
cls: "calendar-template-name",
|
||||
text: template.name,
|
||||
});
|
||||
infoEl.createDiv({
|
||||
cls: "calendar-template-description",
|
||||
text: template.description,
|
||||
});
|
||||
|
||||
// Actions
|
||||
const actionsEl = templateCard.createDiv({
|
||||
cls: "calendar-template-actions",
|
||||
});
|
||||
|
||||
const createBtn = new ButtonComponent(actionsEl);
|
||||
createBtn.setButtonText(t("Create Custom View"));
|
||||
createBtn.setIcon("plus");
|
||||
createBtn.onClick(() => {
|
||||
openCreateViewModal(settingTab, template.type, () => {
|
||||
renderCustomViewsList(settingTab, customViewsContainer);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Custom Views Section
|
||||
new Setting(containerEl)
|
||||
.setName(t("Custom Calendar Views"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Your custom calendar views. Drag to reorder, toggle visibility, or edit settings.",
|
||||
),
|
||||
)
|
||||
.setHeading();
|
||||
|
||||
const customViewsContainer = containerEl.createDiv({
|
||||
cls: "custom-calendar-views-container",
|
||||
});
|
||||
|
||||
// Render the list
|
||||
renderCustomViewsList(settingTab, customViewsContainer);
|
||||
|
||||
// Add New View Button
|
||||
const addBtnContainer = containerEl.createDiv({
|
||||
cls: "calendar-add-view-container",
|
||||
});
|
||||
|
||||
new Setting(addBtnContainer).addButton((button) => {
|
||||
button
|
||||
.setButtonText(t("Create New Calendar View"))
|
||||
.setCta()
|
||||
.setIcon("plus")
|
||||
.onClick(() => {
|
||||
openCreateViewModal(settingTab, "month", () => {
|
||||
renderCustomViewsList(settingTab, customViewsContainer);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the list of custom calendar views
|
||||
*/
|
||||
function renderCustomViewsList(
|
||||
settingTab: TaskProgressBarSettingTab,
|
||||
container: HTMLElement,
|
||||
) {
|
||||
container.empty();
|
||||
|
||||
const customViews = settingTab.plugin.settings.customCalendarViews || [];
|
||||
|
||||
if (customViews.length === 0) {
|
||||
const emptyState = container.createDiv({ cls: "calendar-views-empty" });
|
||||
const emptyIcon = emptyState.createDiv({
|
||||
cls: "calendar-views-empty-icon",
|
||||
});
|
||||
setIcon(emptyIcon, "calendar-plus");
|
||||
emptyState.createDiv({
|
||||
cls: "calendar-views-empty-text",
|
||||
text: t("No custom calendar views yet"),
|
||||
});
|
||||
emptyState.createDiv({
|
||||
cls: "calendar-views-empty-hint",
|
||||
text: t(
|
||||
"Create a custom view from the templates above to get started.",
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const listContainer = container.createDiv({
|
||||
cls: "calendar-views-list sortable-calendar-views",
|
||||
});
|
||||
|
||||
// Render each custom view
|
||||
customViews.forEach((view) => {
|
||||
createCustomViewItem(settingTab, listContainer, view, () => {
|
||||
renderCustomViewsList(settingTab, container);
|
||||
});
|
||||
});
|
||||
|
||||
// Setup sortable
|
||||
Sortable.create(listContainer, {
|
||||
animation: 150,
|
||||
handle: ".calendar-view-drag-handle",
|
||||
ghostClass: "sortable-ghost",
|
||||
chosenClass: "sortable-chosen",
|
||||
dragClass: "sortable-drag",
|
||||
onEnd: async (evt) => {
|
||||
const views = settingTab.plugin.settings.customCalendarViews || [];
|
||||
const [movedItem] = views.splice(evt.oldIndex!, 1);
|
||||
views.splice(evt.newIndex!, 0, movedItem);
|
||||
|
||||
// Update order values
|
||||
views.forEach((v, index) => {
|
||||
v.order = index;
|
||||
v.updatedAt = Date.now();
|
||||
});
|
||||
|
||||
settingTab.plugin.settings.customCalendarViews = views;
|
||||
await settingTab.plugin.saveSettings();
|
||||
|
||||
// Notify calendar component
|
||||
(settingTab.app.workspace as any).trigger(
|
||||
"task-genius:calendar-views-changed",
|
||||
{ reason: "order-changed" },
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a single custom view item in the list
|
||||
*/
|
||||
function createCustomViewItem(
|
||||
settingTab: TaskProgressBarSettingTab,
|
||||
container: HTMLElement,
|
||||
view: CustomCalendarViewConfig,
|
||||
onUpdate: () => void,
|
||||
) {
|
||||
const viewEl = container.createDiv({
|
||||
cls: `calendar-view-item ${!view.enabled ? "is-disabled" : ""}`,
|
||||
attr: { "data-view-id": view.id },
|
||||
});
|
||||
|
||||
// Drag handle
|
||||
const dragHandle = viewEl.createDiv({ cls: "calendar-view-drag-handle" });
|
||||
setIcon(dragHandle, "grip-vertical");
|
||||
|
||||
// Icon
|
||||
const iconEl = viewEl.createDiv({ cls: "calendar-view-icon" });
|
||||
setIcon(iconEl, view.icon);
|
||||
|
||||
// Info
|
||||
const infoEl = viewEl.createDiv({ cls: "calendar-view-info" });
|
||||
infoEl.createDiv({ cls: "calendar-view-name", text: view.name });
|
||||
|
||||
const metaEl = infoEl.createDiv({ cls: "calendar-view-meta" });
|
||||
const baseViewLabel = {
|
||||
month: t("Month"),
|
||||
week: t("Week"),
|
||||
day: t("Day"),
|
||||
agenda: t("Agenda"),
|
||||
year: t("Year"),
|
||||
}[view.baseViewType];
|
||||
metaEl.createSpan({
|
||||
cls: "calendar-view-base-type",
|
||||
text: t("Based on: ") + baseViewLabel,
|
||||
});
|
||||
|
||||
// Config summary
|
||||
const configSummary = getConfigSummary(view);
|
||||
if (configSummary) {
|
||||
metaEl.createSpan({
|
||||
cls: "calendar-view-config-summary",
|
||||
text: configSummary,
|
||||
});
|
||||
}
|
||||
|
||||
// Actions
|
||||
const actionsEl = viewEl.createDiv({ cls: "calendar-view-actions" });
|
||||
|
||||
// Toggle enabled
|
||||
const toggleBtn = actionsEl.createEl("button", {
|
||||
cls: ["calendar-view-action-btn", "clickable-icon"],
|
||||
attr: { "aria-label": view.enabled ? t("Disable") : t("Enable") },
|
||||
});
|
||||
setIcon(toggleBtn, view.enabled ? "eye" : "eye-off");
|
||||
toggleBtn.onclick = async () => {
|
||||
view.enabled = !view.enabled;
|
||||
view.updatedAt = Date.now();
|
||||
await settingTab.plugin.saveSettings();
|
||||
onUpdate();
|
||||
(settingTab.app.workspace as any).trigger(
|
||||
"task-genius:calendar-views-changed",
|
||||
{ reason: "visibility-changed", viewId: view.id },
|
||||
);
|
||||
};
|
||||
|
||||
// Edit
|
||||
const editBtn = actionsEl.createEl("button", {
|
||||
cls: ["calendar-view-action-btn", "clickable-icon"],
|
||||
attr: { "aria-label": t("Edit") },
|
||||
});
|
||||
setIcon(editBtn, "pencil");
|
||||
editBtn.onclick = () => {
|
||||
openEditViewModal(settingTab, view, onUpdate);
|
||||
};
|
||||
|
||||
// Duplicate
|
||||
const duplicateBtn = actionsEl.createEl("button", {
|
||||
cls: ["calendar-view-action-btn", "clickable-icon"],
|
||||
attr: { "aria-label": t("Duplicate") },
|
||||
});
|
||||
setIcon(duplicateBtn, "copy");
|
||||
duplicateBtn.onclick = async () => {
|
||||
const newView: CustomCalendarViewConfig = {
|
||||
...JSON.parse(JSON.stringify(view)),
|
||||
id: `custom-calendar-${Date.now()}`,
|
||||
name: view.name + " " + t("(Copy)"),
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
const views = settingTab.plugin.settings.customCalendarViews || [];
|
||||
views.push(newView);
|
||||
settingTab.plugin.settings.customCalendarViews = views;
|
||||
await settingTab.plugin.saveSettings();
|
||||
|
||||
new Notice(t("View duplicated: ") + newView.name);
|
||||
onUpdate();
|
||||
|
||||
(settingTab.app.workspace as any).trigger(
|
||||
"task-genius:calendar-views-changed",
|
||||
{ reason: "view-added", viewId: newView.id },
|
||||
);
|
||||
};
|
||||
|
||||
// Delete
|
||||
const deleteBtn = actionsEl.createEl("button", {
|
||||
cls: [
|
||||
"calendar-view-action-btn",
|
||||
"calendar-view-action-delete",
|
||||
"clickable-icon",
|
||||
],
|
||||
attr: { "aria-label": t("Delete") },
|
||||
});
|
||||
setIcon(deleteBtn, "trash");
|
||||
deleteBtn.onclick = async () => {
|
||||
const views = settingTab.plugin.settings.customCalendarViews || [];
|
||||
const index = views.findIndex((v) => v.id === view.id);
|
||||
if (index !== -1) {
|
||||
views.splice(index, 1);
|
||||
settingTab.plugin.settings.customCalendarViews = views;
|
||||
await settingTab.plugin.saveSettings();
|
||||
|
||||
new Notice(t("View deleted: ") + view.name);
|
||||
onUpdate();
|
||||
|
||||
(settingTab.app.workspace as any).trigger(
|
||||
"task-genius:calendar-views-changed",
|
||||
{ reason: "view-deleted", viewId: view.id },
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a summary of the view configuration for display
|
||||
*/
|
||||
function getConfigSummary(view: CustomCalendarViewConfig): string {
|
||||
const parts: string[] = [];
|
||||
const config = view.calendarConfig;
|
||||
|
||||
if (config.dayFilter?.type === "hideWeekends") {
|
||||
parts.push(t("No weekends"));
|
||||
} else if (config.dayFilter?.type === "hideWeekdays") {
|
||||
parts.push(t("Weekends only"));
|
||||
} else if (
|
||||
config.dayFilter?.type === "customDays" &&
|
||||
config.dayFilter.hiddenDays?.length
|
||||
) {
|
||||
parts.push(t("Custom days"));
|
||||
}
|
||||
|
||||
if (config.timeFilter?.enabled) {
|
||||
parts.push(
|
||||
`${config.timeFilter.startHour}:00-${config.timeFilter.endHour}:00`,
|
||||
);
|
||||
}
|
||||
|
||||
if (config.firstDayOfWeek === 1) {
|
||||
parts.push(t("Mon first"));
|
||||
} else if (config.firstDayOfWeek === 6) {
|
||||
parts.push(t("Sat first"));
|
||||
}
|
||||
|
||||
return parts.join(" · ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal to create a new custom view
|
||||
*/
|
||||
function openCreateViewModal(
|
||||
settingTab: TaskProgressBarSettingTab,
|
||||
baseViewType: "month" | "week" | "day" | "agenda" | "year",
|
||||
onSave: () => void,
|
||||
) {
|
||||
const newView: CustomCalendarViewConfig = {
|
||||
id: `custom-calendar-${Date.now()}`,
|
||||
name: t("New Calendar View"),
|
||||
icon: "calendar",
|
||||
baseViewType,
|
||||
enabled: true,
|
||||
order: (settingTab.plugin.settings.customCalendarViews || []).length,
|
||||
calendarConfig: {
|
||||
firstDayOfWeek: undefined,
|
||||
showWeekNumbers: false,
|
||||
showEventCounts: true,
|
||||
dayFilter: { type: "none" },
|
||||
timeFilter: {
|
||||
enabled: false,
|
||||
type: "workingHours",
|
||||
startHour: 9,
|
||||
endHour: 18,
|
||||
},
|
||||
},
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
|
||||
new CalendarViewConfigModal(
|
||||
settingTab.app,
|
||||
settingTab.plugin,
|
||||
newView,
|
||||
async (savedView) => {
|
||||
const views = settingTab.plugin.settings.customCalendarViews || [];
|
||||
views.push(savedView);
|
||||
settingTab.plugin.settings.customCalendarViews = views;
|
||||
await settingTab.plugin.saveSettings();
|
||||
|
||||
new Notice(t("Calendar view created: ") + savedView.name);
|
||||
onSave();
|
||||
|
||||
(settingTab.app.workspace as any).trigger(
|
||||
"task-genius:calendar-views-changed",
|
||||
{ reason: "view-added", viewId: savedView.id },
|
||||
);
|
||||
},
|
||||
).open();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal to edit an existing custom view
|
||||
*/
|
||||
function openEditViewModal(
|
||||
settingTab: TaskProgressBarSettingTab,
|
||||
view: CustomCalendarViewConfig,
|
||||
onSave: () => void,
|
||||
) {
|
||||
new CalendarViewConfigModal(
|
||||
settingTab.app,
|
||||
settingTab.plugin,
|
||||
{ ...view },
|
||||
async (savedView) => {
|
||||
const views = settingTab.plugin.settings.customCalendarViews || [];
|
||||
const index = views.findIndex((v) => v.id === savedView.id);
|
||||
if (index !== -1) {
|
||||
savedView.updatedAt = Date.now();
|
||||
views[index] = savedView;
|
||||
settingTab.plugin.settings.customCalendarViews = views;
|
||||
await settingTab.plugin.saveSettings();
|
||||
|
||||
new Notice(t("Calendar view updated: ") + savedView.name);
|
||||
onSave();
|
||||
|
||||
(settingTab.app.workspace as any).trigger(
|
||||
"task-genius:calendar-views-changed",
|
||||
{ reason: "view-updated", viewId: savedView.id },
|
||||
);
|
||||
}
|
||||
},
|
||||
).open();
|
||||
}
|
||||
|
|
@ -249,6 +249,7 @@ function showDeleteWorkspaceDialog(
|
|||
function getAvailableModules(plugin: TaskProgressBarPlugin): {
|
||||
views: ModuleDefinition[];
|
||||
sidebarComponents: ModuleDefinition[];
|
||||
calendarViews: ModuleDefinition[];
|
||||
} {
|
||||
// Get view modules from plugin settings
|
||||
const views: ModuleDefinition[] = plugin.settings.viewConfiguration.map(
|
||||
|
|
@ -260,6 +261,16 @@ function getAvailableModules(plugin: TaskProgressBarPlugin): {
|
|||
}),
|
||||
);
|
||||
|
||||
// Get custom calendar views
|
||||
const calendarViews: ModuleDefinition[] = (
|
||||
plugin.settings.customCalendarViews || []
|
||||
).map((calView) => ({
|
||||
id: calView.id,
|
||||
name: calView.name,
|
||||
icon: calView.icon,
|
||||
type: "calendarView" as const,
|
||||
}));
|
||||
|
||||
// Define sidebar component modules (Fluent interface only)
|
||||
const sidebarComponents: ModuleDefinition[] = [
|
||||
{
|
||||
|
|
@ -276,7 +287,7 @@ function getAvailableModules(plugin: TaskProgressBarPlugin): {
|
|||
},
|
||||
];
|
||||
|
||||
return { views, sidebarComponents };
|
||||
return { views, sidebarComponents, calendarViews };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -323,7 +334,7 @@ function renderHiddenModulesConfig(
|
|||
groupIcon: string,
|
||||
moduleList: ModuleDefinition[],
|
||||
initialHiddenList: string[],
|
||||
moduleType: "views" | "sidebarComponents",
|
||||
moduleType: "views" | "sidebarComponents" | "calendarViews",
|
||||
) => {
|
||||
let hiddenList = [...initialHiddenList];
|
||||
|
||||
|
|
@ -412,6 +423,12 @@ function renderHiddenModulesConfig(
|
|||
workspace.id,
|
||||
);
|
||||
break;
|
||||
case "calendarViews":
|
||||
await plugin.workspaceManager?.setHiddenCalendarViews(
|
||||
[...newHiddenList],
|
||||
workspace.id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const refreshed = plugin.workspaceManager?.getWorkspace(
|
||||
|
|
@ -423,6 +440,7 @@ function renderHiddenModulesConfig(
|
|||
hiddenModules = {
|
||||
views: [],
|
||||
sidebarComponents: [],
|
||||
calendarViews: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -434,6 +452,9 @@ function renderHiddenModulesConfig(
|
|||
case "sidebarComponents":
|
||||
hiddenList = hiddenModules.sidebarComponents || [];
|
||||
break;
|
||||
case "calendarViews":
|
||||
hiddenList = hiddenModules.calendarViews || [];
|
||||
break;
|
||||
}
|
||||
|
||||
itemEl.toggleClass("is-hidden", !shouldBeVisible);
|
||||
|
|
@ -491,6 +512,17 @@ function renderHiddenModulesConfig(
|
|||
hiddenModules.sidebarComponents || [],
|
||||
"sidebarComponents",
|
||||
);
|
||||
|
||||
// Only render calendar views group if there are custom calendar views
|
||||
if (modules.calendarViews.length > 0) {
|
||||
renderModuleGroup(
|
||||
t("Custom Calendar Views"),
|
||||
"calendar",
|
||||
modules.calendarViews,
|
||||
hiddenModules.calendarViews || [],
|
||||
"calendarViews",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
642
src/components/ui/modals/CalendarViewConfigModal.ts
Normal file
642
src/components/ui/modals/CalendarViewConfigModal.ts
Normal file
|
|
@ -0,0 +1,642 @@
|
|||
import {
|
||||
App,
|
||||
Modal,
|
||||
Setting,
|
||||
ButtonComponent,
|
||||
DropdownComponent,
|
||||
TextComponent,
|
||||
ToggleComponent,
|
||||
SliderComponent,
|
||||
setIcon,
|
||||
} from "obsidian";
|
||||
import type TaskProgressBarPlugin from "@/index";
|
||||
import {
|
||||
CustomCalendarViewConfig,
|
||||
DayFilterConfig,
|
||||
TimeFilterConfig,
|
||||
} from "@/common/setting-definition";
|
||||
import { t } from "@/translations/helper";
|
||||
import { attachIconMenu } from "@/components/ui/menus/IconMenu";
|
||||
|
||||
/**
|
||||
* Days of the week for custom day filter
|
||||
*/
|
||||
const WEEKDAYS = [
|
||||
{ value: 0, label: "Sunday", short: "Sun" },
|
||||
{ value: 1, label: "Monday", short: "Mon" },
|
||||
{ value: 2, label: "Tuesday", short: "Tue" },
|
||||
{ value: 3, label: "Wednesday", short: "Wed" },
|
||||
{ value: 4, label: "Thursday", short: "Thu" },
|
||||
{ value: 5, label: "Friday", short: "Fri" },
|
||||
{ value: 6, label: "Saturday", short: "Sat" },
|
||||
];
|
||||
|
||||
/**
|
||||
* Modal for creating/editing custom calendar views
|
||||
*/
|
||||
export class CalendarViewConfigModal extends Modal {
|
||||
private view: CustomCalendarViewConfig;
|
||||
private onSave: (view: CustomCalendarViewConfig) => void;
|
||||
private plugin: TaskProgressBarPlugin;
|
||||
private currentTab: "basic" | "date" | "time" | "display" = "basic";
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
plugin: TaskProgressBarPlugin,
|
||||
view: CustomCalendarViewConfig,
|
||||
onSave: (view: CustomCalendarViewConfig) => void,
|
||||
) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
this.view = JSON.parse(JSON.stringify(view)); // Deep clone
|
||||
this.onSave = onSave;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
this.modalEl.addClass("calendar-view-config-modal");
|
||||
|
||||
// Header
|
||||
const headerEl = contentEl.createDiv({ cls: "modal-header" });
|
||||
headerEl.createEl("h2", {
|
||||
text:
|
||||
this.view.createdAt === this.view.updatedAt
|
||||
? t("Create Calendar View")
|
||||
: t("Edit Calendar View"),
|
||||
});
|
||||
|
||||
// Tabs
|
||||
const tabsEl = contentEl.createDiv({ cls: "calendar-config-tabs" });
|
||||
this.renderTabs(tabsEl);
|
||||
|
||||
// Content
|
||||
const contentContainer = contentEl.createDiv({
|
||||
cls: "calendar-config-content",
|
||||
});
|
||||
this.renderTabContent(contentContainer);
|
||||
|
||||
// Footer
|
||||
const footerEl = contentEl.createDiv({ cls: "modal-button-container" });
|
||||
this.renderFooter(footerEl);
|
||||
}
|
||||
|
||||
private renderTabs(container: HTMLElement) {
|
||||
const tabs = [
|
||||
{ id: "basic", label: t("Basic"), icon: "settings" },
|
||||
{ id: "date", label: t("Date Filter"), icon: "calendar" },
|
||||
{ id: "time", label: t("Time Filter"), icon: "clock" },
|
||||
{ id: "display", label: t("Display"), icon: "eye" },
|
||||
] as const;
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
const tabEl = container.createDiv({
|
||||
cls: `calendar-config-tab ${this.currentTab === tab.id ? "is-active" : ""}`,
|
||||
});
|
||||
|
||||
const iconEl = tabEl.createSpan({
|
||||
cls: "calendar-config-tab-icon",
|
||||
});
|
||||
setIcon(iconEl, tab.icon);
|
||||
tabEl.createSpan({
|
||||
cls: "calendar-config-tab-label",
|
||||
text: tab.label,
|
||||
});
|
||||
|
||||
// Disable time tab for month/agenda/year views
|
||||
const isTimeTabDisabled =
|
||||
tab.id === "time" &&
|
||||
(this.view.baseViewType === "month" ||
|
||||
this.view.baseViewType === "agenda" ||
|
||||
this.view.baseViewType === "year");
|
||||
|
||||
if (isTimeTabDisabled) {
|
||||
tabEl.addClass("is-disabled");
|
||||
tabEl.setAttribute("aria-disabled", "true");
|
||||
tabEl.title = t(
|
||||
"Time filter is only available for Week and Day views",
|
||||
);
|
||||
} else {
|
||||
tabEl.onclick = () => {
|
||||
this.currentTab = tab.id;
|
||||
this.refreshContent();
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private renderTabContent(container: HTMLElement) {
|
||||
container.empty();
|
||||
|
||||
switch (this.currentTab) {
|
||||
case "basic":
|
||||
this.renderBasicTab(container);
|
||||
break;
|
||||
case "date":
|
||||
this.renderDateTab(container);
|
||||
break;
|
||||
case "time":
|
||||
this.renderTimeTab(container);
|
||||
break;
|
||||
case "display":
|
||||
this.renderDisplayTab(container);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private renderBasicTab(container: HTMLElement) {
|
||||
// View Name
|
||||
new Setting(container)
|
||||
.setName(t("View Name"))
|
||||
.setDesc(t("Display name for this calendar view"))
|
||||
.addText((text) => {
|
||||
text.setPlaceholder(t("My Custom View"))
|
||||
.setValue(this.view.name)
|
||||
.onChange((value) => {
|
||||
this.view.name = value;
|
||||
});
|
||||
});
|
||||
|
||||
// Icon
|
||||
const iconSetting = new Setting(container)
|
||||
.setName(t("View Icon"))
|
||||
.setDesc(t("Choose an icon for this view"));
|
||||
|
||||
const iconContainer = iconSetting.controlEl.createDiv({
|
||||
cls: "calendar-view-icon-selector",
|
||||
});
|
||||
|
||||
const iconButton = new ButtonComponent(iconContainer);
|
||||
iconButton.setIcon(this.view.icon);
|
||||
|
||||
attachIconMenu(iconButton, {
|
||||
containerEl: iconContainer,
|
||||
plugin: this.plugin,
|
||||
onIconSelected: (iconId: string) => {
|
||||
this.view.icon = iconId;
|
||||
iconButton.setIcon(iconId);
|
||||
},
|
||||
});
|
||||
|
||||
// Base View Type
|
||||
new Setting(container)
|
||||
.setName(t("Base View Type"))
|
||||
.setDesc(t("The calendar layout this view is based on"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("month", t("Month View"))
|
||||
.addOption("week", t("Week View"))
|
||||
.addOption("day", t("Day View"))
|
||||
.addOption("agenda", t("Agenda View"))
|
||||
.addOption("year", t("Year View"))
|
||||
.setValue(this.view.baseViewType)
|
||||
.onChange(
|
||||
(
|
||||
value: "month" | "week" | "day" | "agenda" | "year",
|
||||
) => {
|
||||
this.view.baseViewType = value;
|
||||
// Reset time filter if switching to non-time views
|
||||
if (
|
||||
(value === "month" ||
|
||||
value === "agenda" ||
|
||||
value === "year") &&
|
||||
this.view.calendarConfig.timeFilter
|
||||
) {
|
||||
this.view.calendarConfig.timeFilter.enabled = false;
|
||||
}
|
||||
this.refreshContent();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// First Day of Week (not for agenda view)
|
||||
if (this.view.baseViewType !== "agenda") {
|
||||
new Setting(container)
|
||||
.setName(t("First Day of Week"))
|
||||
.setDesc(t("Which day should the week start on"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("", t("System Default"))
|
||||
.addOption("0", t("Sunday"))
|
||||
.addOption("1", t("Monday"))
|
||||
.addOption("2", t("Tuesday"))
|
||||
.addOption("3", t("Wednesday"))
|
||||
.addOption("4", t("Thursday"))
|
||||
.addOption("5", t("Friday"))
|
||||
.addOption("6", t("Saturday"))
|
||||
.setValue(
|
||||
this.view.calendarConfig.firstDayOfWeek?.toString() ??
|
||||
"",
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.view.calendarConfig.firstDayOfWeek = value
|
||||
? (parseInt(value) as 0 | 1 | 2 | 3 | 4 | 5 | 6)
|
||||
: undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Agenda-specific options
|
||||
if (this.view.baseViewType === "agenda") {
|
||||
new Setting(container)
|
||||
.setName(t("Days to Show"))
|
||||
.setDesc(t("Number of days to display in the agenda"))
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(1, 30, 1)
|
||||
.setValue(
|
||||
(this.view.calendarConfig as any).daysToShow ?? 7,
|
||||
)
|
||||
.setDynamicTooltip()
|
||||
.onChange((value) => {
|
||||
(this.view.calendarConfig as any).daysToShow =
|
||||
value;
|
||||
});
|
||||
});
|
||||
|
||||
new Setting(container)
|
||||
.setName(t("Show Empty Days"))
|
||||
.setDesc(t("Display days with no events"))
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(
|
||||
(this.view.calendarConfig as any).showEmptyDays ??
|
||||
false,
|
||||
)
|
||||
.onChange((value) => {
|
||||
(this.view.calendarConfig as any).showEmptyDays =
|
||||
value;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderDateTab(container: HTMLElement) {
|
||||
const dayFilter = this.view.calendarConfig.dayFilter || {
|
||||
type: "none",
|
||||
};
|
||||
|
||||
// Day Filter Type
|
||||
new Setting(container)
|
||||
.setName(t("Day Filter"))
|
||||
.setDesc(t("Control which days are displayed in the calendar"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption("none", t("Show All Days"))
|
||||
.addOption("hideWeekends", t("Hide Weekends"))
|
||||
.addOption("hideWeekdays", t("Weekends Only"))
|
||||
.addOption("customDays", t("Custom Selection"))
|
||||
.setValue(dayFilter.type)
|
||||
.onChange((value: DayFilterConfig["type"]) => {
|
||||
this.view.calendarConfig.dayFilter = {
|
||||
type: value,
|
||||
hiddenDays:
|
||||
value === "customDays"
|
||||
? dayFilter.hiddenDays || []
|
||||
: undefined,
|
||||
};
|
||||
this.refreshContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Custom Day Selection (only shown when customDays is selected)
|
||||
if (dayFilter.type === "customDays") {
|
||||
const customDaysSetting = new Setting(container)
|
||||
.setName(t("Hidden Days"))
|
||||
.setDesc(t("Select which days to hide from the calendar"));
|
||||
|
||||
const daysContainer = customDaysSetting.controlEl.createDiv({
|
||||
cls: "calendar-days-selector",
|
||||
});
|
||||
|
||||
const hiddenDays = dayFilter.hiddenDays || [];
|
||||
|
||||
WEEKDAYS.forEach((day) => {
|
||||
const dayEl = daysContainer.createDiv({
|
||||
cls: `calendar-day-chip ${hiddenDays.includes(day.value) ? "is-hidden" : ""}`,
|
||||
});
|
||||
dayEl.createSpan({ text: t(day.short) });
|
||||
|
||||
dayEl.onclick = () => {
|
||||
const currentHidden =
|
||||
this.view.calendarConfig.dayFilter?.hiddenDays || [];
|
||||
const index = currentHidden.indexOf(day.value);
|
||||
|
||||
if (index === -1) {
|
||||
currentHidden.push(day.value);
|
||||
} else {
|
||||
currentHidden.splice(index, 1);
|
||||
}
|
||||
|
||||
this.view.calendarConfig.dayFilter = {
|
||||
type: "customDays",
|
||||
hiddenDays: currentHidden,
|
||||
};
|
||||
|
||||
dayEl.toggleClass(
|
||||
"is-hidden",
|
||||
currentHidden.includes(day.value),
|
||||
);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Show Week Numbers (for month view)
|
||||
if (this.view.baseViewType === "month") {
|
||||
new Setting(container)
|
||||
.setName(t("Show Week Numbers"))
|
||||
.setDesc(t("Display week numbers in the month view"))
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(
|
||||
this.view.calendarConfig.showWeekNumbers ?? false,
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.view.calendarConfig.showWeekNumbers = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderTimeTab(container: HTMLElement) {
|
||||
// Only for week/day views
|
||||
if (
|
||||
this.view.baseViewType === "month" ||
|
||||
this.view.baseViewType === "agenda" ||
|
||||
this.view.baseViewType === "year"
|
||||
) {
|
||||
container.createDiv({
|
||||
cls: "calendar-config-notice",
|
||||
text: t(
|
||||
"Time filter is only available for Week and Day views.",
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const timeFilter = this.view.calendarConfig.timeFilter || {
|
||||
enabled: false,
|
||||
type: "workingHours",
|
||||
startHour: 9,
|
||||
endHour: 18,
|
||||
};
|
||||
|
||||
// Enable Time Filter
|
||||
new Setting(container)
|
||||
.setName(t("Enable Time Filter"))
|
||||
.setDesc(t("Only show certain hours of the day"))
|
||||
.addToggle((toggle) => {
|
||||
toggle.setValue(timeFilter.enabled).onChange((value) => {
|
||||
this.view.calendarConfig.timeFilter = {
|
||||
...timeFilter,
|
||||
enabled: value,
|
||||
};
|
||||
this.refreshContent();
|
||||
});
|
||||
});
|
||||
|
||||
if (timeFilter.enabled) {
|
||||
// Time Filter Type
|
||||
new Setting(container)
|
||||
.setName(t("Time Range Preset"))
|
||||
.setDesc(t("Choose a preset or customize the time range"))
|
||||
.addDropdown((dropdown) => {
|
||||
dropdown
|
||||
.addOption(
|
||||
"workingHours",
|
||||
t("Working Hours (9:00-18:00)"),
|
||||
)
|
||||
.addOption("custom", t("Custom Range"))
|
||||
.setValue(timeFilter.type)
|
||||
.onChange((value: TimeFilterConfig["type"]) => {
|
||||
if (value === "workingHours") {
|
||||
this.view.calendarConfig.timeFilter = {
|
||||
enabled: true,
|
||||
type: "workingHours",
|
||||
startHour: 9,
|
||||
endHour: 18,
|
||||
};
|
||||
} else {
|
||||
this.view.calendarConfig.timeFilter = {
|
||||
...timeFilter,
|
||||
type: "custom",
|
||||
};
|
||||
}
|
||||
this.refreshContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Custom Time Range
|
||||
if (timeFilter.type === "custom") {
|
||||
// Start Hour
|
||||
new Setting(container)
|
||||
.setName(t("Start Hour"))
|
||||
.setDesc(t("First hour to display (0-23)"))
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(0, 23, 1)
|
||||
.setValue(timeFilter.startHour)
|
||||
.setDynamicTooltip()
|
||||
.onChange((value) => {
|
||||
this.view.calendarConfig.timeFilter = {
|
||||
...this.view.calendarConfig.timeFilter!,
|
||||
startHour: value,
|
||||
};
|
||||
this.updateTimeRangeDisplay(container);
|
||||
});
|
||||
});
|
||||
|
||||
// End Hour
|
||||
new Setting(container)
|
||||
.setName(t("End Hour"))
|
||||
.setDesc(t("Last hour to display (0-23)"))
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(1, 24, 1)
|
||||
.setValue(timeFilter.endHour)
|
||||
.setDynamicTooltip()
|
||||
.onChange((value) => {
|
||||
this.view.calendarConfig.timeFilter = {
|
||||
...this.view.calendarConfig.timeFilter!,
|
||||
endHour: value,
|
||||
};
|
||||
this.updateTimeRangeDisplay(container);
|
||||
});
|
||||
});
|
||||
|
||||
// Time Range Display
|
||||
const rangeDisplay = container.createDiv({
|
||||
cls: "calendar-time-range-display",
|
||||
});
|
||||
this.updateTimeRangeDisplay(container, rangeDisplay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private updateTimeRangeDisplay(
|
||||
container: HTMLElement,
|
||||
displayEl?: HTMLElement,
|
||||
) {
|
||||
const el =
|
||||
displayEl ||
|
||||
container.querySelector(".calendar-time-range-display");
|
||||
if (!el) return;
|
||||
|
||||
const timeFilter = this.view.calendarConfig.timeFilter;
|
||||
if (!timeFilter) return;
|
||||
|
||||
const formatHour = (h: number) => `${h.toString().padStart(2, "0")}:00`;
|
||||
(el as HTMLElement).textContent =
|
||||
`${formatHour(timeFilter.startHour)} - ${formatHour(timeFilter.endHour)}`;
|
||||
}
|
||||
|
||||
private renderDisplayTab(container: HTMLElement) {
|
||||
// Show Event Counts (not for agenda view)
|
||||
if (this.view.baseViewType !== "agenda") {
|
||||
new Setting(container)
|
||||
.setName(t("Show Event Counts"))
|
||||
.setDesc(t("Display event count badges on date cells"))
|
||||
.addToggle((toggle) => {
|
||||
toggle
|
||||
.setValue(
|
||||
this.view.calendarConfig.showEventCounts ?? true,
|
||||
)
|
||||
.onChange((value) => {
|
||||
this.view.calendarConfig.showEventCounts = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Max Events Per Row (for month view only)
|
||||
if (this.view.baseViewType === "month") {
|
||||
new Setting(container)
|
||||
.setName(t("Max Events Per Row"))
|
||||
.setDesc(
|
||||
t(
|
||||
"Maximum events to show per day cell before showing '+N more'",
|
||||
),
|
||||
)
|
||||
.addSlider((slider) => {
|
||||
slider
|
||||
.setLimits(1, 10, 1)
|
||||
.setValue(this.view.calendarConfig.maxEventsPerRow ?? 3)
|
||||
.setDynamicTooltip()
|
||||
.onChange((value) => {
|
||||
this.view.calendarConfig.maxEventsPerRow = value;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Agenda-specific display options
|
||||
if (this.view.baseViewType === "agenda") {
|
||||
new Setting(container)
|
||||
.setName(t("Day Header Format"))
|
||||
.setDesc(t("Format for day headers (e.g., dddd, MMMM D)"))
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("dddd, MMMM D")
|
||||
.setValue(
|
||||
(this.view.calendarConfig as any).dayHeaderFormat ??
|
||||
"",
|
||||
)
|
||||
.onChange((value) => {
|
||||
(this.view.calendarConfig as any).dayHeaderFormat =
|
||||
value || undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Year view note
|
||||
if (this.view.baseViewType === "year") {
|
||||
container.createDiv({
|
||||
cls: "calendar-config-notice",
|
||||
text: t(
|
||||
"Year view displays 12 mini-month calendars. Use the Date Filter tab to hide weekends if needed.",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Date Formats Section
|
||||
new Setting(container)
|
||||
.setName(t("Custom Date Formats"))
|
||||
.setDesc(t("Customize how dates are displayed in headers"))
|
||||
.setHeading();
|
||||
|
||||
// Month Header Format
|
||||
new Setting(container)
|
||||
.setName(t("Month Header Format"))
|
||||
.setDesc(t("Format for month view header (e.g., yyyy M)"))
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("yyyy M")
|
||||
.setValue(
|
||||
this.view.calendarConfig.dateFormats?.monthHeader ?? "",
|
||||
)
|
||||
.onChange((value) => {
|
||||
if (!this.view.calendarConfig.dateFormats) {
|
||||
this.view.calendarConfig.dateFormats = {};
|
||||
}
|
||||
this.view.calendarConfig.dateFormats.monthHeader =
|
||||
value || undefined;
|
||||
});
|
||||
});
|
||||
|
||||
// Day Header Format
|
||||
new Setting(container)
|
||||
.setName(t("Day Header Format"))
|
||||
.setDesc(t("Format for day view header (e.g., yyyy'年'M'月'd'日')"))
|
||||
.addText((text) => {
|
||||
text.setPlaceholder("yyyy'年'M'月'd'日'")
|
||||
.setValue(
|
||||
this.view.calendarConfig.dateFormats?.dayHeader ?? "",
|
||||
)
|
||||
.onChange((value) => {
|
||||
if (!this.view.calendarConfig.dateFormats) {
|
||||
this.view.calendarConfig.dateFormats = {};
|
||||
}
|
||||
this.view.calendarConfig.dateFormats.dayHeader =
|
||||
value || undefined;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderFooter(container: HTMLElement) {
|
||||
const cancelBtn = new ButtonComponent(container);
|
||||
cancelBtn.setButtonText(t("Cancel")).onClick(() => {
|
||||
this.close();
|
||||
});
|
||||
|
||||
const saveBtn = new ButtonComponent(container);
|
||||
saveBtn
|
||||
.setButtonText(t("Save"))
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
if (!this.view.name.trim()) {
|
||||
this.view.name = t("Untitled View");
|
||||
}
|
||||
this.onSave(this.view);
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
|
||||
private refreshContent() {
|
||||
const { contentEl } = this;
|
||||
|
||||
// Re-render tabs
|
||||
const tabsEl = contentEl.querySelector(".calendar-config-tabs");
|
||||
if (tabsEl) {
|
||||
tabsEl.empty();
|
||||
this.renderTabs(tabsEl as HTMLElement);
|
||||
}
|
||||
|
||||
// Re-render content
|
||||
const contentContainer = contentEl.querySelector(
|
||||
".calendar-config-content",
|
||||
);
|
||||
if (contentContainer) {
|
||||
this.renderTabContent(contentContainer as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import {
|
|||
renderIndexSettingsTab,
|
||||
IcsSettingsComponent,
|
||||
renderDesktopIntegrationSettingsTab,
|
||||
renderCalendarViewSettingsTab,
|
||||
} from "./components/features/settings";
|
||||
import { renderFileFilterSettingsTab } from "./components/features/settings/tabs/FileFilterSettingsTab";
|
||||
import { renderTimeParsingSettingsTab } from "./components/features/settings/tabs/TimeParsingSettingsTab";
|
||||
|
|
@ -180,6 +181,12 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
},
|
||||
|
||||
// Integration & Advanced
|
||||
{
|
||||
id: "calendar-views",
|
||||
name: t("Calendar Views"),
|
||||
icon: "calendar-range",
|
||||
category: "integration",
|
||||
},
|
||||
{
|
||||
id: "ics-integration",
|
||||
name: t("Calendar Sync"),
|
||||
|
|
@ -238,7 +245,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
if (plugin.dataflowOrchestrator) {
|
||||
// Call async updateSettings and await to ensure incremental reindex completes
|
||||
await plugin.dataflowOrchestrator.updateSettings(
|
||||
plugin.settings
|
||||
plugin.settings,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +256,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
await plugin.triggerViewUpdate();
|
||||
},
|
||||
100,
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
this.debouncedApplyNotifications = debounce(
|
||||
|
|
@ -260,7 +267,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Minimal view updates are unnecessary here
|
||||
},
|
||||
100,
|
||||
true
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -280,7 +287,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
}
|
||||
this.searchComponent = new SettingsSearchComponent(
|
||||
this,
|
||||
this.containerEl
|
||||
this.containerEl,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -377,7 +384,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
tab.name +
|
||||
(tab.id === "about"
|
||||
? " v" + this.plugin.manifest.version
|
||||
: "")
|
||||
: ""),
|
||||
);
|
||||
|
||||
// Add click handler
|
||||
|
|
@ -410,7 +417,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
|
||||
// Show active section, hide others
|
||||
const sections = this.containerEl.querySelectorAll(
|
||||
".settings-tab-section"
|
||||
".settings-tab-section",
|
||||
);
|
||||
sections.forEach((section) => {
|
||||
if (section.getAttribute("data-tab-id") === tabId) {
|
||||
|
|
@ -424,10 +431,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
|
||||
// Handle tab container and header visibility based on selected tab
|
||||
const tabsContainer = this.containerEl.querySelector(
|
||||
".settings-tabs-categorized-container"
|
||||
".settings-tabs-categorized-container",
|
||||
);
|
||||
const settingsHeader = this.containerEl.querySelector(
|
||||
".task-genius-settings-header"
|
||||
".task-genius-settings-header",
|
||||
);
|
||||
|
||||
if (tabId === "general") {
|
||||
|
|
@ -452,7 +459,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
if (tabId === "workspaces") {
|
||||
// Make sure the workspace section is visible even though the tab is hidden
|
||||
const workspaceSection = this.containerEl.querySelector(
|
||||
'[data-tab-id="workspaces"]'
|
||||
'[data-tab-id="workspaces"]',
|
||||
);
|
||||
if (workspaceSection) {
|
||||
(workspaceSection as unknown as HTMLElement).style.display =
|
||||
|
|
@ -473,7 +480,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
public navigateToTab(
|
||||
tabId: string,
|
||||
section?: string,
|
||||
search?: string
|
||||
search?: string,
|
||||
): void {
|
||||
// Set the current tab
|
||||
this.currentTab = tabId;
|
||||
|
|
@ -514,7 +521,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
// Special handling for MCP sections
|
||||
if (sectionId === "cursor" && this.currentTab === "mcp-integration") {
|
||||
const cursorSection = this.containerEl.querySelector(
|
||||
".mcp-client-section"
|
||||
".mcp-client-section",
|
||||
);
|
||||
if (cursorSection) {
|
||||
const header =
|
||||
|
|
@ -534,7 +541,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
private createTabSection(tabId: string): HTMLElement {
|
||||
// Get the sections container
|
||||
const sectionsContainer = this.containerEl.querySelector(
|
||||
".settings-tab-sections"
|
||||
".settings-tab-sections",
|
||||
);
|
||||
if (!sectionsContainer) return this.containerEl;
|
||||
|
||||
|
|
@ -567,7 +574,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
new IframeModal(
|
||||
this.app,
|
||||
url,
|
||||
`How to use — ${tabInfo?.name ?? tabId}`
|
||||
`How to use — ${tabInfo?.name ?? tabId}`,
|
||||
).open();
|
||||
} catch (e) {
|
||||
window.open(url);
|
||||
|
|
@ -689,13 +696,17 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
const habitSection = this.createTabSection("habit");
|
||||
this.displayHabitSettings(habitSection);
|
||||
|
||||
// Calendar Views Tab
|
||||
const calendarViewsSection = this.createTabSection("calendar-views");
|
||||
this.displayCalendarViewsSettings(calendarViewsSection);
|
||||
|
||||
// ICS Integration Tab
|
||||
const icsSection = this.createTabSection("ics-integration");
|
||||
this.displayIcsSettings(icsSection);
|
||||
|
||||
// Notifications Tab
|
||||
const notificationsSection = this.createTabSection(
|
||||
"desktop-integration"
|
||||
"desktop-integration",
|
||||
);
|
||||
this.displayDesktopIntegrationSettings(notificationsSection);
|
||||
|
||||
|
|
@ -767,6 +778,8 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
return `${base}/reward`;
|
||||
case "habit":
|
||||
return `${base}/habit`;
|
||||
case "calendar-views":
|
||||
return `${base}/task-view/calendar`;
|
||||
case "ics-integration":
|
||||
return `${base}/ics-support`;
|
||||
case "mcp-integration":
|
||||
|
|
@ -852,6 +865,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
renderProjectSettingsTab(this, containerEl);
|
||||
}
|
||||
|
||||
private displayCalendarViewsSettings(containerEl: HTMLElement): void {
|
||||
renderCalendarViewSettingsTab(this, containerEl);
|
||||
}
|
||||
|
||||
private displayIcsSettings(containerEl: HTMLElement): void {
|
||||
const icsSettingsComponent = new IcsSettingsComponent(
|
||||
this.plugin,
|
||||
|
|
@ -859,7 +876,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
() => {
|
||||
this.currentTab = "general";
|
||||
this.display();
|
||||
}
|
||||
},
|
||||
);
|
||||
icsSettingsComponent.display();
|
||||
}
|
||||
|
|
@ -870,7 +887,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
|
|||
|
||||
private displayMcpSettings(containerEl: HTMLElement): void {
|
||||
renderMcpIntegrationSettingsTab(this, containerEl, this.plugin, () =>
|
||||
this.applySettingsUpdate()
|
||||
this.applySettingsUpdate(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
384
src/styles/calendar-view-settings.css
Normal file
384
src/styles/calendar-view-settings.css
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
/* ============================================
|
||||
Calendar View Settings Tab Styles
|
||||
============================================ */
|
||||
|
||||
/* Templates Container */
|
||||
.calendar-templates-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: var(--size-4-3);
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
.calendar-template-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: var(--size-4-3);
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.calendar-template-card:hover {
|
||||
border-color: var(--interactive-accent);
|
||||
box-shadow: 0 2px 8px var(--background-modifier-box-shadow);
|
||||
}
|
||||
|
||||
.calendar-template-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--background-primary);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.calendar-template-icon svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.calendar-template-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calendar-template-name {
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-normal);
|
||||
margin-bottom: var(--size-2-1);
|
||||
}
|
||||
|
||||
.calendar-template-description {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.calendar-template-actions {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.calendar-template-actions button {
|
||||
font-size: var(--font-ui-smaller);
|
||||
}
|
||||
|
||||
/* Custom Views Container */
|
||||
.custom-calendar-views-container {
|
||||
margin-bottom: var(--size-4-4);
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.calendar-views-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--size-4-8) var(--size-4-4);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
border: 2px dashed var(--background-modifier-border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-views-empty-icon {
|
||||
color: var(--text-faint);
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
.calendar-views-empty-icon svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.calendar-views-empty-text {
|
||||
font-size: var(--font-ui-medium);
|
||||
color: var(--text-muted);
|
||||
font-weight: var(--font-medium);
|
||||
margin-bottom: var(--size-2-2);
|
||||
}
|
||||
|
||||
.calendar-views-empty-hint {
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
/* Views List */
|
||||
.calendar-views-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
/* View Item */
|
||||
.calendar-view-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-4-2);
|
||||
padding: var(--size-4-2) var(--size-4-3);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.calendar-view-item:hover {
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.calendar-view-item.is-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.calendar-view-item.is-disabled .calendar-view-name {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.calendar-view-drag-handle {
|
||||
cursor: grab;
|
||||
color: var(--text-faint);
|
||||
padding: var(--size-2-1);
|
||||
}
|
||||
|
||||
.calendar-view-drag-handle:hover {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.calendar-view-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.calendar-view-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background: var(--background-primary);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-accent);
|
||||
}
|
||||
|
||||
.calendar-view-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.calendar-view-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.calendar-view-name {
|
||||
font-weight: var(--font-medium);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.calendar-view-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2-2);
|
||||
margin-top: var(--size-2-1);
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.calendar-view-base-type {
|
||||
background: var(--background-primary);
|
||||
padding: 1px 6px;
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.calendar-view-config-summary {
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.calendar-view-actions {
|
||||
display: flex;
|
||||
gap: var(--size-2-1);
|
||||
}
|
||||
|
||||
.calendar-view-action-btn {
|
||||
padding: var(--size-2-2);
|
||||
border-radius: var(--radius-s);
|
||||
}
|
||||
|
||||
.calendar-view-action-delete:hover {
|
||||
color: var(--text-error);
|
||||
}
|
||||
|
||||
/* Add View Container */
|
||||
.calendar-add-view-container {
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
|
||||
/* Sortable States */
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
background: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.sortable-chosen {
|
||||
box-shadow: 0 4px 12px var(--background-modifier-box-shadow);
|
||||
}
|
||||
|
||||
.sortable-drag {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
Calendar View Config Modal Styles
|
||||
============================================ */
|
||||
|
||||
.calendar-view-config-modal {
|
||||
width: 560px;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.calendar-view-config-modal .modal-header {
|
||||
margin-bottom: var(--size-4-3);
|
||||
}
|
||||
|
||||
.calendar-view-config-modal .modal-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.calendar-config-tabs {
|
||||
display: flex;
|
||||
gap: var(--size-2-1);
|
||||
margin-bottom: var(--size-4-4);
|
||||
padding-bottom: var(--size-4-2);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.calendar-config-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
padding: var(--size-2-2) var(--size-4-2);
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.calendar-config-tab:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.calendar-config-tab.is-active {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.calendar-config-tab.is-disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.calendar-config-tab.is-disabled:hover {
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.calendar-config-tab-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.calendar-config-tab-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.calendar-config-content {
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.calendar-config-content .setting-item {
|
||||
border-top: none;
|
||||
padding: var(--size-4-2) 0;
|
||||
}
|
||||
|
||||
.calendar-config-content .setting-item:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.calendar-config-content .setting-item-heading {
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-bottom: var(--size-4-2);
|
||||
margin-bottom: var(--size-4-2);
|
||||
}
|
||||
|
||||
/* Icon Selector */
|
||||
.calendar-view-icon-selector {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.calendar-view-icon-selector button {
|
||||
padding: var(--size-4-2);
|
||||
}
|
||||
|
||||
/* Days Selector */
|
||||
.calendar-days-selector {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
.calendar-day-chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 44px;
|
||||
padding: var(--size-2-2) var(--size-4-2);
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--radius-s);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-ui-smaller);
|
||||
color: var(--text-normal);
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.calendar-day-chip:hover {
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.calendar-day-chip.is-hidden {
|
||||
background: var(--text-error);
|
||||
border-color: var(--text-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Time Range Display */
|
||||
.calendar-time-range-display {
|
||||
text-align: center;
|
||||
padding: var(--size-4-2);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
font-size: var(--font-ui-medium);
|
||||
font-weight: var(--font-semibold);
|
||||
color: var(--text-accent);
|
||||
margin-top: var(--size-4-2);
|
||||
}
|
||||
|
||||
/* Notice */
|
||||
.calendar-config-notice {
|
||||
padding: var(--size-4-3);
|
||||
background: var(--background-secondary);
|
||||
border-radius: var(--radius-s);
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -19,31 +19,177 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-header button {
|
||||
margin: 0 var(--size-4-1);
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-nav,
|
||||
.full-calendar-container .calendar-view-switcher {
|
||||
display: flex;
|
||||
gap: var(--size-2-2);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Navigation Control (prev/today/next)
|
||||
======================================== */
|
||||
|
||||
.full-calendar-container .calendar-nav {
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-nav > div {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-nav button {
|
||||
border-radius: var(--radius-s);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 10px;
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: calc(var(--radius-m) - 2px);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-smaller);
|
||||
font-weight: var(--font-medium);
|
||||
|
||||
text-transform: uppercase;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-view-switcher button {
|
||||
border-radius: var(--radius-s);
|
||||
text-transform: uppercase;
|
||||
.full-calendar-container .calendar-nav button:hover {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-view-switcher button:not(.is-active),
|
||||
.full-calendar-container .calendar-nav button:not(.is-active) {
|
||||
box-shadow: var(--shadow-xs);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
.full-calendar-container .calendar-nav button:active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-nav .today-button {
|
||||
padding: 6px 12px;
|
||||
font-weight: var(--font-semibold);
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
Segmented Control View Switcher
|
||||
======================================== */
|
||||
|
||||
.full-calendar-container .calendar-segmented-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
background-color: var(--background-secondary);
|
||||
border-radius: var(--radius-m);
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
/* Sliding active indicator */
|
||||
.full-calendar-container .calendar-segmented-indicator {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
bottom: 3px;
|
||||
left: 3px;
|
||||
background-color: var(--interactive-accent);
|
||||
border-radius: calc(var(--radius-m) - 2px);
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
width 0.2s ease;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Segment button base styles */
|
||||
.full-calendar-container button.calendar-segment-btn {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
min-width: 32px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: calc(var(--radius-m) - 2px);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background-color 0.15s ease;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-ui-small);
|
||||
font-weight: var(--font-medium);
|
||||
white-space: nowrap;
|
||||
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.full-calendar-container button.calendar-segment-btn:hover {
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.full-calendar-container button.calendar-segment-btn.is-active {
|
||||
color: var(--text-on-accent);
|
||||
background-color: var(--color-accent); /* Indicator provides background */
|
||||
}
|
||||
|
||||
.full-calendar-container button.calendar-segment-btn.is-active:hover {
|
||||
background-color: var(--color-accent-1);
|
||||
}
|
||||
|
||||
/* Short label (always visible) */
|
||||
.full-calendar-container .calendar-segment-short {
|
||||
font-weight: var(--font-semibold);
|
||||
font-size: var(--font-ui-small);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Full label (hidden by default, shown on wider screens) */
|
||||
.full-calendar-container .calendar-segment-full {
|
||||
display: none;
|
||||
font-size: var(--font-ui-smaller);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Custom view icon */
|
||||
.full-calendar-container .calendar-segment-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-segment-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
/* Separator between built-in and custom views */
|
||||
.full-calendar-container .calendar-segment-separator {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background-color: var(--background-modifier-border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* Custom segment button adjustments */
|
||||
.full-calendar-container .calendar-segment-btn.calendar-segment-custom {
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.full-calendar-container
|
||||
.calendar-segment-btn.calendar-segment-custom
|
||||
.calendar-segment-icon {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-current-date {
|
||||
|
|
@ -58,10 +204,22 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-view-switcher button.is-active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent-hover);
|
||||
/* Show full labels on wider screens */
|
||||
@container (min-width: 700px) {
|
||||
.full-calendar-container .calendar-segment-full {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-segment-btn {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
/* Show short label for custom views on wider screens too */
|
||||
.full-calendar-container
|
||||
.calendar-segment-btn.calendar-segment-custom
|
||||
.calendar-segment-short {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-view-container {
|
||||
|
|
@ -638,7 +796,7 @@
|
|||
}
|
||||
|
||||
.full-calendar-container input.task-list-item-checkbox {
|
||||
scale: 0.9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-view-switcher-selector {
|
||||
|
|
@ -687,8 +845,9 @@
|
|||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Hide segmented control and show dropdown on narrow screens */
|
||||
@container (max-width: 600px) {
|
||||
.full-calendar-container .calendar-view-switcher button {
|
||||
.full-calendar-container .calendar-segmented-control {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
|
@ -704,3 +863,19 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
/* Medium width: show only short labels */
|
||||
@container (min-width: 601px) and (max-width: 699px) {
|
||||
.full-calendar-container .calendar-segment-full {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-segment-short {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.full-calendar-container .calendar-segment-btn {
|
||||
padding: 6px 8px;
|
||||
min-width: 28px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@
|
|||
|
||||
.tg-month-row {
|
||||
position: relative;
|
||||
height: 120px;
|
||||
/*height: 120px;*/
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -60,6 +60,9 @@ export interface HiddenModulesConfig {
|
|||
|
||||
/** Hidden feature components */
|
||||
features?: FeatureComponentType[];
|
||||
|
||||
/** Hidden custom calendar view IDs */
|
||||
calendarViews?: string[];
|
||||
}
|
||||
|
||||
/** Sidebar component types that can be hidden (Fluent interface) */
|
||||
|
|
@ -98,7 +101,7 @@ export interface ModuleDefinition {
|
|||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
type: "view" | "sidebar" | "feature";
|
||||
type: "view" | "sidebar" | "feature" | "calendarView";
|
||||
}
|
||||
|
||||
export interface WorkspacesConfig {
|
||||
|
|
|
|||
650
styles.css
650
styles.css
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue