"use strict";
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Returns a display string given the date & time specified in dateString.
// Example format: 2023-04-30T14:20:10
function getDateTimeDisplayString(dateString) {
if (!dateString || dateString.length == 0) {
return '';
}
const date = new Date(dateString);
return date.toLocaleString('default', { year: 'numeric', day: 'numeric', month: 'short' });
}
// Returns a string representation of the size sizeInBytes.
function getSizeDisplayString(sizeInBytes) {
if (!sizeInBytes || sizeInBytes == 0) {
return '0 B';
}
if (sizeInBytes < 1024) {
return sizeInBytes.toFixed() + ' B';
}
if (sizeInBytes < (1024 * 1024)) {
return (sizeInBytes / 1024).toFixed(1) + ' KB';
}
if (sizeInBytes < (1024 * 1024 * 1024)) {
return (sizeInBytes / 1024 / 1024).toFixed(1) + ' MB';
}
return (sizeInBytes / 1024 / 1024 / 1024).toFixed(1) + ' GB';
}
// A set of common audio file extensions.
const AUDIO_FORMATS = new Set(['AAC', 'AIFF', 'ALAC', 'DSD', 'FLAC', 'MP3', 'OGG', 'WAV']);
// A set of common image file extensions.
const IMAGE_FORMATS = new Set(['BMP', 'GIF', 'JPEG', 'JPG', 'PNG', 'TIF', 'TIFF']);
// A set of common video file extensions.
const VIDEO_FORMATS = new Set([
'AVCHD', 'AVI', 'FLV', 'M4P', 'M4V', 'MOV', 'MP2', 'MP4', 'MPE', 'MPEG',
'MPG', 'MPV', 'OGG', 'QT', 'SWF', 'WEBM', 'WMV'
]);
// Returns an icon (as a single emoji item) based on the given `filename`'s
// extension.
function iconForFilename(filename) {
let extension = filename.split('.').pop();
if (extension) {
extension = extension.toUpperCase();
}
if (!extension) {
return '📄';
}
if (extension == 'PDF') {
return '📋';
}
if (AUDIO_FORMATS.has(extension)) {
return '🎶';
}
if (IMAGE_FORMATS.has(extension)) {
return '📷';
}
if (VIDEO_FORMATS.has(extension)) {
return '📹';
}
return '📄';
}
// Returns a sorted list of the given `items` based on the value of `sorting`.
function sortItems(items, sorting) {
const sortedItems = items;
// return items.toSorted((a: Item, b: Item) => {
sortedItems.sort((a, b) => {
switch (sorting) {
case 'nameAsc':
return a.name.localeCompare(b.name);
case 'nameDesc':
return b.name.localeCompare(a.name);
case 'sizeAsc':
if (!a.size) {
return -1;
}
if (!b.size) {
return 1;
}
if (a.size < b.size) {
return -1;
}
else if (a.size == b.size) {
return 0;
}
return 1;
case 'sizeDesc':
if (!b.size) {
return -1;
}
if (!a.size) {
return 1;
}
if (b.size < a.size) {
return -1;
}
else if (a.size == b.size) {
return 0;
}
return 1;
case 'accessedAsc':
if (!b.accessed) {
return -1;
}
if (!a.accessed) {
return 1;
}
return b.accessed.localeCompare(a.accessed);
case 'accessedDesc':
if (!a.accessed) {
return -1;
}
if (!b.accessed) {
return 1;
}
return a.accessed.localeCompare(b.accessed);
}
return 0;
});
return sortedItems;
}
let collapsedDirectoryPaths = new Set();
// Updates the expanded/collapsed state of directory contents and updates
// directory icons to be in the correct open/closed state.
function refreshExpandedState() {
const contents = document.getElementById('contents');
for (const row of contents.querySelectorAll('.item_row')) {
if (row.hasAttribute('path')) {
const rowPath = row.getAttribute('path');
if (row.classList.contains('directory')) {
const itemIcon = row.querySelector('.item_icon');
if (collapsedDirectoryPaths.has(rowPath)) {
itemIcon.innerText = '📁';
}
else {
itemIcon.innerText = '📂';
}
}
let collapsed = false;
for (const collapsedPath of collapsedDirectoryPaths) {
if (rowPath.startsWith(collapsedPath + '/')) {
collapsed = true;
break;
}
}
if (collapsed) {
row.style.display = 'none';
}
else {
row.style.display = 'flex';
}
}
}
}
// Creates row items for `root` and all children, recursively.
function createEntryRowForRoot(root, level = 0, parentPath = '') {
const path = parentPath + '/' + root.name;
let currentRootIncludesThisRow = true;
if (window.location.hash) {
const rootPath = decodeURIComponent(window.location.hash.substring(1));
currentRootIncludesThisRow = path.indexOf(rootPath) == 0;
}
let nextLevel = level;
if (currentRootIncludesThisRow &&
// No search terms or this item matches the search terms.
(!searchTerms ||
root.name.toUpperCase().indexOf(searchTerms.toUpperCase()) >= 0)) {
nextLevel = nextLevel + 1;
const itemRow = document.createElement('div');
itemRow.setAttribute('path', path);
if (root.contents) {
itemRow.classList.add('directory');
}
itemRow.classList.add('item_row');
const itemInset = document.createElement('span');
itemInset.classList.add('item_spacing');
itemInset.style.width = (25 * level) + 'px';
itemRow.appendChild(itemInset);
const itemIcon = document.createElement('span');
itemIcon.classList.add('item_icon');
if (!root.contents) {
itemIcon.innerText = iconForFilename(root.name);
}
itemRow.appendChild(itemIcon);
const itemName = document.createElement('span');
itemName.classList.add('item_name');
let backupIcon = '<span class="backed_up_cloud">☁️</span>';
if (root.excludedFromBackups) {
backupIcon = '';
}
let makeDirRootLink = '';
if (root.contents && level > 0) {
makeDirRootLink =
'<a class="arrow-up" href="#' + encodeURIComponent(path) + '">⬆️</a>';
}
itemName.innerHTML = '' + root.name + backupIcon + makeDirRootLink;
itemRow.appendChild(itemName);
const itemSize = document.createElement('span');
itemSize.classList.add('item_size');
itemSize.innerText = getSizeDisplayString(root.size);
itemRow.appendChild(itemSize);
const itemAccessed = document.createElement('span');
itemAccessed.classList.add('item_accessed');
itemAccessed.innerText = getDateTimeDisplayString(root.accessed);
itemRow.appendChild(itemAccessed);
const itemCreated = document.createElement('span');
itemCreated.classList.add('item_created');
itemCreated.innerText = getDateTimeDisplayString(root.created);
itemRow.appendChild(itemCreated);
const itemModified = document.createElement('span');
itemModified.classList.add('item_modified');
itemModified.innerText = getDateTimeDisplayString(root.modified);
itemRow.appendChild(itemModified);
if (parentPath.split('/').length % 2 == 1) {
itemName.classList.add('grey_bg');
itemSize.classList.add('grey_bg');
itemAccessed.classList.add('grey_bg');
itemCreated.classList.add('grey_bg');
itemModified.classList.add('grey_bg');
}
const contents = document.getElementById('contents');
contents.appendChild(itemRow);
if (root.contents) {
itemRow.addEventListener('click', function (event) {
if (!event.target || !(event.target instanceof Element) ||
event.target.classList.contains('arrow-up')) {
// Don't change expansion state on arrow click.
return;
}
if (collapsedDirectoryPaths.has(path)) {
// Expand previously collapsed directory.
collapsedDirectoryPaths.delete(path);
}
else {
// Collapse previously expanded directory.
collapsedDirectoryPaths.add(path);
}
refreshExpandedState();
});
}
}
if (root.contents) {
let sorting = 'nameAsc';
const sortDropdown = document.getElementById('sorting');
if (sortDropdown && sortDropdown instanceof HTMLSelectElement) {
sorting = sortDropdown.value;
}
const sortedItems = sortItems(root.contents, sorting);
for (const item of sortedItems) {
createEntryRowForRoot(item, nextLevel, path);
}
}
}
let allStatistics = null;
let searchTerms = null;
let rootPath = null;
// Reloads the displayed items, taking into account collapsed directories,
// `searchTerms`, and the chosen sorting.
function reloadStatistics() {
const contents = document.getElementById('contents');
for (const row of contents.querySelectorAll('div:not(.header_row)')) {
contents.removeChild(row);
}
if (window.location.hash) {
rootPath = decodeURIComponent(window.location.hash.substring(1));
document.getElementById('root_path').innerText = rootPath;
let one_up_location = '';
if (rootPath.includes('/')) {
one_up_location =
encodeURIComponent(rootPath.substring(0, rootPath.lastIndexOf('/')));
}
document.getElementById('nav_up').setAttribute('onclick', 'window.location.hash=\'#' + one_up_location + '\'');
}
else {
document.getElementById('root_path').innerText = '/';
}
if (!allStatistics) {
return;
}
createEntryRowForRoot(allStatistics);
refreshExpandedState();
}
// Recursively marks all directories in items as collapsed
function collapseDirectories(items, parentPath = '') {
if (!items || items.length == 0) {
return;
}
for (const item of items) {
const path = parentPath + '/' + item.name;
if (item.contents) {
let currentRootIncludesThisItemAsChild = true;
if (window.location.hash) {
const rootPath = decodeURIComponent(window.location.hash.substring(1));
if (path == rootPath) {
// Don't collapse the top level item.
currentRootIncludesThisItemAsChild = false;
}
else {
currentRootIncludesThisItemAsChild = path.indexOf(rootPath) == 0;
}
}
if (currentRootIncludesThisItemAsChild) {
collapsedDirectoryPaths.add(path);
}
collapseDirectories(item.contents, path);
}
}
}
// Marks every directory as collapsed and refreshes the UI.
function collapseAllDirectories() {
if (!allStatistics) {
return;
}
collapsedDirectoryPaths.clear();
collapseDirectories([allStatistics]);
refreshExpandedState();
}
// Marks every directory as expanded and refreshes the UI.
function expandAllDirectories() {
collapsedDirectoryPaths.clear();
refreshExpandedState();
}
// Triggered when the user chose a data file. Reads the file contents and loads
// the contents.
function fileSelected(file) {
// Clear file selection listeners
const reportSelector = document.getElementById('report_file_input');
reportSelector.removeEventListener('change', fileInputValueChanged);
const dropArea = document.getElementById('drop_target');
dropArea.removeEventListener('dragover', dragoverEvent);
dropArea.removeEventListener('drop', dropEvent);
document.getElementById('report_upload').hidden = true;
document.getElementById('loading').hidden = false;
document.getElementById('local_file').innerText = file.name;
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
const statistics = JSON.parse(fileReader.result);
document.getElementById('loading').hidden = true;
document.getElementById('viewer').hidden = false;
allStatistics = statistics;
reloadStatistics();
});
fileReader.readAsText(file);
}
function fileInputValueChanged(event) {
if (!event.target || !(event.target instanceof HTMLInputElement)) {
return;
}
const fileList = event.target.files;
if (fileList && fileList.length > 0) {
fileSelected(fileList[0]);
}
}
function dragoverEvent(event) {
event.stopPropagation();
event.preventDefault();
if (!event.dataTransfer) {
return;
}
// Style the drag-and-drop as a "copy file" operation.
event.dataTransfer.dropEffect = 'copy';
}
function dropEvent(event) {
event.stopPropagation();
event.preventDefault();
if (!event.dataTransfer) {
return;
}
const fileList = event.dataTransfer.files;
if (fileList && fileList.length > 0) {
fileSelected(fileList[0]);
}
}
function searchBarTextChanged(event) {
if (!event.target || !(event.target instanceof HTMLInputElement)) {
return;
}
searchTerms = event.target.value;
reloadStatistics();
}
document.addEventListener('DOMContentLoaded', function () {
const reportSelector = document.getElementById('report_file_input');
reportSelector.addEventListener('change', fileInputValueChanged);
const dropArea = document.getElementById('drop_target');
dropArea.addEventListener('dragover', dragoverEvent);
dropArea.addEventListener('drop', dropEvent);
const searchbar = document.getElementById('searchbar');
searchbar.addEventListener('input', searchBarTextChanged);
window.addEventListener('hashchange', reloadStatistics);
const sortDropdown = document.getElementById('sorting');
sortDropdown.addEventListener('change', reloadStatistics);
});