<?php
/*
* Jirafeau, your web file repository
- * Copyright (C) 2015 Jerome Jutteau <j.jutteau@gmail.com>
+ * Copyright (C) 2015 Jerome Jutteau <jerome@jutteau.fr>
* Copyright (C) 2015 Nicola Spanti (RyDroid) <dev@nicola-spanti.info>
*
* This program is free software: you can redistribute it and/or modify
*/
header('Content-Type: text/javascript');
-
define('JIRAFEAU_ROOT', dirname(__FILE__) . '/../');
require(JIRAFEAU_ROOT . 'lib/settings.php');
require(JIRAFEAU_ROOT . 'lib/functions.php');
require(JIRAFEAU_ROOT . 'lib/lang.php');
+
?>
+// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
+var web_root = "<?php echo $cfg['web_root']; ?>";
-function translate (expr)
-{
- var lang_array = <?php echo json_lang_generator() ?>;
- if (lang_array.hasOwnProperty(expr))
- return lang_array[expr];
- return expr;
+var lang_array = <?php echo json_lang_generator(null); ?>;
+var lang_array_fallback = <?php echo json_lang_generator("en"); ?>;
+
+function translate (expr) {
+ if (lang_array.hasOwnProperty(expr)) {
+ var e = lang_array[expr];
+ if (!isEmpty(e))
+ return e;
+ }
+ if (lang_array_fallback.hasOwnProperty(expr)) {
+ var e = lang_array_fallback[expr];
+ if (!isEmpty(e))
+ return e;
+ }
+ return "FIXME: " + expr;
+}
+
+function isEmpty(str) {
+ return (!str || 0 === str.length);
+}
+
+// Extend date object with format method
+Date.prototype.format = function(format) {
+ format = format || 'YYYY-MM-DD hh:mm';
+
+ var zeropad = function(number, length) {
+ number = number.toString();
+ length = length || 2;
+ while(number.length < length)
+ number = '0' + number;
+ return number;
+ },
+ formats = {
+ YYYY: this.getFullYear(),
+ MM: zeropad(this.getMonth() + 1),
+ DD: zeropad(this.getDate()),
+ hh: zeropad(this.getHours()),
+ mm: zeropad(this.getMinutes()),
+ O: (function() {
+ localDate = new Date;
+ sign = (localDate.getTimezoneOffset() > 0) ? '-' : '+';
+ offset = Math.abs(localDate.getTimezoneOffset());
+ hours = zeropad(Math.floor(offset / 60));
+ minutes = zeropad(offset % 60);
+ return sign + hours + ":" + minutes;
+ })()
+ },
+ pattern = '(' + Object.keys(formats).join(')|(') + ')';
+
+ return format.replace(new RegExp(pattern, 'g'), function(match) {
+ return formats[match];
+ });
+};
+
+function dateFromUtcString(datestring) {
+ // matches »YYYY-MM-DD hh:mm«
+ var m = datestring.match(/(\d+)-(\d+)-(\d+)\s+(\d+):(\d+)/);
+ return new Date(Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], 0));
+}
+
+function dateFromUtcTimestamp(datetimestamp) {
+ return new Date(parseInt(datetimestamp) * 1000)
+}
+
+function dateToUtcString(datelocal) {
+ return new Date(
+ datelocal.getUTCFullYear(),
+ datelocal.getUTCMonth(),
+ datelocal.getUTCDate(),
+ datelocal.getUTCHours(),
+ datelocal.getUTCMinutes(),
+ datelocal.getUTCSeconds()
+ ).format();
+}
+
+function dateToUtcTimestamp(datelocal) {
+ return (Date.UTC(
+ datelocal.getUTCFullYear(),
+ datelocal.getUTCMonth(),
+ datelocal.getUTCDate(),
+ datelocal.getUTCHours(),
+ datelocal.getUTCMinutes(),
+ datelocal.getUTCSeconds()
+ ) / 1000);
+}
+
+function convertAllDatetimeFields() {
+ datefields = document.getElementsByClassName('datetime')
+ for(var i=0; i<datefields.length; i++) {
+ dateUTC = datefields[i].getAttribute('data-datetime');
+ datefields[i].setAttribute('title', dateUTC + ' (GMT)');
+ datefields[i].innerHTML = dateFromUtcString(dateUTC).format('YYYY-MM-DD hh:mm (GMT O)');
+ }
}
-function show_link (url, reference, delete_code, crypt_key, date)
+function show_link (reference, delete_code, crypt_key, date)
{
// Upload finished
document.getElementById('uploading').style.display = 'none';
document.getElementById('upload').style.display = 'none';
document.getElementById('upload_finished').style.display = '';
- document.title = 'Jirafeau - 100%';
+ document.title = "100% - <?php echo empty($cfg['title']) ? 'Jirafeau' : $cfg['title']; ?>";
// Download page
- var download_link = url + 'f.php?h=' + reference;
- var download_link_href = url + 'f.php?h=' + reference;
+ var download_link_href = 'f.php?h=' + reference;
if (crypt_key.length > 0)
{
- download_link += '&k=' + crypt_key;
download_link_href += '&k=' + crypt_key;
}
if (!!document.getElementById('upload_finished_download_page'))
{
- document.getElementById('upload_link').innerHTML = download_link;
document.getElementById('upload_link').href = download_link_href;
+ document.getElementById('upload_link_text').innerHTML = web_root + download_link_href;
}
// Email link
var filename = document.getElementById('file_select').files[0].name;
- var b = encodeURIComponent("Download file \"" + filename + "\":") + "%0D";
- b += encodeURIComponent(download_link_href) + "%0D";
- if (date)
- b += "%0D" + encodeURIComponent("This file will be available until " + date) + "%0D";
- document.getElementById('upload_link_email').href = "mailto:?body=" + b + "&subject=" + encodeURIComponent(filename);
+ var b = encodeURIComponent("<?php echo t("DL"); ?> \"" + filename + "\":") + "%0D" + "%0A";
+ b += encodeURIComponent(web_root + download_link_href) + "%0D" + "%0A";
+ if (false == isEmpty(date))
+ {
+ b += "%0D" + "%0A" + encodeURIComponent("<?php echo t("VALID_UNTIL"); ?>: " + date.format('YYYY-MM-DD hh:mm (GMT O)')) + "%0D" + "%0A";
+ document.getElementById('upload_link_email').href = "mailto:?body=" + b + "&subject=" + encodeURIComponent(filename);
+ }
// Delete link
- var delete_link = url + 'f.php?h=' + reference + '&d=' + delete_code;
- var delete_link_href = url + 'f.php?h=' + reference + '&d=' + delete_code;
- document.getElementById('delete_link').innerHTML = delete_link;
+ var delete_link_href = 'f.php?h=' + reference + '&d=' + delete_code;
document.getElementById('delete_link').href = delete_link_href;
+ document.getElementById('delete_link_text').innerHTML = web_root + delete_link_href;
// Validity date
- if (date)
+ if (isEmpty(date))
{
- document.getElementById('date').innerHTML = date;
- document.getElementById('validity').style.display = '';
+ document.getElementById('date').style.display = 'none';
+ }
+ else {
+ document.getElementById('date').innerHTML = '<span class="datetime" title="'
+ + dateToUtcString(date) + ' (GMT)">'
+ + date.format('YYYY-MM-DD hh:mm (GMT O)')
+ + '</span>';
+ document.getElementById('date').style.display = '';
}
- else
- document.getElementById('validity').style.display = 'none';
// Preview link (if allowed)
if (!!document.getElementById('preview_link'))
{
document.getElementById('upload_finished_preview').style.display = 'none';
- var preview_link = url + 'f.php?h=' + reference + '&p=1';
- var preview_link_href = url + 'f.php?h=' + reference + '&p=1';
+ var preview_link_href = 'f.php?h=' + reference + '&p=1';
if (crypt_key.length > 0)
{
- preview_link += '&k=' + crypt_key;
preview_link_href += '&k=' + crypt_key;
}
// Test if content can be previewed
- type = document.getElementById('file_select').files[0].type;
- if (type.indexOf("image") > -1 ||
- type.indexOf("audio") > -1 ||
- type.indexOf("text") > -1 ||
- type.indexOf("video") > -1)
- {
- document.getElementById('preview_link').innerHTML = preview_link;
+ type = document.getElementById('file_select').files[0].type;
+ if ((type.startsWith('image/')
+ || type.startsWith('audio')
+ || type.startsWith('text/plain')
+ || type.startsWith('video/'))
+ && !type.includes('image/svg+xml'))
+ {
document.getElementById('preview_link').href = preview_link_href;
+ document.getElementById('preview_link_text').innerHTML = web_root + preview_link_href;
document.getElementById('upload_finished_preview').style.display = '';
- }
+ }
}
// Direct download link
- var direct_download_link = url + 'f.php?h=' + reference + '&d=1';
- var direct_download_link_href = url + 'f.php?h=' + reference + '&d=1';
+ var direct_download_link_href = 'f.php?h=' + reference + '&d=1';
if (crypt_key.length > 0)
{
- direct_download_link += '&k=' + crypt_key;
direct_download_link_href += '&k=' + crypt_key;
}
- document.getElementById('direct_link').innerHTML = direct_download_link;
document.getElementById('direct_link').href = direct_download_link_href;
-
+ document.getElementById('direct_link_text').innerHTML = web_root + direct_download_link_href;
// Hide preview and direct download link if password is set
if (document.getElementById('input_key').value.length > 0)
document.getElementById('uploaded_percentage').innerHTML = percentage;
document.getElementById('uploaded_speed').innerHTML = speed;
document.getElementById('uploaded_time').innerHTML = time_left;
- document.title = 'Jirafeau - ' + percentage;
+ document.title = percentage + " - <?php echo empty($cfg['title']) ? 'Jirafeau' : $cfg['title']; ?>";
}
function hide_upload_progression ()
document.getElementById('uploaded_percentage').style.display = 'none';
document.getElementById('uploaded_speed').style.display = 'none';
document.getElementById('uploaded_time').style.display = 'none';
- document.title = 'Jirafeau';
+ document.title = "<?php echo empty($cfg['title']) ? 'Jirafeau' : $cfg['title']; ?>";
}
function upload_progress (e)
}
else
{
- document.getElementById('options').style.display = '';
- document.getElementById('send').style.display = '';
+ // add class to restyle upload form in next step
+ document.getElementById('upload').setAttribute('class', 'file-selected');
+ // display options
+ document.getElementById('options').style.display = 'block';
+ document.getElementById('send').style.display = 'block';
document.getElementById('error_pop').style.display = 'none';
- document.getElementById('file_select').style.left = 'inherit';
- document.getElementById('file_select').style.height = 'inherit';
- document.getElementById('file_select').style.opacity = '1';
+ document.getElementById('send').focus();
}
}
+function XHRErrorHandler(e)
+{
+ var text = "${e.type}: ${e.loaded} bytes transferred"
+ console.log(text)
+}
+
function pop_failure (e)
{
- var text = "An error occured";
+ var text = "<p>An error occured";
if (typeof e !== 'undefined')
- text = e;
- text = "<p>" + text + "</p>";
+ text += ": " + e;
+ text += "</p>";
document.getElementById('error_pop').innerHTML = e;
document.getElementById('uploading').style.display = 'none';
d.setSeconds (d.getSeconds() + 604800);
return true;
}
+ if (time == 'fortnight')
+ {
+ d.setSeconds (d.getSeconds() + 1209600);
+ return true;
+ }
if (time == 'month')
{
- d.setSeconds (d.getSeconds() + 2419200);
+ d.setSeconds (d.getSeconds() + 2592000);
return true;
}
if (time == 'quarter')
{
- d.setSeconds (d.getSeconds() + 7257600);
+ d.setSeconds (d.getSeconds() + 7776000);
return true;
}
if (time == 'year')
{
- d.setSeconds (d.getSeconds() + 29030400);
+ d.setSeconds (d.getSeconds() + 31536000);
return true;
}
return false;
}
-function classic_upload (url, file, time, password, one_time, upload_password)
+function classic_upload (file, time, password, one_time)
{
// Delay time estimation init as we can't have file size
upload_time_estimation_init(0);
var req = new XMLHttpRequest ();
req.upload.addEventListener ("progress", upload_progress, false);
- req.addEventListener ("error", pop_failure, false);
- req.addEventListener ("abort", pop_failure, false);
+ req.addEventListener ("error", XHRErrorHandler, false);
+ req.addEventListener ("abort", XHRErrorHandler, false);
req.onreadystatechange = function ()
{
if (req.readyState == 4 && req.status == 200)
{
var res = req.responseText;
- if (res == "Error")
+
+ // if response starts with "Error" then show a failure
+ if (/^Error/.test(res))
{
- pop_failure ();
+ pop_failure (res);
return;
}
+
res = res.split ("\n");
+ var expiryDate = '';
if (time != 'none')
{
- var d = new Date();
- if(!add_time_string_to_date(d, time))
+ // convert time (local time + selected expiry date)
+ var localDatetime = new Date();
+ if(!add_time_string_to_date(localDatetime, time))
+ {
+ pop_failure ('Error: Date can not be parsed');
return;
- show_link (url, res[0], res[1], res[2], d.toString());
+ }
+ expiryDate = localDatetime;
}
- else
- show_link (url, res[0], res[1], res[2]);
+
+ show_link (res[0], res[1], res[2], expiryDate);
+ }
+ else
+ {
+ pop_failure ("<?php echo t("ERR_OCC"); ?>");
}
}
- req.open ("POST", url + 'script.php' , true);
+ req.open ("POST", 'script.php' , true);
var form = new FormData();
form.append ("file", file);
form.append ("key", password);
if (one_time)
form.append ("one_time_download", '1');
- if (upload_password.length > 0)
- form.append ("upload_password", upload_password);
-
req.send (form);
}
}
var async_global_transfered = 0;
-var async_global_url = '';
var async_global_file;
var async_global_ref = '';
var async_global_max_size = 0;
var async_global_time;
var async_global_transfering = 0;
+var async_global_last_code;
-function async_upload_start (url, max_size, file, time, password, one_time, upload_password)
+function async_upload_start (max_size, file, time, password, one_time)
{
async_global_transfered = 0;
- async_global_url = url;
async_global_file = file;
async_global_max_size = max_size;
async_global_time = time;
var req = new XMLHttpRequest ();
- req.addEventListener ("error", pop_failure, false);
- req.addEventListener ("abort", pop_failure, false);
+ req.addEventListener ("error", XHRErrorHandler, false);
+ req.addEventListener ("abort", XHRErrorHandler, false);
req.onreadystatechange = function ()
{
if (req.readyState == 4 && req.status == 200)
{
var res = req.responseText;
- if (res == "Error")
+
+ if (/^Error/.test(res))
{
- pop_failure ();
+ pop_failure (res);
return;
}
+
res = res.split ("\n");
async_global_ref = res[0];
var code = res[1];
async_upload_push (code);
}
}
- req.open ("POST", async_global_url + 'script.php?init_async' , true);
+ req.open ("POST", 'script.php?init_async' , true);
var form = new FormData();
form.append ("filename", async_global_file.name);
form.append ("key", password);
if (one_time)
form.append ("one_time_download", '1');
- if (upload_password.length > 0)
- form.append ("upload_password", upload_password);
// Start time estimation
upload_time_estimation_init(async_global_file.size);
function async_upload_push (code)
{
+ async_global_last_code = code;
if (async_global_transfered == async_global_file.size)
{
hide_upload_progression ();
}
var req = new XMLHttpRequest ();
req.upload.addEventListener ("progress", async_upload_progress, false);
- req.addEventListener ("error", pop_failure, false);
- req.addEventListener ("abort", pop_failure, false);
+ req.addEventListener ("error", XHRErrorHandler, false);
+ req.addEventListener ("abort", XHRErrorHandler, false);
req.onreadystatechange = function ()
{
- if (req.readyState == 4 && req.status == 200)
+ if (req.readyState == 4)
{
- var res = req.responseText;
- if (res == "Error")
+ if (req.status == 200)
{
- pop_failure ();
+ var res = req.responseText;
+
+ // This error may be triggered when Jirafeau does not receive any file in POST.
+ // This may be due to bad php configuration where post_max_size is too low
+ // comparing to upload_max_filesize. Let's retry with lower file size.
+ if (res === "Error 23")
+ {
+ async_global_max_size = Math.max(1, async_global_max_size - 500);
+ async_upload_push (async_global_last_code);
+ return;
+ }
+ else if (/^Error/.test(res))
+ {
+ pop_failure (res);
+ return;
+ }
+
+ res = res.split ("\n");
+ var code = res[0]
+ async_global_transfered = async_global_transfering;
+ async_upload_push (code);
+ return;
+ }
+ else
+ {
+ // lower async_global_max_size and retry
+ // This can occurs in several cases:
+ // - Request Entity Too Large (413) due to server bad configuration relative to PHP configuration
+ // - Server Error (500) which can happen when PHP's `max_execution_time` is too low comparared to sent size
+ async_global_max_size = Math.max(1, parseInt (async_global_max_size * 0.5));
+ async_upload_push (async_global_last_code);
return;
}
- res = res.split ("\n");
- var code = res[0]
- async_global_transfered = async_global_transfering;
- async_upload_push (code);
}
}
- req.open ("POST", async_global_url + 'script.php?push_async' , true);
+ req.open ("POST", 'script.php?push_async' , true);
- var chunk_size = parseInt (async_global_max_size * 0.50);
var start = async_global_transfered;
- var end = start + chunk_size;
+ var end = start + async_global_max_size;
if (end >= async_global_file.size)
end = async_global_file.size;
var blob = async_global_file.slice (start, end);
function async_upload_end (code)
{
var req = new XMLHttpRequest ();
- req.addEventListener ("error", pop_failure, false);
- req.addEventListener ("abort", pop_failure, false);
+ req.addEventListener ("error", XHRErrorHandler, false);
+ req.addEventListener ("abort", XHRErrorHandler, false);
req.onreadystatechange = function ()
{
if (req.readyState == 4 && req.status == 200)
{
var res = req.responseText;
- if (res == "Error")
+
+ if (/^Error/.test(res))
{
- pop_failure ();
+ pop_failure (res);
return;
}
+
res = res.split ("\n");
+ var expiryDate = '';
if (async_global_time != 'none')
{
- var d = new Date();
- if(!add_time_string_to_date(d, async_global_time))
- return;
- show_link (async_global_url, res[0], res[1], res[2], d.toString());
+ // convert time (local time + selected expiry date)
+ var localDatetime = new Date();
+ if(!add_time_string_to_date(localDatetime, async_global_time)) {
+ pop_failure ('Error: Date can not be parsed');
+ return;
+ }
+ expiryDate = localDatetime;
}
- else
- show_link (async_global_url, res[0], res[1], res[2]);
+
+ show_link (res[0], res[1], res[2], expiryDate);
}
}
- req.open ("POST", async_global_url + 'script.php?end_async' , true);
+ req.open ("POST", 'script.php?end_async' , true);
var form = new FormData();
form.append ("ref", async_global_ref);
req.send (form);
}
-function upload (url, max_size)
+function upload (max_chunk_size)
{
- if (check_html5_file_api ()
- && document.getElementById('file_select').files[0].size >= max_size)
+ var one_time_checkbox = document.getElementById('one_time_download');
+ var one_time = one_time_checkbox !== null ? one_time_checkbox.checked : false;
+ if (check_html5_file_api ())
{
- async_upload_start (url,
- max_size,
+ async_upload_start (
+ max_chunk_size,
document.getElementById('file_select').files[0],
document.getElementById('select_time').value,
document.getElementById('input_key').value,
- document.getElementById('one_time_download').checked,
- document.getElementById('upload_password').value
+ one_time
);
}
else
{
- classic_upload (url,
+ classic_upload (
document.getElementById('file_select').files[0],
document.getElementById('select_time').value,
document.getElementById('input_key').value,
- document.getElementById('one_time_download').checked,
- document.getElementById('upload_password').value
+ one_time
);
}
}
// Let's compute the current speed
var d = new Date();
var speed = upload_time_estimation_moving_average_speed;
- if (d.getTime() - upload_time_estimation_transfered_date != 0)
+ if (d.getTime() - upload_time_estimation_transfered_date != 0) {
speed = (total_transfered_size - upload_time_estimation_transfered_size)
/ (d.getTime() - upload_time_estimation_transfered_date);
+ speed = Math.max(0, speed);
+ }
// Let's compute moving average speed on 30 values
var m = (upload_time_estimation_moving_average_speed * 29 + speed) / 30;
// Update global values
if (s <= 1000)
{
res = s.toString();
- scale = "o/s";
+ scale = "B/s";
}
else if (s < 1000000)
{
res = Math.floor(s/100) / 10;
- scale = "Ko/s";
+ scale = "KB/s";
}
else
{
res = Math.floor(s/100000) / 10;
- scale = "Mo/s";
+ scale = "MB/s";
}
if (res == 0)
return '';
function milliseconds_to_time_string (milliseconds)
{
function numberEnding (number) {
- return (number > 1) ? 's' : '';
+ return (number > 1) ? translate ('PLURAL_ENDING') : '';
}
var temp = Math.floor(milliseconds / 1000);
var years = Math.floor(temp / 31536000);
if (years) {
- return years + ' ' + translate ('year') + numberEnding(years);
+ return years + ' ' + translate ('YEAR') + numberEnding(years);
}
var days = Math.floor((temp %= 31536000) / 86400);
if (days) {
- return days + ' ' + translate ('day') + numberEnding(days);
+ return days + ' ' + translate ('DAY') + numberEnding(days);
}
var hours = Math.floor((temp %= 86400) / 3600);
if (hours) {
- return hours + ' ' + translate ('hour') + numberEnding(hours);
+ return hours + ' ' + translate ('HOUR') + numberEnding(hours);
}
var minutes = Math.floor((temp %= 3600) / 60);
if (minutes) {
- return minutes + ' ' + translate ('minute') + numberEnding(minutes);
+ return minutes + ' ' + translate ('MINUTE') + numberEnding(minutes);
}
var seconds = temp % 60;
if (seconds) {
- return seconds + ' ' + translate ('second') + numberEnding(seconds);
+ return seconds + ' ' + translate ('SECOND') + numberEnding(seconds);
}
- return translate ('less than a second');
+ return translate ('LESS_1_SEC');
}
function upload_time_estimation_time()
{
// Estimate remaining time
- if (upload_time_estimation_moving_average_speed == 0)
+ if (upload_time_estimation_moving_average_speed <= 0)
return 0;
return (upload_time_estimation_total_size - upload_time_estimation_transfered_size)
/ upload_time_estimation_moving_average_speed;
}
return upload_speed_refresh_limiter_last_value;
}
+
+// document.ready()
+document.addEventListener('DOMContentLoaded', function(event) {
+ // Search for all datetime fields and convert the time to local timezone
+ convertAllDatetimeFields();
+});
+
+// Add copy event listeners
+function copyLinkToClipboard(link_id) {
+ var focus = document.activeElement;
+ var e = document.getElementById(link_id);
+
+ var tmp = document.createElement("textarea");
+ document.body.appendChild(tmp);
+ tmp.textContent = e.href;
+ tmp.focus();
+ tmp.setSelectionRange(0, tmp.value.length);
+ document.execCommand("copy");
+ document.body.removeChild(tmp);
+
+ focus.focus();
+}
+
+function addCopyListener(button_id, link_id) {
+ if(document.getElementById(button_id)){
+ document.getElementById(button_id)
+ .addEventListener("click", function() {
+ copyLinkToClipboard(link_id);});
+ }
+}
+
+function set_dark_mode() {
+ let steel_sheet = "<?php echo 'media/' . $cfg['dark_style'] . '/style.css.php'; ?>";
+ let shortcut_icon = "<?php echo 'media/' . $cfg['dark_style'] . '/favicon.ico'; ?>";
+ document.getElementById('stylesheet').href = steel_sheet;
+ document.getElementById('shortcut_icon').href = steel_sheet;
+}
+
+function set_light_mode() {
+ let steel_sheet = "<?php echo 'media/' . $cfg['style'] . '/style.css.php'; ?>";
+ let shortcut_icon = "<?php echo 'media/' . $cfg['style'] . '/favicon.ico'; ?>";
+ document.getElementById('stylesheet').href = steel_sheet;
+ document.getElementById('shortcut_icon').href = steel_sheet;
+}
+
+function color_scheme_preferences() {
+
+ let dark_mode_steel_sheet = "<?php echo 'media/' . $cfg['dark_style'] . '/style.css.php'; ?>"
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ set_dark_mode();
+ } else {
+ set_light_mode();
+ }
+
+ // When user change its preference
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', lightMode => {
+ lightMode.matches ? set_dark_mode() : set_light_mode();
+ });
+}
+
+// @license-end