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,585 @@
/* global _, debug, Toolkit, Formatting, Common */
/* exported ChartUtils */
'use strict';
var ChartUtils = (function() {
var DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;
var DAY = 24 * 60 * 60 * 1000;
var NEVER_PERIOD = 30 * DAY;
var NEVER_ANCHOR = 21 * DAY;
var CHART_BG_RATIO = 0.87;
var FONT_SIZE = toDevicePixels(13);
var FONT_WEIGHT = '600';
var FONT_WEIGHT_AXIS = '400';
var WIFI_CHART_STROKE = '#8b9052';
var WIFI_CHART_FILL = '#cbd936';
var MOBILE_CHART_STROKE = '#762d4a';
var MOBILE_CHART_FILL = 'rgba(147, 21, 98, 0.7)';
function toDevicePixels(value) {
return value * DEVICE_PIXEL_RATIO;
}
function makeCSSFontString(fontSize, fontWeight) {
return fontWeight + ' ' + fontSize + 'px sans-serif';
}
function formatChartDate(date) {
return _('verbose-chart-date-format', {
'monthday-number': date.getDate(),
'em-month': _('month-' + date.getMonth() + '-short')
});
}
function getLimitInBytes(settings) {
var multiplier = 1000000; // MB
if (settings.dataLimitUnit === 'GB') {
multiplier = 1000000000; // GB
}
var value = settings.dataLimitValue;
return (value && value !== 0) ? value * multiplier : null;
}
function calculateUpperDate(settings) {
var today = new Date();
var trackingPeriod = settings.trackingPeriod;
var nextReset = settings.nextReset;
if (trackingPeriod !== 'never' && nextReset) {
return new Date(nextReset.getTime() - DAY);
}
var lastReset = settings.lastCompleteDataReset || today;
var offset = today.getTime() - lastReset.getTime();
var upperDate = new Date(lastReset.getTime() + NEVER_PERIOD);
if (offset >= NEVER_ANCHOR) {
upperDate = new Date(today.getTime() + (NEVER_PERIOD - NEVER_ANCHOR));
}
debug('Upper date:', upperDate);
return upperDate;
}
function calculateLowerDate(settings) {
var today = new Date();
var lowerDate = Toolkit.toMidnight(new Date());
var nextReset = settings.nextReset || lowerDate;
var trackingPeriod = settings.trackingPeriod;
if (trackingPeriod === 'weekly') {
lowerDate.setTime(nextReset.getTime() - (7 * DAY));
} else if (trackingPeriod === 'monthly') {
var monthDate = settings.resetTime;
var isFirstDayOfPeriod = today.getDate() == monthDate;
var isAfterFirstDayOfPeriod = today.getDate() > monthDate;
if (isAfterFirstDayOfPeriod) {
// lowerDate is in the current month
lowerDate.setDate(monthDate);
} else if (!isFirstDayOfPeriod) {
// lowerDate is on the previous month
var LAST_DAY_OF_PREVIOUS_MONTH = 0;
var newMonth = today.getMonth() - 1;
var newYear = today.getFullYear();
if (newMonth < 0) {
newMonth = 11;
newYear--;
}
lowerDate = Toolkit.toMidnight(new Date(newYear, newMonth, monthDate));
// The day of the month of lowerDate is different to settings.resetTime
// value when the resetTime day doesn't exists this month, (eg. 30th
// February), on this case, the lowerDate must be the last day of the
// previous month
if (lowerDate.getDate() != monthDate) {
lowerDate = Toolkit.toMidnight(new Date());
lowerDate.setDate(LAST_DAY_OF_PREVIOUS_MONTH);
}
}
} else {
var lastReset = settings.lastCompleteDataReset || lowerDate;
lowerDate = lastReset;
var offset = today.getTime() - lastReset.getTime();
if (offset >= NEVER_ANCHOR) {
lowerDate = new Date(today.getTime() - NEVER_ANCHOR);
}
}
debug('Lower date:', lowerDate);
return lowerDate;
}
// Expand the model with some computed values
function expandModel(base) {
// Update today
var today = Toolkit.toMidnight(new Date());
// Graphic settings
base.originY = Math.floor(base.height * CHART_BG_RATIO);
// Today value
base.axis.X.today = today;
// Normalize today
Toolkit.toMidnight(base.axis.X.today);
Toolkit.toMidnight(base.axis.X.lower);
Toolkit.toMidnight(base.axis.X.upper);
// X axis projection function to convert a value into a pixel value
var xLowerBound = base.axis.X.lower.getTime();
var xSize = base.axis.X.upper.getTime() - xLowerBound;
var realWidth = base.endX - base.originX;
base.axis.X.len = realWidth;
base.axis.X.get = function cc_dataToXPx(value) {
var projection = (value.getTime() - xLowerBound) / xSize;
return projection * realWidth + base.originX;
};
// Y max value
var limitEnabled = true; // XXX: model.limits.enabled;
base.axis.Y.maxValue = Math.max(limitEnabled ? base.limits.value : 0,
base.data.mobile.total,
base.data.wifi.total);
// Y axis projection function and automatic values
base.axis.Y.range = base.axis.Y.upper - base.axis.Y.lower;
base.axis.Y.upper = (1 + base.axis.Y.margin) * base.axis.Y.maxValue;
var yLowerBound = base.axis.Y.lower;
var ySize = base.axis.Y.upper - yLowerBound;
base.axis.Y.get = function cc_dataToYPx(value) {
var projection = (value - yLowerBound) / ySize;
return base.originY * (1 - projection);
};
base.axis.Y.step = base.axis.Y.get(base.axis.Y.maxValue);
// Limits
base.limits.warning = Common.DATA_USAGE_WARNING;
base.limits.warningValue = base.limits.value * Common.DATA_USAGE_WARNING;
}
// Check if the segment of the graph is inside chart area. If so, draw it
function clipAndDrawSegment(ctx, model, x0, y0, x1, y1) {
if (x0 >= model.originX && x1 <= model.endX) {
var x0Fixed = Math.floor(x0) - 0.5;
var x1Fixed = Math.floor(x1) + 0.5;
// Fill
ctx.globalCompositeOperation = 'destination-over';
ctx.beginPath();
ctx.lineTo(x1Fixed, y1);
ctx.lineTo(x1Fixed, model.originY);
ctx.lineTo(x0Fixed, model.originY);
ctx.lineTo(x0Fixed, y0);
ctx.moveTo(x1Fixed, y0);
ctx.fill();
// Stroke
ctx.globalCompositeOperation = 'source-over';
ctx.beginPath();
// It's necessary add 1 to the X coordinate to make up for the offset
// produced with the drawing of the previous line.
ctx.moveTo(x0Fixed + 1, y0);
ctx.lineTo(x1Fixed, y1);
ctx.stroke();
}
}
function drawTodayMark(ctx, x, y, color) {
ctx.save();
var radius = toDevicePixels(4);
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
function drawBackgroundLayer(canvas, model, showMobile) {
canvas.height = model.height;
var width = canvas.width = model.width;
var ctx = canvas.getContext('2d');
ctx.save();
// White bg
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, width, model.originY);
// Horizontal lines every step
var step = model.axis.Y.step;
var limitY = model.axis.Y.get(model.limits.value);
ctx.strokeStyle = '#e0e0e0';
ctx.lineWidth = toDevicePixels(1);
var displayLimit = model.limits.enabled && showMobile;
for (var y = model.originY - step; y > step; y -= step) {
if (displayLimit && same(y, limitY, 0.1)) {
continue;
}
var drawY = Math.floor(y) - 0.5;
ctx.beginPath();
ctx.moveTo(model.originX, drawY);
ctx.lineTo(model.endX, drawY);
ctx.stroke();
}
// Vertical lines every day
var days = (model.axis.X.upper - model.axis.X.lower) / DAY;
step = model.axis.X.len / days;
ctx.strokeStyle = '#eeeeee';
ctx.lineWidth = toDevicePixels(1);
for (var x = model.originX; x <= model.endX; x += step) {
var drawX = Math.floor(x) + 0.5;
ctx.beginPath();
ctx.moveTo(drawX, model.originY);
ctx.lineTo(drawX, 0);
ctx.stroke();
// Ensure draw the last vertical line
if (((x + step) > model.endX) && (x !== model.endX)) {
// the 0.5 offset is needed to avoid drawing a double line.
drawX = model.endX - 0.5;
ctx.beginPath();
ctx.moveTo(drawX, model.originY);
ctx.lineTo(drawX, 0);
ctx.stroke();
}
}
ctx.restore();
// Return true if a and b not differ more than threshold
function same(a, b, threshold) {
threshold = threshold || 0;
return Math.abs(a - b) <= threshold;
}
}
function drawTodayLayer(canvas, model) {
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
// Compute the X offset
var offsetX = model.axis.X.get(model.axis.X.today);
// Configure Centered today text
var marginTop = toDevicePixels(10);
var todayTag = formatChartDate(model.axis.X.today);
// Render the text
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
// Update measues of today label
var metrics = ctx.measureText(todayTag);
model.todayLabel.x0 = offsetX - metrics.width / 2;
model.todayLabel.x1 = offsetX + metrics.width / 2;
if (model.todayLabel.x1 > model.endX) {
ctx.textAlign = 'right';
}
ctx.fillStyle = 'black';
ctx.fillText(todayTag, offsetX, model.originY + marginTop);
setAccessibilityAttributes(canvas, 'today-layer', { today: todayTag });
}
function drawAxisLayer(canvas, model, showMobile) {
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
// Start drawing Y axis
var step = model.axis.Y.step;
var dataStep = model.axis.Y.upper - model.axis.Y.maxValue;
var marginRight = 4;
var offsetX = model.originX - marginRight;
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT_AXIS);
ctx.textAlign = 'right';
var displayLimit = showMobile && model.limits.enabled;
var lastUnit;
for (var y = 0.5 + model.originY, value = 0;
y > step; y -= step, value += dataStep) {
// First X label for 0 is aligned with the bottom
if (value === 0) {
lastUnit = Formatting.smartRound(dataStep, 0)[1];
ctx.textBaseline = 'bottom';
ctx.fillStyle = '#6a6a6a';
ctx.fillText(Formatting.formatData([0, lastUnit]), offsetX, y - 2.5);
continue;
}
// Rest of labels are aligned with the middle
var rounded = Formatting.smartRound(value, -1);
var v = rounded[0];
var u = rounded[1];
var label = v;
if (lastUnit !== u) {
label = Formatting.formatData([v, u]);
}
lastUnit = u;
ctx.textBaseline = 'middle';
ctx.fillStyle = displayLimit &&
(value === model.limits.value) &&
model.limits.enabled ?
'#b50202' : '#6a6a6a';
ctx.fillText(label, offsetX, y);
}
// Now the X axis
ctx.fillStyle = '#6a6a6a';
var marginTop = toDevicePixels(10);
// Left tag
var leftTag = formatChartDate(model.axis.X.lower);
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
var isBelowToday = model.todayLabel.x0 <=
model.originX + ctx.measureText(leftTag).width;
if (!isBelowToday) {
ctx.fillText(leftTag, model.originX, model.originY + marginTop);
}
// Right tag
var rightTag = formatChartDate(model.axis.X.upper);
ctx.textAlign = 'right';
isBelowToday = model.todayLabel.x1 >=
model.endX - ctx.measureText(rightTag).width;
if (!isBelowToday) {
ctx.fillText(rightTag, model.endX, model.originY + marginTop);
}
setAccessibilityAttributes(canvas, 'axis-layer',
{ from: leftTag, to: rightTag });
}
function setAccessibilityAttributes(element, identifier, data) {
if (identifier) {
element.setAttribute('data-l10n-id', identifier);
if (data) {
element.setAttribute('data-l10n-args', JSON.stringify(data));
}
} else {
element.removeAttribute('data-l10n-id');
element.removeAttribute('data-l10n-args');
element.removeAttribute('aria-label');
}
}
function drawLimits(canvas, model, showMobile) {
var set = model.limits.value;
var color = '#b50202';
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
var displayLimit = showMobile && model.limits.enabled;
if (!displayLimit) {
return;
}
ctx.save();
var marginTop = 1;
var offsetY = set ? model.axis.Y.get(model.limits.value) :
FONT_SIZE + 2 * marginTop;
ctx.font = makeCSSFontString(FONT_SIZE, FONT_WEIGHT);
// The dashed limit line
var lineLength = toDevicePixels(15);
var gapLength = toDevicePixels(7);
ctx.strokeStyle = color;
ctx.lineWidth = toDevicePixels(1);
ctx.beginPath();
for (var x = model.originX, drawY = Math.floor(offsetY) - 0.5;
x < model.endX; x += gapLength) {
ctx.moveTo(x, drawY);
ctx.lineTo(Math.min(x += lineLength, model.endX), drawY);
}
ctx.stroke();
ctx.restore();
setAccessibilityAttributes(canvas,
'limit-layer-' + model.limits.dataLimitUnit,
{ value: model.limits.dataLimitValue });
}
function drawDataLayer(canvas, model, sampleType, style) {
var samples = (model.data[sampleType] || {}).samples;
if (!samples) {
return;
}
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
// Style
ctx.fillStyle = style.fill;
ctx.strokeStyle = style.stroke;
ctx.lineWidth = toDevicePixels(2);
ctx.lineJoin = 'round';
ctx.moveTo(model.originX, model.originY);
var today = Toolkit.toMidnight(new Date());
// offset in milliseconds
var offset = today.getTimezoneOffset() * 60 * 1000;
var sum = 0; var x, y = model.originY;
var lastX = model.originX, lastY = model.axis.Y.get(sum);
// Only dealing with the use cases of negative UTC offset because, due to
// the database granularity, the returned samples are given in UTC days, and
// we don't know what part of the traffic sample values correspond to the
// current localtime day. The query for the samples is generated on the
// costcontrol module, and the case into which the returned samples only
// have one record with the data for the localtime day of yesterday is not
// possible.
for (var i = 0, len = samples.length; i < len; i++) {
var sample = samples[i];
var sampleLocalTime = sample.date.getTime() + offset;
var sampleUTCDate = Toolkit.toMidnight(new Date(sampleLocalTime));
var isToday = (today.getTime() === sampleUTCDate.getTime());
var isTomorrow = (today.getTime() + DAY === sampleUTCDate.getTime());
var thereIsATomorrowSample = (isToday && (i + 2 === len));
// Depends on the hour of the day and the offset, it is possible the
// networkStats API returns the current data mobile in the tomorrow
// sample, because on the UTC hour is another day.
if (thereIsATomorrowSample) {
// Join the value of the samples for today and tomorrow
var tomorrowSample = samples[i+1];
if (typeof sample.value === 'undefined') {
sample.value = tomorrowSample.value;
} else if (typeof tomorrowSample.value !== 'undefined') {
sample.value += tomorrowSample.value;
}
if (i === 0) {
lastX = model.axis.X.get(sample.date);
}
i++;
}
if (typeof sample.value === 'undefined') {
lastX = x = model.axis.X.get(sample.date);
ctx.moveTo(x, y);
} else {
if (i === 0) {
lastX = model.axis.X.get(sample.date);
}
sum += sample.value;
x = model.axis.X.get(sample.date);
y = model.axis.Y.get(sum);
clipAndDrawSegment(ctx, model, lastX, lastY, x, y);
lastX = x;
lastY = y;
}
var onlyExistTomorrowSample = (i===0 && isTomorrow);
var isXInsideTheGraph = (x >= model.originX);
if ((isToday || onlyExistTomorrowSample) && isXInsideTheGraph) {
drawTodayMark(ctx, x, y, style.stroke);
return;
}
}
if (!style.pattern) {
return;
}
var pattern = ctx.createPattern(style.pattern, 'repeat');
ctx.globalCompositeOperation = 'source-atop';
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, model.width, model.originY);
}
function drawWarningLayer(canvas, model) {
canvas.height = model.height;
canvas.width = model.width;
var ctx = canvas.getContext('2d');
if (!model.limits.enabled || model.limits.value === null) {
setAccessibilityAttributes(canvas);
return;
}
// No problem here
var mobileUsage = model.data.mobile.total;
var mobileUsageDifference = mobileUsage - model.limits.value;
if (mobileUsage <= model.limits.warningValue) {
setAccessibilityAttributes(canvas);
return;
}
// Warning mode
if (mobileUsage <= model.limits.value) {
var limitValue = Math.floor(model.axis.Y.get(model.limits.value));
var warningValue =
Math.round(model.axis.Y.get(model.limits.warningValue));
ctx.beginPath();
ctx.fillStyle = 'rgba(255, 112, 0, 0.5)';
ctx.fillRect(
model.originX, limitValue,
model.axis.X.len + 0.5, warningValue - limitValue
);
setAccessibilityAttributes(canvas, 'warning-layer', {
value: Formatting.formatData(Formatting.roundData(
-mobileUsageDifference))
});
return;
}
// Limit exceeded
var limitValueExceeded = model.axis.Y.get(model.limits.value);
ctx.beginPath();
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fillRect(
model.originX, 0,
model.axis.X.len + 0.5, limitValueExceeded
);
setAccessibilityAttributes(canvas, 'limit-exceeded-layer', {
value: Formatting.formatData(Formatting.roundData(mobileUsageDifference))
});
}
return {
// Constants
WIFI_CHART_STROKE: WIFI_CHART_STROKE,
WIFI_CHART_FILL: WIFI_CHART_FILL,
MOBILE_CHART_STROKE: MOBILE_CHART_STROKE,
MOBILE_CHART_FILL: MOBILE_CHART_FILL,
// Functions
toDevicePixels: toDevicePixels,
makeCSSFontString: makeCSSFontString,
formatChartDate: formatChartDate,
getLimitInBytes: getLimitInBytes,
calculateUpperDate: calculateUpperDate,
calculateLowerDate: calculateLowerDate,
expandModel: expandModel,
clipAndDrawSegment: clipAndDrawSegment,
drawTodayMark: drawTodayMark,
drawBackgroundLayer: drawBackgroundLayer,
drawTodayLayer: drawTodayLayer,
drawAxisLayer: drawAxisLayer,
drawLimits: drawLimits,
drawDataLayer: drawDataLayer,
drawWarningLayer: drawWarningLayer
};
}());