Add calendar toolbox with weekly and monthly scheduling
All checks were successful
Deploy Sokko G / deploy (push) Successful in 14s

This commit is contained in:
Shinuwa 2026-08-20 15:48:24 +02:00
parent 4f73e0068b
commit 992665f4ea
39 changed files with 4331 additions and 36 deletions

View file

@ -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}` : "#",