Add calendar toolbox with weekly and monthly scheduling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s
This commit is contained in:
parent
4f73e0068b
commit
992665f4ea
39 changed files with 4331 additions and 36 deletions
1215
website/src/features/toolboxes/modules/CalendarModule.jsx
Normal file
1215
website/src/features/toolboxes/modules/CalendarModule.jsx
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -442,9 +442,6 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
<div className="combos-module">
|
||||
{(editing || editingComboId) && (
|
||||
<div className="module-add-panel combos-editor">
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
<div className="combos-device-row">
|
||||
<label>
|
||||
<span>{textContent.deviceLabel || "Périphérique"}</span>
|
||||
|
|
@ -592,6 +589,9 @@ export function CombosModule({ toolboxId, moduleId, context, editing }) {
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{editing && !editingComboId && (
|
||||
<TextExchangeActions type="combos" data={data} context={context} textContent={textContent} onImport={(nextData) => save(nextData)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
266
website/src/features/toolboxes/modules/calendarUtils.js
Normal file
266
website/src/features/toolboxes/modules/calendarUtils.js
Normal file
|
|
@ -0,0 +1,266 @@
|
|||
// Rôle : regroupe les calculs hebdomadaires/mensuels, occurrences et alertes du calendrier.
|
||||
export const CALENDAR_VIEW_MODES = ["week", "month"];
|
||||
export const CALENDAR_MONTHLY_REPEAT_BY = ["monthDay", "weekday"];
|
||||
export const CALENDAR_REPEAT_MODES = ["none", "daily", "weekly", "monthly"];
|
||||
export const CALENDAR_MONTH_REPEAT_MODES = ["none", "weekly", "monthly"];
|
||||
export const CALENDAR_ALERT_MODES = ["off", "visible", "site"];
|
||||
export const CALENDAR_ALERT_OFFSETS = [10, 5, 0];
|
||||
export const CALENDAR_COLORS = ["violet", "gold", "cyan", "pink", "green", "gray"];
|
||||
export const WEEK_SLOT_MINUTES = 30;
|
||||
export const WEEK_TIME_INPUT_STEP_MINUTES = 5;
|
||||
export const WEEK_SLOT_COUNT = 48;
|
||||
export const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
export const MINUTE_MS = 60 * 1000;
|
||||
export const WEEK_DAY_COUNT = 7;
|
||||
|
||||
export function pad2(value) {
|
||||
return String(value).padStart(2, "0");
|
||||
}
|
||||
|
||||
export function formatLocalDateKey(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())}`;
|
||||
}
|
||||
|
||||
export function parseLocalDateKey(value) {
|
||||
const match = String(value || "").match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
const year = Number(match[1]);
|
||||
const month = Number(match[2]) - 1;
|
||||
const day = Number(match[3]);
|
||||
const date = new Date(year, month, day);
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) return null;
|
||||
return date;
|
||||
}
|
||||
|
||||
export function minutesToTimeValue(minutes) {
|
||||
const normalized = Math.max(0, Math.min(23 * 60 + 59, Number(minutes) || 0));
|
||||
return `${pad2(Math.floor(normalized / 60))}:${pad2(normalized % 60)}`;
|
||||
}
|
||||
|
||||
export function timeValueToMinutes(value) {
|
||||
const match = String(value || "").match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (!match) return -1;
|
||||
const hours = Number(match[1]);
|
||||
const minutes = Number(match[2]);
|
||||
if (!Number.isInteger(hours) || !Number.isInteger(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) return -1;
|
||||
return hours * 60 + minutes;
|
||||
}
|
||||
|
||||
export function startOfLocalDay(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
}
|
||||
|
||||
export function addDays(dateLike, days) {
|
||||
const date = new Date(dateLike);
|
||||
date.setDate(date.getDate() + days);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function addMonths(dateLike, months) {
|
||||
const date = new Date(dateLike);
|
||||
date.setMonth(date.getMonth() + months);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getLocalDayIndex(dateLike) {
|
||||
return (new Date(dateLike).getDay() + 6) % WEEK_DAY_COUNT;
|
||||
}
|
||||
|
||||
export function getWeekStart(dateLike, weekStartsOn = 1) {
|
||||
const day = startOfLocalDay(dateLike);
|
||||
const current = day.getDay();
|
||||
const offset = (current - weekStartsOn + 7) % 7;
|
||||
return addDays(day, -offset);
|
||||
}
|
||||
|
||||
export function getWeekDays(weekStart) {
|
||||
return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
|
||||
}
|
||||
|
||||
export function getMonthStart(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return new Date(date.getFullYear(), date.getMonth(), 1);
|
||||
}
|
||||
|
||||
export function getMonthEnd(dateLike) {
|
||||
const date = getMonthStart(dateLike);
|
||||
date.setMonth(date.getMonth() + 1);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getDaysInMonth(dateLike) {
|
||||
return new Date(new Date(dateLike).getFullYear(), new Date(dateLike).getMonth() + 1, 0).getDate();
|
||||
}
|
||||
|
||||
export function getMonthGridDays(dateLike) {
|
||||
const monthStart = getMonthStart(dateLike);
|
||||
const monthEnd = getMonthEnd(dateLike);
|
||||
const gridStart = getWeekStart(monthStart, 1);
|
||||
const gridEnd = addDays(getWeekStart(addDays(monthEnd, -1), 1), 7);
|
||||
const days = [];
|
||||
for (let cursor = new Date(gridStart); cursor < gridEnd; cursor = addDays(cursor, 1)) {
|
||||
const date = new Date(cursor);
|
||||
days.push({
|
||||
date,
|
||||
dayNumber: date.getDate(),
|
||||
dayIndex: getLocalDayIndex(date),
|
||||
inMonth: date.getMonth() === monthStart.getMonth() && date.getFullYear() === monthStart.getFullYear()
|
||||
});
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
export function getDateForWeekDay(weekStart, dayIndex, minutes = 0) {
|
||||
const date = addDays(weekStart, Math.max(0, Math.min(WEEK_DAY_COUNT - 1, Number(dayIndex) || 0)));
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
export function getDateForMonthDay(monthDate, monthDay, minutes = 0) {
|
||||
const day = Number(monthDay);
|
||||
const monthStart = getMonthStart(monthDate);
|
||||
if (!Number.isInteger(day) || day < 1 || day > getDaysInMonth(monthStart)) return null;
|
||||
const date = new Date(monthStart.getFullYear(), monthStart.getMonth(), day);
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function getWeekdayOrdinalInMonth(dateLike) {
|
||||
const date = new Date(dateLike);
|
||||
return Math.floor((date.getDate() - 1) / 7) + 1;
|
||||
}
|
||||
|
||||
function getMonthlyWeekdayDate(monthDate, sourceDate, minutes = 0) {
|
||||
const source = new Date(sourceDate);
|
||||
const ordinal = getWeekdayOrdinalInMonth(source);
|
||||
const dayIndex = getLocalDayIndex(source);
|
||||
const monthStart = getMonthStart(monthDate);
|
||||
const firstDayOffset = (dayIndex - getLocalDayIndex(monthStart) + WEEK_DAY_COUNT) % WEEK_DAY_COUNT;
|
||||
const date = addDays(monthStart, firstDayOffset + (ordinal - 1) * WEEK_DAY_COUNT);
|
||||
if (date.getMonth() !== monthStart.getMonth()) return null;
|
||||
date.setHours(0, Math.max(0, Number(minutes) || 0), 0, 0);
|
||||
return date;
|
||||
}
|
||||
|
||||
function getMonthlyDateRange(event, monthDate) {
|
||||
const sourceStart = parseLocalDateKey(event.startDate);
|
||||
const sourceEnd = parseLocalDateKey(event.endDate || event.startDate);
|
||||
if (!sourceStart || !sourceEnd || sourceEnd < sourceStart) return null;
|
||||
const spanDays = Math.max(0, Math.round((startOfLocalDay(sourceEnd).getTime() - startOfLocalDay(sourceStart).getTime()) / DAY_MS));
|
||||
const start = event.monthlyRepeatBy === "weekday"
|
||||
? getMonthlyWeekdayDate(monthDate, sourceStart, event.startMinutes)
|
||||
: getDateForMonthDay(monthDate, sourceStart.getDate(), event.startMinutes);
|
||||
if (!start) return null;
|
||||
const end = addDays(start, spanDays + 1);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
function getDatedEventRange(event) {
|
||||
const start = parseLocalDateKey(event.startDate);
|
||||
const endSource = parseLocalDateKey(event.endDate || event.startDate);
|
||||
if (!start || !endSource || endSource < start) return null;
|
||||
start.setHours(0, Math.max(0, Number(event.startMinutes) || 0), 0, 0);
|
||||
const end = addDays(endSource, 1);
|
||||
end.setHours(0, 0, 0, 0);
|
||||
return { start, end };
|
||||
}
|
||||
|
||||
export function getEventDurationMs(event) {
|
||||
const startMinutes = Number(event.startMinutes);
|
||||
const endMinutes = Number(event.endMinutes);
|
||||
return Number.isFinite(startMinutes) && Number.isFinite(endMinutes) && endMinutes > startMinutes ? (endMinutes - startMinutes) * MINUTE_MS : 0;
|
||||
}
|
||||
|
||||
export function getEventEndMs(event, startMs) {
|
||||
const durationMs = getEventDurationMs(event);
|
||||
return durationMs > 0 ? startMs + durationMs : startMs;
|
||||
}
|
||||
|
||||
export function getCalendarAlertOffsets(event) {
|
||||
const rawOffsets = Array.isArray(event?.alertOffsetMinutes) ? event.alertOffsetMinutes : [event?.alertOffsetMinutes];
|
||||
const offsets = [...new Set(rawOffsets.map(Number).filter((offset) => CALENDAR_ALERT_OFFSETS.includes(offset)))];
|
||||
return offsets.length ? offsets.sort((a, b) => b - a) : [10];
|
||||
}
|
||||
|
||||
function getWeeklyOccurrenceStart(event, weekStart, offset) {
|
||||
if (event.repeat === "none") return getDateForWeekDay(weekStart, event.dayIndex, event.startMinutes);
|
||||
return getDateForWeekDay(addDays(weekStart, offset * 7), event.dayIndex, event.startMinutes);
|
||||
}
|
||||
|
||||
export function getCalendarOccurrences(events, rangeStart, rangeEnd) {
|
||||
const baseWeekStart = getWeekStart(rangeStart);
|
||||
const startMs = rangeStart.getTime();
|
||||
const endMs = rangeEnd.getTime();
|
||||
return events.flatMap((event) => {
|
||||
const durationMs = getEventDurationMs(event);
|
||||
const candidates = [];
|
||||
|
||||
if (event.startDate) {
|
||||
const baseRange = getDatedEventRange(event);
|
||||
if (baseRange) {
|
||||
const spanDays = Math.max(0, Math.round((baseRange.end.getTime() - baseRange.start.getTime()) / DAY_MS) - 1);
|
||||
if (event.repeat === "monthly") {
|
||||
for (let cursor = getMonthStart(rangeStart); cursor < rangeEnd; cursor = addMonths(cursor, 1)) {
|
||||
const range = getMonthlyDateRange(event, cursor);
|
||||
if (range) candidates.push(range);
|
||||
}
|
||||
} else if (event.repeat === "weekly") {
|
||||
const firstOffset = Math.floor((getWeekStart(rangeStart).getTime() - getWeekStart(baseRange.start).getTime()) / (7 * DAY_MS)) - 1;
|
||||
const weekCount = Math.ceil((rangeEnd.getTime() - rangeStart.getTime()) / (7 * DAY_MS)) + 3;
|
||||
for (let index = Math.max(-1, firstOffset); index < firstOffset + weekCount; index += 1) {
|
||||
const start = addDays(baseRange.start, index * 7);
|
||||
const end = addDays(start, spanDays + 1);
|
||||
candidates.push({ start, end });
|
||||
}
|
||||
} else {
|
||||
candidates.push(baseRange);
|
||||
}
|
||||
}
|
||||
} else if (event.repeat === "none") {
|
||||
candidates.push(getWeeklyOccurrenceStart(event, baseWeekStart, 0));
|
||||
} else {
|
||||
candidates.push(getWeeklyOccurrenceStart(event, baseWeekStart, -1), getWeeklyOccurrenceStart(event, baseWeekStart, 0), getWeeklyOccurrenceStart(event, baseWeekStart, 1));
|
||||
}
|
||||
|
||||
return candidates
|
||||
.map((candidate) => candidate instanceof Date ? { start: candidate, end: null } : candidate)
|
||||
.filter((candidate) => candidate?.start && !Number.isNaN(candidate.start.getTime()))
|
||||
.filter((candidate) => !event.excludedOccurrences?.includes?.(formatLocalDateKey(candidate.start)))
|
||||
.map((candidate) => {
|
||||
const occurrenceStartMs = candidate.start.getTime();
|
||||
const occurrenceEndMs = candidate.end && !Number.isNaN(candidate.end.getTime()) ? candidate.end.getTime() : 0;
|
||||
return {
|
||||
id: `${event.id}:${occurrenceStartMs}`,
|
||||
event,
|
||||
startMs: occurrenceStartMs,
|
||||
endMs: occurrenceEndMs > occurrenceStartMs ? occurrenceEndMs : durationMs > 0 ? occurrenceStartMs + durationMs : occurrenceStartMs
|
||||
};
|
||||
})
|
||||
.filter((occurrence) => {
|
||||
const visibleEndMs = occurrence.endMs || occurrence.startMs;
|
||||
return visibleEndMs >= startMs && occurrence.startMs < endMs;
|
||||
});
|
||||
}).sort((a, b) => a.startMs - b.startMs);
|
||||
}
|
||||
|
||||
export function getNextCalendarAlertTime(event, nowMs) {
|
||||
if (!event || event.alertMode === "off") return 0;
|
||||
const rangeStart = addDays(new Date(nowMs), -1);
|
||||
const rangeEnd = addDays(new Date(nowMs), 370);
|
||||
const alert = getCalendarOccurrences([event], rangeStart, rangeEnd)
|
||||
.flatMap((occurrence) => getCalendarAlertOffsets(event).map((offset) => occurrence.startMs - offset * MINUTE_MS))
|
||||
.filter((alertMs) => alertMs >= nowMs)
|
||||
.sort((a, b) => a - b)[0];
|
||||
return alert || 0;
|
||||
}
|
||||
|
||||
export function getCurrentCalendarOccurrence(event, nowMs) {
|
||||
if (!event?.endMinutes && !event?.startDate) return null;
|
||||
const rangeStart = addDays(new Date(nowMs), -1);
|
||||
const rangeEnd = addDays(new Date(nowMs), 1);
|
||||
return getCalendarOccurrences([event], rangeStart, rangeEnd)
|
||||
.find((occurrence) => occurrence.startMs <= nowMs && occurrence.endMs > nowMs) || null;
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { useInlineEdit } from "../../../hooks/useInlineEdit.js";
|
|||
import { lockBodyScroll } from "../../../utils/bodyScrollLock.js";
|
||||
import { TOOLBOX_MODULE_DEFINITIONS, TOOLBOX_MODULES } from "../toolboxModuleRegistry.js";
|
||||
import { CalculatorModule } from "./CalculatorModule.jsx";
|
||||
import { CalendarModule } from "./CalendarModule.jsx";
|
||||
import { ChecklistModule } from "./ChecklistModule.jsx";
|
||||
import { CombosModule } from "./CombosModule.jsx";
|
||||
import { CountersModule } from "./CountersModule.jsx";
|
||||
|
|
@ -28,6 +29,7 @@ const MODULE_COMPONENTS = {
|
|||
counters: { ...TOOLBOX_MODULE_DEFINITIONS.counters, Component: CountersModule },
|
||||
combos: { ...TOOLBOX_MODULE_DEFINITIONS.combos, Component: CombosModule },
|
||||
calculator: { ...TOOLBOX_MODULE_DEFINITIONS.calculator, Component: CalculatorModule },
|
||||
calendar: { ...TOOLBOX_MODULE_DEFINITIONS.calendar, Component: CalendarModule },
|
||||
table: { ...TOOLBOX_MODULE_DEFINITIONS.table, Component: TableModule },
|
||||
timer: { ...TOOLBOX_MODULE_DEFINITIONS.timer, Component: TimerModule },
|
||||
taskPlanner: { ...TOOLBOX_MODULE_DEFINITIONS.taskPlanner, Component: TaskPlannerModule },
|
||||
|
|
@ -352,7 +354,7 @@ function ModuleShell({ toolbox, module, context, reorder, itemGroup = "", onRena
|
|||
</div>
|
||||
</header>
|
||||
<div className={`module-content ${scrollable ? "is-scrollable legacy-scrollbar" : ""}`}>
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} />
|
||||
<Component toolboxId={toolbox.id} moduleId={module.id} context={context} editing={editing} setEditing={setEditing} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
// Rôle : parse et sérialise les formats texte/TSV d'échange des outils toolbox.
|
||||
import { CALENDAR_ALERT_MODES, CALENDAR_ALERT_OFFSETS, CALENDAR_COLORS, CALENDAR_MONTHLY_REPEAT_BY, CALENDAR_MONTH_REPEAT_MODES, minutesToTimeValue, timeValueToMinutes } from "./calendarUtils.js";
|
||||
import { cellAddress, columnIndexToName } from "./tableFormulaEngine.js";
|
||||
import { getCountdownTargetMs, getDailyTargetMs, getIntervalAnchorMs } from "./timerUtils.js";
|
||||
|
||||
|
|
@ -27,6 +28,7 @@ const SUPPORTED_TEXT_EXCHANGE_TYPES = new Set([
|
|||
"calculator",
|
||||
"table",
|
||||
"timer",
|
||||
"calendar",
|
||||
"taskPlanner",
|
||||
"equipmentPlanner",
|
||||
"images",
|
||||
|
|
@ -36,6 +38,12 @@ const COMBO_DEVICES = new Set(["playstation", "xbox", "switch", "n64", "keyboard
|
|||
const COMBO_INPUT_KINDS = new Set(["button", "direction", "key", "mouse"]);
|
||||
const TIMER_TYPES = new Set(["duration", "daily_time", "time_pattern", "interval"]);
|
||||
const ALERT_MODES = new Set(["off", "visible", "site"]);
|
||||
const CALENDAR_ALERT_MODE_SET = new Set(CALENDAR_ALERT_MODES);
|
||||
const CALENDAR_ALERT_OFFSET_SET = new Set(CALENDAR_ALERT_OFFSETS);
|
||||
const CALENDAR_COLOR_SET = new Set(CALENDAR_COLORS);
|
||||
const CALENDAR_MONTHLY_REPEAT_BY_SET = new Set(CALENDAR_MONTHLY_REPEAT_BY);
|
||||
const CALENDAR_MONTH_REPEAT_MODE_SET = new Set(CALENDAR_MONTH_REPEAT_MODES);
|
||||
const CALENDAR_WEEKDAYS = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"];
|
||||
const IMAGE_DATA_URL_PATTERN = /^data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+$/i;
|
||||
|
||||
export function supportsTextExchange(type) {
|
||||
|
|
@ -51,6 +59,7 @@ export function hasTextExchangeContent(type, data) {
|
|||
if (type === "calculator") return Boolean(data?.entries?.length || data?.scrollResults);
|
||||
if (type === "table") return Boolean(Object.keys(data?.cells || {}).length || Object.keys(data?.rowLabels || {}).length || Object.keys(data?.columnLabels || {}).length);
|
||||
if (type === "timer") return Boolean(data?.stopwatch?.laps?.length || data?.countdowns?.length || data?.stopwatch?.elapsedMs);
|
||||
if (type === "calendar") return Boolean(data?.events?.length);
|
||||
if (type === "taskPlanner") return Boolean(data?.tasks?.length);
|
||||
if (type === "equipmentPlanner") return Boolean(data?.equipments?.length);
|
||||
if (type === "images") return Boolean(data?.images?.length);
|
||||
|
|
@ -67,6 +76,7 @@ export function exportModuleText(type, data) {
|
|||
if (type === "calculator") return exportCalculatorText(data);
|
||||
if (type === "table") return exportTableTsv(data);
|
||||
if (type === "timer") return exportTimerText(data);
|
||||
if (type === "calendar") return exportCalendarText(data);
|
||||
if (type === "taskPlanner") return exportTaskPlannerText(data);
|
||||
if (type === "equipmentPlanner") return exportEquipmentPlannerText(data);
|
||||
if (type === "images") return exportImagesText(data);
|
||||
|
|
@ -87,6 +97,7 @@ export function importModuleText(type, text, context) {
|
|||
else if (type === "calculator") data = importCalculatorText(value, context);
|
||||
else if (type === "table") data = importTableTsv(value);
|
||||
else if (type === "timer") data = importTimerText(value, context);
|
||||
else if (type === "calendar") data = importCalendarText(value, context);
|
||||
else if (type === "taskPlanner") data = importTaskPlannerText(value, context);
|
||||
else if (type === "equipmentPlanner") data = importEquipmentPlannerText(value, context);
|
||||
else if (type === "images") data = importImagesText(value, context);
|
||||
|
|
@ -644,6 +655,112 @@ function importTimerText(text, context) {
|
|||
return context.normalizeTimerData(data);
|
||||
}
|
||||
|
||||
function parseCalendarDayIndex(value) {
|
||||
const normalized = String(value || "").trim().toLowerCase();
|
||||
const weekdayIndex = CALENDAR_WEEKDAYS.indexOf(normalized);
|
||||
if (weekdayIndex >= 0) return weekdayIndex;
|
||||
const parsed = parseInteger(normalized, -1);
|
||||
return parsed >= 0 && parsed <= 6 ? parsed : -1;
|
||||
}
|
||||
|
||||
function parseCalendarAlertOffsets(value) {
|
||||
return String(value || "")
|
||||
.split(",")
|
||||
.map((item) => parseInteger(item.trim(), Number.NaN))
|
||||
.filter((offset, index, offsets) => CALENDAR_ALERT_OFFSET_SET.has(offset) && offsets.indexOf(offset) === index)
|
||||
.sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
function formatCalendarEventMeta(event, viewMode) {
|
||||
const alertMode = CALENDAR_ALERT_MODE_SET.has(event.alertMode) ? event.alertMode : "off";
|
||||
const meta = {
|
||||
title: event.title,
|
||||
color: event.color && event.color !== "violet" ? event.color : "",
|
||||
repeatable: event.repeatable === false ? "false" : "",
|
||||
alertMode: alertMode !== "off" ? alertMode : "",
|
||||
alerts: alertMode !== "off" ? (event.alertOffsetMinutes || [10]).join(",") : ""
|
||||
};
|
||||
if (viewMode === "month") {
|
||||
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(event.monthlyRepeatBy) ? event.monthlyRepeatBy : "monthDay";
|
||||
if (monthlyRepeatBy === "weekday") {
|
||||
return {
|
||||
...meta,
|
||||
repeatBy: "weekday",
|
||||
repeat: event.repeat || "none",
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||
};
|
||||
}
|
||||
return {
|
||||
...meta,
|
||||
repeatBy: "monthDay",
|
||||
repeat: event.repeat || "none",
|
||||
startDate: event.startDate,
|
||||
endDate: event.endDate && event.endDate !== event.startDate ? event.endDate : ""
|
||||
};
|
||||
}
|
||||
return {
|
||||
...meta,
|
||||
day: CALENDAR_WEEKDAYS[event.dayIndex] || "",
|
||||
start: minutesToTimeValue(event.startMinutes),
|
||||
end: event.endMinutes ? minutesToTimeValue(event.endMinutes) : ""
|
||||
};
|
||||
}
|
||||
|
||||
function exportCalendarText(data) {
|
||||
const viewMode = data?.viewMode === "month" ? "month" : "week";
|
||||
const lines = [`@viewMode: ${viewMode}`];
|
||||
if (data?.weekStartsOn === 0) lines.push("@weekStartsOn: 0");
|
||||
(data?.events || []).forEach((event) => {
|
||||
lines.push(`event | ${formatMeta(formatCalendarEventMeta(event, viewMode))}`);
|
||||
});
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
function importCalendarText(text, context) {
|
||||
const trimmed = String(text || "").trim();
|
||||
if (trimmed.startsWith("{")) {
|
||||
return context.normalizeCalendarData(JSON.parse(trimmed));
|
||||
}
|
||||
|
||||
const data = { initialized: true, viewMode: "week", weekStartsOn: 1, events: [] };
|
||||
splitLines(text).forEach((line, index) => {
|
||||
const cleanLine = line.trim();
|
||||
if (!cleanLine) return;
|
||||
const meta = parseMetaLine(cleanLine);
|
||||
if (meta) {
|
||||
if (meta.key === "viewMode") data.viewMode = meta.value === "month" ? "month" : "week";
|
||||
if (meta.key === "weekStartsOn") data.weekStartsOn = parseInteger(meta.value, 1) === 0 ? 0 : 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const { title, meta: lineMeta } = parsePipeParts(cleanLine);
|
||||
if (title !== "event") throw lineError(index, `ligne calendrier invalide: ${cleanLine}`);
|
||||
const event = {
|
||||
id: context.uid("calendar"),
|
||||
title: lineMeta.title || "",
|
||||
repeatable: lineMeta.repeatable !== "false",
|
||||
alertMode: CALENDAR_ALERT_MODE_SET.has(lineMeta.alertMode) ? lineMeta.alertMode : "off",
|
||||
alertOffsetMinutes: parseCalendarAlertOffsets(lineMeta.alerts),
|
||||
color: CALENDAR_COLOR_SET.has(lineMeta.color) ? lineMeta.color : "violet"
|
||||
};
|
||||
if (data.viewMode === "month") {
|
||||
const monthlyRepeatBy = CALENDAR_MONTHLY_REPEAT_BY_SET.has(lineMeta.repeatBy) ? lineMeta.repeatBy : "monthDay";
|
||||
event.startMinutes = 0;
|
||||
event.monthlyRepeatBy = monthlyRepeatBy;
|
||||
event.repeat = CALENDAR_MONTH_REPEAT_MODE_SET.has(lineMeta.repeat) ? lineMeta.repeat : "none";
|
||||
event.startDate = lineMeta.startDate || "";
|
||||
event.endDate = lineMeta.endDate || event.startDate;
|
||||
} else {
|
||||
event.dayIndex = parseCalendarDayIndex(lineMeta.day);
|
||||
event.startMinutes = timeValueToMinutes(lineMeta.start);
|
||||
if (lineMeta.end) event.endMinutes = timeValueToMinutes(lineMeta.end);
|
||||
}
|
||||
data.events.push(event);
|
||||
});
|
||||
return context.normalizeCalendarData(data);
|
||||
}
|
||||
|
||||
function exportImagesText(data) {
|
||||
return (data.images || []).flatMap((image) => [
|
||||
image.label ? `# ${image.label}` : "#",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue