Major new version #2

Merged
LeoVasanko merged 86 commits from vol002 into main 2025-08-26 05:58:24 +01:00
Showing only changes of commit 70ffd2881f - Show all commits

View File

@ -115,27 +115,42 @@ const contentHeight = computed(() => {
const visibleWeeks = ref([]) const visibleWeeks = ref([])
let lastScrollRange = { startVW: null, endVW: null } let lastScrollRange = { startVW: null, endVW: null }
let windowTimer = null let updating = 0 // 0 idle, 1 window incremental, 2 full rebuild
let dataTimer = null
const WINDOW_DEBOUNCE_MS = 30
const DATA_DEBOUNCE_MS = 40
function scheduleWindowUpdate(reason) { function scheduleWindowUpdate(reason) {
if (windowTimer) return if (updating !== 0) return
windowTimer = setTimeout(() => { updating = 1
windowTimer = null const run = () => {
const fn = () => updateVisibleWeeks(reason) let complete = true
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 80 }) try {
else requestAnimationFrame(fn) complete = updateVisibleWeeks(reason)
}, WINDOW_DEBOUNCE_MS) } finally {
updating = 0
}
if (!complete) scheduleWindowUpdate('incremental-build')
}
if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 16 })
else requestAnimationFrame(run)
} }
function scheduleDataRebuild(reason) { function scheduleDataRebuild(reason) {
if (dataTimer) return if (updating === 2) return // already rebuilding
dataTimer = setTimeout(() => { const doRebuild = () => {
dataTimer = null updating = 2
const fn = () => rebuildVisibleWeeks(reason) const run = () => {
if ('requestIdleCallback' in window) requestIdleCallback(fn, { timeout: 120 }) try {
else requestAnimationFrame(fn) rebuildVisibleWeeks(reason)
}, DATA_DEBOUNCE_MS) } finally {
updating = 0
}
}
if ('requestIdleCallback' in window) requestIdleCallback(run, { timeout: 60 })
else requestAnimationFrame(run)
}
// If we're mid incremental window update, defer slightly to next frame
if (updating === 1) {
requestAnimationFrame(doRebuild)
return
}
doRebuild()
} }
const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate }) const scrollManager = createScrollManager({ viewport, scheduleRebuild: scheduleWindowUpdate })
@ -177,6 +192,7 @@ const selectedDateRange = computed(() => {
}) })
function updateVisibleWeeks(reason) { function updateVisibleWeeks(reason) {
// Compute desired virtual week window with buffer
const buffer = 4 const buffer = 4
const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value const currentScrollTop = viewport.value?.scrollTop ?? scrollTop.value
const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value) const startIdx = Math.floor((currentScrollTop - buffer * rowHeight.value) / rowHeight.value)
@ -185,34 +201,86 @@ function updateVisibleWeeks(reason) {
) )
const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value) const startVW = Math.max(minVirtualWeek.value, startIdx + minVirtualWeek.value)
const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value) const endVW = Math.min(maxVirtualWeek.value, endIdx + minVirtualWeek.value)
if (
lastScrollRange.startVW === startVW && // Step 1: prune anything outside the desired window
lastScrollRange.endVW === endVW &&
visibleWeeks.value.length
)
return
if (visibleWeeks.value.length) { if (visibleWeeks.value.length) {
while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW) while (visibleWeeks.value.length && visibleWeeks.value[0].virtualWeek < startVW) {
visibleWeeks.value.shift() visibleWeeks.value.shift()
}
while ( while (
visibleWeeks.value.length && visibleWeeks.value.length &&
visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek > endVW visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek > endVW
) ) {
visibleWeeks.value.pop() visibleWeeks.value.pop()
let needFirst = visibleWeeks.value[0]?.virtualWeek
if (visibleWeeks.value.length === 0) needFirst = endVW + 1
for (let vw = (needFirst ?? startVW) - 1; vw >= startVW; vw--)
visibleWeeks.value.unshift(createWeek(vw))
let needLast = visibleWeeks.value[visibleWeeks.value.length - 1]?.virtualWeek
for (let vw = (needLast ?? startVW - 1) + 1; vw <= endVW; vw++)
visibleWeeks.value.push(createWeek(vw))
lastScrollRange = { startVW, endVW }
return
} }
const weeks = [] }
for (let vw = startVW; vw <= endVW; vw++) weeks.push(createWeek(vw))
visibleWeeks.value = weeks // Step 2: ensure no gaps; add at most one adjacent missing week each pass
let added = false
const len = visibleWeeks.value.length
if (len === 0) {
visibleWeeks.value.push(createWeek(startVW))
added = true
} else {
// Sort defensively (should already be sorted)
visibleWeeks.value.sort((a, b) => a.virtualWeek - b.virtualWeek)
const firstVW = visibleWeeks.value[0].virtualWeek
const lastVW = visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek
if (firstVW > startVW) {
// Add one week just before current first to close front gap gradually
visibleWeeks.value.unshift(createWeek(firstVW - 1))
added = true
} else {
// Look for first internal gap
let gapInserted = false
for (let i = 0; i < visibleWeeks.value.length - 1; i++) {
const curVW = visibleWeeks.value[i].virtualWeek
const nextVW = visibleWeeks.value[i + 1].virtualWeek
if (nextVW - curVW > 1 && curVW < endVW) {
// Insert the immediate missing week after curVW
visibleWeeks.value.splice(i + 1, 0, createWeek(curVW + 1))
added = true
gapInserted = true
break
}
}
if (!gapInserted && lastVW < endVW) {
// Extend at end
visibleWeeks.value.push(createWeek(lastVW + 1))
added = true
}
}
}
// Step 3: assess coverage
const firstAfter = visibleWeeks.value[0].virtualWeek
const lastAfter = visibleWeeks.value[visibleWeeks.value.length - 1].virtualWeek
const contiguous = (() => {
for (let i = 0; i < visibleWeeks.value.length - 1; i++) {
if (visibleWeeks.value[i + 1].virtualWeek !== visibleWeeks.value[i].virtualWeek + 1)
return false
}
return true
})()
const coverageComplete =
firstAfter <= startVW &&
lastAfter >= endVW &&
contiguous &&
visibleWeeks.value.length === endVW - startVW + 1
if (!coverageComplete) {
// Incomplete; do not update lastScrollRange so subsequent runs keep adding
return false
}
if (
lastScrollRange.startVW === startVW &&
lastScrollRange.endVW === endVW &&
!added &&
visibleWeeks.value.length
) {
return true
}
lastScrollRange = { startVW, endVW } lastScrollRange = { startVW, endVW }
return true
} }
function rebuildVisibleWeeks(reason) { function rebuildVisibleWeeks(reason) {
const buffer = 4 const buffer = 4
@ -650,6 +718,8 @@ const handleHeaderYearChange = ({ scrollTop: st }) => {
const maxScroll = contentHeight.value - viewportHeight.value const maxScroll = contentHeight.value - viewportHeight.value
const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st)) const clamped = Math.max(0, Math.min(st, isFinite(maxScroll) ? maxScroll : st))
setScrollTop(clamped, 'header-year-change') setScrollTop(clamped, 'header-year-change')
// Force a full rebuild so the new year range appears instantly
scheduleDataRebuild('header-year-change')
} }
// Heuristic: rotate month label (180deg) only for predominantly Latin text. // Heuristic: rotate month label (180deg) only for predominantly Latin text.