chromium/ios/tools/documents_statistics_viewer/tsc/viewer.js

"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);
});