mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -04:00
Merge remote-tracking branch 'origin/dev' into test-main-dev-merge-20260615
# Conflicts: # src/tool_implementations.py # static/js/research/panel.js
This commit is contained in:
+153
-44
@@ -9,7 +9,7 @@ import { makeWindowDraggable } from './windowDrag.js';
|
||||
import { attachColorPicker } from './colorPicker.js';
|
||||
import { bindMenuDismiss } from './escMenuStack.js';
|
||||
import {
|
||||
WEEKDAYS, MONTHS, MON_SHORT,
|
||||
WEEKDAYS, WEEKDAYS_SUN, MONTHS, MON_SHORT,
|
||||
CAL_PALETTE, CAL_COLORS, _CAL_CUSTOM_GRADIENT, _TYPE_PALETTE,
|
||||
_trashIcon, _moreIcon, _bellIcon,
|
||||
_isCalBgImage, _calBgImageUrl, _calBgCss,
|
||||
@@ -64,6 +64,8 @@ let _hiddenTypes = new Set(); // event_type values to hide
|
||||
let _onlyImportant = false;
|
||||
|
||||
let _filtersCollapsed = localStorage.getItem('cal-filters-collapsed') === '1';
|
||||
// Week-start preference: 'mon' (default, Mon=first col) or 'sun' (Sun=first col).
|
||||
let _weekStartSun = localStorage.getItem('cal-week-start') === 'sun';
|
||||
let _selectedDay = null;
|
||||
let _view = 'month';
|
||||
let _searchQuery = '';
|
||||
@@ -360,14 +362,14 @@ function _today() { return _ds(new Date()); }
|
||||
function _monthRange(d) {
|
||||
const y = d.getFullYear(), m = d.getMonth();
|
||||
const first = new Date(y, m, 1);
|
||||
const dow = (first.getDay() + 6) % 7;
|
||||
const dow = _weekStartSun ? first.getDay() : (first.getDay() + 6) % 7;
|
||||
const gs = new Date(y, m, 1 - dow);
|
||||
const ge = new Date(gs); ge.setDate(gs.getDate() + 42);
|
||||
return [_ds(gs), _ds(ge)];
|
||||
}
|
||||
|
||||
function _weekRange(d) {
|
||||
const dow = (d.getDay() + 6) % 7;
|
||||
const dow = _weekStartSun ? d.getDay() : (d.getDay() + 6) % 7;
|
||||
const s = new Date(d); s.setDate(d.getDate() - dow);
|
||||
const e = new Date(s); e.setDate(s.getDate() + 7);
|
||||
return [_ds(s), _ds(e)];
|
||||
@@ -950,11 +952,11 @@ async function _renderMonth() {
|
||||
_slideDir = 0;
|
||||
let h = _headerHTML() + _filtersRowHTML() + `<div class="cal-grid${slideClass}">`;
|
||||
h += '<div class="cal-week-headers">';
|
||||
for (const wd of WEEKDAYS) h += `<div class="cal-weekday">${wd}</div>`;
|
||||
for (const wd of (_weekStartSun ? WEEKDAYS_SUN : WEEKDAYS)) h += `<div class="cal-weekday">${wd}</div>`;
|
||||
h += '</div>';
|
||||
|
||||
const first = new Date(y, m, 1);
|
||||
const dow = (first.getDay() + 6) % 7;
|
||||
const dow = _weekStartSun ? first.getDay() : (first.getDay() + 6) % 7;
|
||||
const gs = new Date(y, m, 1 - dow);
|
||||
|
||||
const multiDay = _events.filter(e => {
|
||||
@@ -1163,13 +1165,13 @@ function _wkEventTopHeight(ev, dayStr) {
|
||||
// Date math if the string isn't shaped as expected.
|
||||
const _toMin = (iso, fallbackDate) => {
|
||||
if (!iso) return null;
|
||||
const m = iso.match(/T(\d{2}):(\d{2})/);
|
||||
if (m) {
|
||||
const mins = _timeToMin(iso);
|
||||
if (mins !== null && iso.includes('T')) {
|
||||
// If the event spans into a previous/next day, clamp to today's bounds.
|
||||
const evDate = iso.slice(0, 10);
|
||||
const evDate = _localDateOf(iso);
|
||||
if (evDate < fallbackDate) return 0; // event started before today
|
||||
if (evDate > fallbackDate) return 24 * 60; // event ends after today
|
||||
return parseInt(m[1], 10) * 60 + parseInt(m[2], 10);
|
||||
return mins;
|
||||
}
|
||||
// All-day or date-only — treat as start of day.
|
||||
return 0;
|
||||
@@ -1226,8 +1228,8 @@ async function _renderWeek() {
|
||||
const timedEvents = _eventsForDay(ds).filter(e => _eventVisible(e) && !e.all_day);
|
||||
|
||||
const isSun = d.getDay() === 0;
|
||||
colsHtml += `<div class="cal-wk-col${isToday ? ' cal-wk-today' : ''}${isSun ? ' cal-wk-sun' : ''}" data-date="${ds}">`;
|
||||
colsHtml += `<div class="cal-wk-col-head"><span class="cal-wk-dn">${WEEKDAYS[idx]}</span><span class="cal-wk-dt">${d.getDate()}</span></div>`;
|
||||
colsHtml += `<div class="cal-wk-col${isToday ? ' cal-wk-today' : ''}${isSun && !_weekStartSun ? ' cal-wk-sun' : ''}" data-date="${ds}">`;
|
||||
colsHtml += `<div class="cal-wk-col-head"><span class="cal-wk-dn">${(_weekStartSun ? WEEKDAYS_SUN : WEEKDAYS)[idx]}</span><span class="cal-wk-dt">${d.getDate()}</span></div>`;
|
||||
// All-day strip
|
||||
colsHtml += `<div class="cal-wk-allday">`;
|
||||
for (const ev of allDayEvents) {
|
||||
@@ -1308,12 +1310,17 @@ async function _renderWeek() {
|
||||
if (!ev) return;
|
||||
const cols = Array.from(body.querySelectorAll('.cal-wk-grid'));
|
||||
if (!cols.length) return;
|
||||
// Original timing
|
||||
const m1 = (ev.dtstart || '').match(/T(\d{2}):(\d{2})/);
|
||||
const m2 = (ev.dtend || '').match(/T(\d{2}):(\d{2})/);
|
||||
const startMin0 = m1 ? parseInt(m1[1], 10) * 60 + parseInt(m1[2], 10) : 0;
|
||||
const endMin0 = m2 ? parseInt(m2[1], 10) * 60 + parseInt(m2[2], 10) : startMin0 + 60;
|
||||
const durationMin = Math.max(15, endMin0 - startMin0);
|
||||
// Local/display timing
|
||||
const startMin0 = _timeToMin(ev.dtstart) ?? 0;
|
||||
const endMin0 = _timeToMin(ev.dtend) ?? startMin0 + 60;
|
||||
|
||||
let durationMin = endMin0 - startMin0;
|
||||
const startDs = _localDateOf(ev.dtstart);
|
||||
const endDs = ev.dtend ? _localDateOf(ev.dtend) : startDs;
|
||||
if (endDs > startDs && endMin0 <= startMin0) {
|
||||
durationMin += 24 * 60;
|
||||
}
|
||||
durationMin = Math.max(15, durationMin);
|
||||
|
||||
// Where did the cursor grab the block? (offset from block-top in px)
|
||||
const blockRect = block.getBoundingClientRect();
|
||||
@@ -1387,7 +1394,7 @@ async function _renderWeek() {
|
||||
// a plain click (no movement) must still open the event.
|
||||
if (moved) block.dataset.justResized = '1';
|
||||
// Decide whether anything actually moved.
|
||||
const oldDs = (ev.dtstart || '').slice(0, 10);
|
||||
const oldDs = _localDateOf(ev.dtstart);
|
||||
if (!nextDs) return;
|
||||
if (nextDs === oldDs && nextStartMin === startMin0) return;
|
||||
// Snapshot the original times so we can offer an Undo.
|
||||
@@ -1396,11 +1403,10 @@ async function _renderWeek() {
|
||||
const newEndMin = nextStartMin + durationMin;
|
||||
const hh = String(Math.floor(nextStartMin / 60)).padStart(2, '0');
|
||||
const mm = String(nextStartMin % 60).padStart(2, '0');
|
||||
const hh2 = String(Math.floor(newEndMin / 60)).padStart(2, '0');
|
||||
const mm2 = String((newEndMin) % 60).padStart(2, '0');
|
||||
const _tz = _tzOffset();
|
||||
const newDtstartDate = new Date(`${nextDs}T${hh}:${mm}:00`);
|
||||
const _tz = _tzOffsetForDate(newDtstartDate);
|
||||
const newDtstart = `${nextDs}T${hh}:${mm}:00${_tz}`;
|
||||
const newDtend = `${nextDs}T${hh2}:${mm2}:00${_tz}`;
|
||||
const newDtend = _addMinutesToLocalIso(newDtstart, durationMin);
|
||||
try {
|
||||
await _updateEvent(uid, { dtstart: newDtstart, dtend: newDtend });
|
||||
_render();
|
||||
@@ -1432,10 +1438,7 @@ async function _renderWeek() {
|
||||
const uid = block.dataset.uid;
|
||||
const ev = _events.find(x => x.uid === uid);
|
||||
if (!ev || !grid || !ds) return;
|
||||
const startMin = (() => {
|
||||
const m = (ev.dtstart || '').match(/T(\d{2}):(\d{2})/);
|
||||
return m ? parseInt(m[1], 10) * 60 + parseInt(m[2], 10) : 0;
|
||||
})();
|
||||
const startMin = _timeToMin(ev.dtstart) ?? 0;
|
||||
const initialTop = parseFloat(block.style.top || '0');
|
||||
const gridRect = grid.getBoundingClientRect();
|
||||
let newEndMin = startMin;
|
||||
@@ -1460,9 +1463,8 @@ async function _renderWeek() {
|
||||
if (resized) block.dataset.justResized = '1';
|
||||
if (newEndMin === startMin) return;
|
||||
const prevDtend = ev.dtend;
|
||||
const hh = String(Math.floor(newEndMin / 60)).padStart(2, '0');
|
||||
const mm = String(newEndMin % 60).padStart(2, '0');
|
||||
const newDtend = `${ds}T${hh}:${mm}:00${_tzOffset()}`;
|
||||
const durationMin = newEndMin - startMin;
|
||||
const newDtend = _addMinutesToLocalIso(ev.dtstart, durationMin);
|
||||
try {
|
||||
await _updateEvent(uid, { dtend: newDtend });
|
||||
_render();
|
||||
@@ -1746,9 +1748,9 @@ async function _renderYear() {
|
||||
for (let m = 0; m < 12; m++) {
|
||||
h += `<div class="cal-year-month" data-month="${m}"><div class="cal-year-month-title">${MON_SHORT[m]}</div>`;
|
||||
h += '<div class="cal-year-grid">';
|
||||
for (const wd of ['M', 'T', 'W', 'T', 'F', 'S', 'S']) h += `<div class="cal-year-wd">${wd}</div>`;
|
||||
for (const wd of (_weekStartSun ? ['S','M','T','W','T','F','S'] : ['M','T','W','T','F','S','S'])) h += `<div class="cal-year-wd">${wd}</div>`;
|
||||
const first = new Date(y, m, 1);
|
||||
const dow = (first.getDay() + 6) % 7;
|
||||
const dow = _weekStartSun ? first.getDay() : (first.getDay() + 6) % 7;
|
||||
const daysInMonth = new Date(y, m + 1, 0).getDate();
|
||||
for (let p = 0; p < dow; p++) h += '<div class="cal-year-cell"></div>';
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
@@ -1989,10 +1991,10 @@ function _wireAll(body) {
|
||||
const ad = document.getElementById('cal-f-allday');
|
||||
if (ad && !ad.checked) { ad.checked = true; ad.dispatchEvent(new Event('change')); }
|
||||
} else {
|
||||
const t1 = (ev.dtstart || '').match(/T(\d{2}:\d{2})/);
|
||||
const t2 = (ev.dtend || '').match(/T(\d{2}:\d{2})/);
|
||||
if (t1) set('cal-f-start', t1[1]);
|
||||
if (t2) set('cal-f-end', t2[1]);
|
||||
const t1 = _fmtTime(ev.dtstart);
|
||||
const t2 = _fmtTime(ev.dtend);
|
||||
if (t1) set('cal-f-start', t1);
|
||||
if (t2) set('cal-f-end', t2);
|
||||
document.getElementById('cal-f-start')?.dispatchEvent(new Event('input'));
|
||||
}
|
||||
// Make sure the details panel is open so the user can verify time.
|
||||
@@ -2497,6 +2499,13 @@ async function _showCalSettings() {
|
||||
</div>
|
||||
<div style="font-size:10px;opacity:0.4;margin-top:4px;">Download a calendar as .ics for backup or to import into another app.</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border);padding-top:12px;">
|
||||
<div style="font-size:11px;opacity:0.5;margin-bottom:6px;">Week starts on</div>
|
||||
<div style="display:flex;gap:6px;">
|
||||
<button id="cal-wstart-mon" type="button" style="font-size:12px;padding:3px 10px;border-radius:4px;border:1px solid var(--border);background:${!_weekStartSun ? 'color-mix(in srgb, var(--accent,var(--red)) 18%, var(--panel))' : 'var(--panel)'};color:var(--fg);cursor:pointer;transition:background 0.1s,border-color 0.1s;outline:none;">Monday</button>
|
||||
<button id="cal-wstart-sun" type="button" style="font-size:12px;padding:3px 10px;border-radius:4px;border:1px solid var(--border);background:${_weekStartSun ? 'color-mix(in srgb, var(--accent,var(--red)) 18%, var(--panel))' : 'var(--panel)'};color:var(--fg);cursor:pointer;transition:background 0.1s,border-color 0.1s;outline:none;">Sunday</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="border-top:1px solid var(--border);padding-top:12px;">
|
||||
<div style="font-size:11px;opacity:0.5;margin-bottom:6px;">Sync</div>
|
||||
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
|
||||
@@ -2517,6 +2526,28 @@ async function _showCalSettings() {
|
||||
overlay.querySelector('#cal-settings-close').addEventListener('click', cleanup);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanup(); });
|
||||
|
||||
// Week-start toggle: save to localStorage, update module state, re-render.
|
||||
const _monBtn = overlay.querySelector('#cal-wstart-mon');
|
||||
const _sunBtn = overlay.querySelector('#cal-wstart-sun');
|
||||
const _activeStyle = 'color-mix(in srgb, var(--accent,var(--red)) 18%, var(--panel))';
|
||||
const _inactiveStyle = 'var(--panel)';
|
||||
const _applyWeekStartActive = () => {
|
||||
if (_monBtn) _monBtn.style.background = _weekStartSun ? _inactiveStyle : _activeStyle;
|
||||
if (_sunBtn) _sunBtn.style.background = _weekStartSun ? _activeStyle : _inactiveStyle;
|
||||
};
|
||||
_monBtn?.addEventListener('click', () => {
|
||||
_weekStartSun = false;
|
||||
localStorage.setItem('cal-week-start', 'mon');
|
||||
_applyWeekStartActive();
|
||||
if (_open) _render();
|
||||
});
|
||||
_sunBtn?.addEventListener('click', () => {
|
||||
_weekStartSun = true;
|
||||
localStorage.setItem('cal-week-start', 'sun');
|
||||
_applyWeekStartActive();
|
||||
if (_open) _render();
|
||||
});
|
||||
|
||||
// Create a new (local) calendar. Defaults the name + next palette color, then
|
||||
// reopens the panel so the user can rename it inline and pick a color.
|
||||
overlay.querySelector('#cal-settings-add')?.addEventListener('click', async (e) => {
|
||||
@@ -2941,35 +2972,68 @@ function _showEventForm(existing, defaultDate, defaultEndDate) {
|
||||
const startEl = document.getElementById('cal-f-start');
|
||||
const endEl = document.getElementById('cal-f-end');
|
||||
if (!startEl || !endEl) return;
|
||||
|
||||
const _toMin = (v) => {
|
||||
if (!v || !/^\d{2}:\d{2}$/.test(v)) return null;
|
||||
const [h, m] = v.split(':').map(n => parseInt(n, 10));
|
||||
return h * 60 + m;
|
||||
};
|
||||
|
||||
const _toHHMM = (mins) => {
|
||||
let m = ((mins % 1440) + 1440) % 1440;
|
||||
const hh = String(Math.floor(m / 60)).padStart(2, '0');
|
||||
const mm = String(m % 60).padStart(2, '0');
|
||||
return `${hh}:${mm}`;
|
||||
};
|
||||
|
||||
const _autoAdvanceEndDate = () => {
|
||||
const isAD = document.getElementById('cal-f-allday')?.checked;
|
||||
if (isAD) return;
|
||||
|
||||
const dv = document.getElementById('cal-f-date')?.value;
|
||||
const dvEndEl = document.getElementById('cal-f-date-end');
|
||||
if (!dv || !dvEndEl || dvEndEl.value !== dv) return;
|
||||
|
||||
const sVal = startEl.value;
|
||||
const eVal = endEl.value;
|
||||
|
||||
if (sVal && eVal && eVal <= sVal) {
|
||||
const d = new Date(`${dv}T00:00:00`);
|
||||
d.setDate(d.getDate() + 1);
|
||||
|
||||
dvEndEl.value = _ds(d);
|
||||
}
|
||||
};
|
||||
|
||||
let prevStartMin = _toMin(startEl.value);
|
||||
endEl.addEventListener('input', () => { endEl.dataset.userEdited = '1'; });
|
||||
|
||||
endEl.addEventListener('input', () => {
|
||||
endEl.dataset.userEdited = '1';
|
||||
});
|
||||
|
||||
endEl.addEventListener('change', _autoAdvanceEndDate);
|
||||
|
||||
startEl.addEventListener('change', () => {
|
||||
const newStartMin = _toMin(startEl.value);
|
||||
const endMin = _toMin(endEl.value);
|
||||
if (newStartMin == null) { prevStartMin = newStartMin; return; }
|
||||
// Compute the duration before the change. Use the user's existing
|
||||
// start→end gap, fallback to 1 hour.
|
||||
let durationMin = 60;
|
||||
if (prevStartMin != null && endMin != null && endMin > prevStartMin) {
|
||||
durationMin = endMin - prevStartMin;
|
||||
} else if (endMin != null && newStartMin != null && endMin > newStartMin && endEl.dataset.userEdited === '1') {
|
||||
// User already set a custom end before changing start — leave it.
|
||||
|
||||
if (newStartMin == null) {
|
||||
prevStartMin = newStartMin;
|
||||
return;
|
||||
}
|
||||
|
||||
let durationMin = 60;
|
||||
|
||||
if (prevStartMin != null && endMin != null && endMin > prevStartMin) {
|
||||
durationMin = endMin - prevStartMin;
|
||||
} else if (endMin != null && newStartMin != null && endMin > newStartMin && endEl.dataset.userEdited === '1') {
|
||||
prevStartMin = newStartMin;
|
||||
return;
|
||||
}
|
||||
|
||||
endEl.value = _toHHMM(newStartMin + durationMin);
|
||||
prevStartMin = newStartMin;
|
||||
_autoAdvanceEndDate();
|
||||
});
|
||||
})();
|
||||
// Custom reminder picker
|
||||
@@ -3030,6 +3094,20 @@ function _showEventForm(existing, defaultDate, defaultEndDate) {
|
||||
// proper UTC instants (is_utc=True). Without this, naive "10:00" gets
|
||||
// re-interpreted as local elsewhere — the timezone-misfire bug.
|
||||
const _tz = _tzOffset();
|
||||
|
||||
if (!isAD) {
|
||||
const startVal = document.getElementById('cal-f-start').value;
|
||||
const endVal = document.getElementById('cal-f-end').value;
|
||||
|
||||
const startDt = new Date(`${dv}T${startVal}:00`);
|
||||
const endDt = new Date(`${dvEnd}T${endVal}:00`);
|
||||
|
||||
if (endDt <= startDt) {
|
||||
uiModule.showToast('End time must be after start time');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
summary,
|
||||
dtstart: isAD ? dv : `${dv}T${document.getElementById('cal-f-start').value}:00${_tz}`,
|
||||
@@ -3261,6 +3339,37 @@ function _fmtTime(s) {
|
||||
}
|
||||
return s.slice(11, 16);
|
||||
}
|
||||
|
||||
function _timeToMin(iso) {
|
||||
const hm = _fmtTime(iso);
|
||||
if (!hm) return null;
|
||||
const m = hm.match(/^(\d{1,2}):(\d{2})$/);
|
||||
if (!m) return null;
|
||||
const h = parseInt(m[1], 10);
|
||||
const min = parseInt(m[2], 10);
|
||||
if (h < 0 || h > 23 || min < 0 || min > 59) return null;
|
||||
return h * 60 + min;
|
||||
}
|
||||
|
||||
function _tzOffsetForDate(d) {
|
||||
const off = -d.getTimezoneOffset();
|
||||
const sign = off >= 0 ? '+' : '-';
|
||||
const abs = Math.abs(off);
|
||||
const hh = String(Math.floor(abs / 60)).padStart(2, '0');
|
||||
const mm = String(abs % 60).padStart(2, '0');
|
||||
return `${sign}${hh}:${mm}`;
|
||||
}
|
||||
|
||||
function _addMinutesToLocalIso(baseIso, addMinutes) {
|
||||
const d = new Date(new Date(baseIso).getTime() + addMinutes * 60000);
|
||||
const y = d.getFullYear();
|
||||
const mo = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const da = String(d.getDate()).padStart(2, '0');
|
||||
const h = String(d.getHours()).padStart(2, '0');
|
||||
const m = String(d.getMinutes()).padStart(2, '0');
|
||||
return `${y}-${mo}-${da}T${h}:${m}:00${_tzOffsetForDate(d)}`;
|
||||
}
|
||||
|
||||
function _e(s) { return uiModule.esc ? uiModule.esc(s || '') : (s || '').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); }
|
||||
|
||||
// Linkify a location string: URLs become clickable, plain addresses get a Maps link.
|
||||
|
||||
Reference in New Issue
Block a user