Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,384 @@
define(function(require, exports) {
'use strict';
// Listen for changes on all busytimes inside a given day
// ---
// This module will listen for changes on any busytime inside a day and
// group/batch multiple add/remove events into a single emit call; making it
// easier to rerender all the busytimes at once and allowing us to use the
// same method to add/remove busytimes from the DOM, drastically simplifying
// the logic required.
// ---
// On the most common scenario there should not be that many busytimes inside
// the same day, and every time we add a new busytime to the DOM we need to
// check for "overlaps", so doing the full rerender will make a lot of sense
// and should not affect performance that much.
// ---
// It's way simpler to use a day-based logic for the whole calendar front-end,
// instead of knowing how to handle months/weeks.
var Calc = require('common/calc');
var EventEmitter2 = require('ext/eventemitter2');
var binsearch = require('common/binsearch');
var compare = require('common/compare');
var core = require('core');
var daysBetween = Calc.daysBetween;
var debounce = require('utils/mout').debounce;
var getDayId = Calc.getDayId;
var isAllDay = Calc.isAllDay;
var spanOfMonth = Calc.spanOfMonth;
// make sure we only trigger a single emit for multiple consecutive changes
var DISPATCH_DELAY = 25;
// maximum amount of months to keep in the cache
var MAX_CACHED_MONTHS = 5;
// stores busytimes by Id
// {key:id, value:Busytime}
var busytimes = new Map();
// stores events by Id
// {key:id, value:Event}
var events = new Map();
// relationships between event Ids and busytimes Ids
// {key:eventId, value:Array<busytimeId>}
var eventsToBusytimes = new Map();
// cache a reference to all busytime+event data for the days based on the dayId
// {key:dayId, value:DayRecords}
var cache = new Map();
// stores the days IDs that changed since last dispatch
var dayQueue = new Set();
// stores the timespans for all the cached months
var cachedSpans = [];
// we lock the cache pruning during sync
var cacheLocked = false;
// emitter exposed because of unit tests
var emitter = exports.emitter = new EventEmitter2();
exports.init = function() {
// both "change" and "add" operations triggers a "persist" event
var eventStore = core.storeFactory.get('Event');
eventStore.on('persist', (id, event) => cacheEvent(event));
eventStore.on('remove', removeEventById);
var busytimeStore = core.storeFactory.get('Busytime');
busytimeStore.on('persist', (id, busy) => cacheBusytime(busy));
busytimeStore.on('remove', removeBusytimeById);
core.syncController.on('syncStart', () => {
cacheLocked = true;
});
core.syncController.on('syncComplete', () => {
cacheLocked = false;
pruneCache();
dispatch();
});
var calendarStore = core.storeFactory.get('Calendar');
calendarStore.on('calendarVisibilityChange', (id, calendar) => {
var type = calendar.localDisplayed ? 'add' : 'remove';
busytimes.forEach((busy, busyId) => {
if (busy.calendarId === id) {
registerBusytimeChange(busyId, type);
}
});
});
core.timeController.on('monthChange', loadMonth);
// make sure loadMonth is called during setup if 'monthChange' was dispatched
// before we added the listener
var month = core.timeController.month;
if (month) {
loadMonth(month);
}
};
exports.on = function(date, callback) {
var dayId = getDayId(date);
// important to trigger the callback to avoid getting into weird states when
// busytimes are loaded while views are not listening for changes
callback(getDay(dayId, date));
emitter.on(dayId, callback);
};
function getDay(dayId, date) {
if (cache.has(dayId)) {
return cache.get(dayId);
}
var day = {
dayId: dayId,
date: date,
amount: 0,
basic: [],
allday: []
};
cache.set(dayId, day);
return day;
}
exports.off = function(date, callback) {
emitter.off(getDayId(date), callback);
};
exports.removeAllListeners = function() {
emitter.removeAllListeners();
};
// findAssociated is not on the Event/Busytime store so that it can get the
// cache data and show the event details faster on the most common cases
exports.findAssociated = function(busytimeId) {
return queryBusytime(busytimeId).then(busytime => {
return queryEvent(busytime.eventId).then(event => {
return {
busytime: busytime,
event: event
};
});
});
};
function queryBusytime(busytimeId) {
if (busytimes.has(busytimeId)) {
return Promise.resolve(busytimes.get(busytimeId));
}
var busytimeStore = core.storeFactory.get('Busytime');
return busytimeStore.get(busytimeId);
}
function queryEvent(eventId) {
if (events.has(eventId)) {
return Promise.resolve(events.get(eventId));
}
var eventStore = core.storeFactory.get('Event');
return eventStore.get(eventId);
}
function cacheBusytime(busy) {
var {_id, startDate, endDate, eventId} = busy;
if (outsideSpans(startDate) && outsideSpans(endDate)) {
// ignore busytimes outside the cached spans
return;
}
busytimes.set(_id, busy);
eventsToBusytimes.get(eventId).push(_id);
registerBusytimeChange(_id);
}
function removeBusytimeById(id) {
var busy = busytimes.get(id);
if (!busy) {
// when removing all the data from the calendar the busytime might not be
// on the cache but still emit a "remove" event
return;
}
var eventId = busy.eventId;
var ids = eventsToBusytimes.get(eventId).filter(i => i !== id);
eventsToBusytimes.set(eventId, ids);
removeEventIfNoBusytimes(ids, eventId);
registerBusytimeChange(id, 'remove');
busytimes.delete(id);
}
function cacheEvent(event) {
var id = event._id;
events.set(id, event);
if (!eventsToBusytimes.has(id)) {
eventsToBusytimes.set(id, []);
}
}
function removeEventById(id) {
events.delete(id);
// busytimeStore will emit a 'remove' event for each busytime, so no need to
// handle it here; we simply remove the "join table"
eventsToBusytimes.delete(id);
}
// every time an event/calendar/busytime changes we need to trigger a change
// event and notify all the views that are listening to that specific date
function registerBusytimeChange(id, type) {
var busy = busytimes.get(id);
var {startDate, endDate} = busy;
// subtract 1 millisecond because allday events ends at 00:00:00 of next day,
// which would include one day more than expected for daysBetween
var end = new Date(endDate.getTime() - 1);
var calendarStore = core.storeFactory.get('Calendar');
// events from hidden calendars should not be displayed
var isRemove = type === 'remove' ||
!calendarStore.shouldDisplayCalendar(busy.calendarId);
daysBetween(startDate, end).forEach(date => {
if (outsideSpans(date)) {
// ignore dates outside the cached spans
return;
}
var dayId = getDayId(date);
if (isRemove && !cache.has(dayId)) {
return;
}
var day = getDay(dayId, date);
// it should always override the old data
day.basic = day.basic.filter(r => r.busytime._id !== id);
day.allday = day.allday.filter(r => r.busytime._id !== id);
if (!isRemove) {
var group = isAllDay(date, startDate, endDate) ?
day.allday :
day.basic;
sortedInsert(group, busy);
}
day.amount = day.basic.length + day.allday.length;
dayQueue.add(dayId);
});
dispatch();
}
function sortedInsert(group, busy) {
var index = binsearch.insert(group, busy.startDate, (date, record) => {
return compare(date, record.busytime.startDate);
});
var event = events.get(busy.eventId);
var calendarStore = core.storeFactory.get('Calendar');
group.splice(index, 0, {
event: event,
busytime: busy,
color: calendarStore.getColorByCalendarId(event.calendarId)
});
}
// debounce will make sure that we only trigger one "change" event per day even
// if we have multiple changes happening in a row
var dispatch = debounce(function() {
dayQueue.forEach(id => {
// need to remove ID from queue before dispatching event to avoid race
// conditions (eg. handler triggering a new queue+dispatch)
dayQueue.delete(id);
// dispatch is async so there is a small chance data isn't cached anymore
if (cache.has(id)) {
emitter.emit(id, cache.get(id));
}
});
}, DISPATCH_DELAY);
function loadMonth(newMonth) {
var span = spanOfMonth(newMonth);
// ensure we load the minimum amount of busytimes possible
var toLoad = span;
cachedSpans.every(cached => toLoad = cached.trimOverlap(toLoad));
if (!toLoad) {
// already loaded all the busytimes
return;
}
// cache the whole month instead of `toLoad` because we purge whole months
cachedSpans.push(span);
var busytimeStore = core.storeFactory.get('Busytime');
busytimeStore.loadSpan(toLoad, onBusytimeSpanLoad);
}
function onBusytimeSpanLoad(err, busytimes) {
if (err) {
console.error('Error loading Busytimes from TimeSpan:', err.toString());
return;
}
// remove duplicates and avoid loading events that are already cached
var eventIds = Array.from(new Set(
busytimes.map(b => b.eventId).filter(id => !events.has(id))
));
var eventStore = core.storeFactory.get('Event');
eventStore.findByIds(eventIds).then(events => {
// it's very important to cache the events before the busytimes otherwise
// the records won't contain the event data
Object.keys(events).forEach(key => cacheEvent(events[key]));
busytimes.forEach(cacheBusytime);
pruneCache();
});
}
function pruneCache() {
if (cacheLocked) {
return;
}
trimCachedSpans();
cache.forEach(removeDayFromCacheIfOutsideSpans);
eventsToBusytimes.forEach(removeEventIfNoBusytimes);
}
function trimCachedSpans() {
while (cachedSpans.length > MAX_CACHED_MONTHS) {
// since most changes are sequential, remove the timespans that are further
// away from the current month
var baseDate = core.timeController.month;
var maxDiff = 0;
var maxDiffIndex = 0;
cachedSpans.forEach((span, i) => {
var diff = Math.abs(span.start - baseDate);
if (diff > maxDiff) {
maxDiff = diff;
maxDiffIndex = i;
}
});
cachedSpans.splice(maxDiffIndex, 1);
}
}
function removeDayFromCacheIfOutsideSpans(day, id) {
if (outsideSpans(day.date)) {
day.basic.forEach(removeRecordIfOutsideSpans);
day.allday.forEach(removeRecordIfOutsideSpans);
cache.delete(id);
}
}
function outsideSpans(date) {
return !cachedSpans.some(timespan => timespan.contains(date));
}
function removeRecordIfOutsideSpans(record) {
removeBusytimeIfOutsideSpans(record.busytime);
}
function removeBusytimeIfOutsideSpans(busytime) {
var {_id, startDate, endDate} = busytime;
if (outsideSpans(startDate) && outsideSpans(endDate)) {
removeBusytimeById(_id);
}
}
function removeEventIfNoBusytimes(ids, eventId) {
if (!ids.length) {
removeEventById(eventId);
}
}
});