| // 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: string|undefined): string { |
| 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: number|undefined): string { |
| 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: string): string { |
| 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 '📄'; |
| } |
| |
| declare interface Item { |
| name: string; |
| size?: number; |
| |
| accessed?: string; |
| created?: string; |
| modified?: string; |
| |
| excludedFromBackups?: boolean; |
| |
| contents?: Item[]; |
| } |
| |
| // Returns a sorted list of the given `items` based on the value of `sorting`. |
| function sortItems(items: Item[], sorting: string): Item[] { |
| const sortedItems = items; |
| // return items.toSorted((a: Item, b: Item) => { |
| sortedItems.sort((a: Item, b: Item) => { |
| 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; |
| } |
| |
| const collapsedDirectoryPaths: Set<string> = new Set(); |
| |
| // Updates the expanded/collapsed state of directory contents and updates |
| // directory icons to be in the correct open/closed state. |
| function refreshExpandedState(): void { |
| 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<HTMLElement>('.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 as HTMLElement).style.display = 'none'; |
| } else { |
| (row as HTMLElement).style.display = 'flex'; |
| } |
| } |
| } |
| } |
| |
| // Creates row items for `root` and all children, recursively. |
| function createEntryRowForRoot(root: Item, level = 0, parentPath = ''): void { |
| 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: Item|null = null; |
| let searchTerms: string|null = null; |
| let rootPath: string|null = null; |
| // Reloads the displayed items, taking into account collapsed directories, |
| // `searchTerms`, and the chosen sorting. |
| function reloadStatistics(): void { |
| 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: Item[], parentPath = ''): void { |
| 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. |
| // Suppress ESLint error because the function is actually used in the HTML file. |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars |
| function collapseAllDirectories(): void { |
| if (!allStatistics) { |
| return; |
| } |
| |
| collapsedDirectoryPaths.clear(); |
| collapseDirectories([allStatistics]); |
| refreshExpandedState(); |
| } |
| |
| // Marks every directory as expanded and refreshes the UI. |
| // Suppress ESLint error because the function is actually used in the HTML file. |
| // eslint-disable-next-line @typescript-eslint/no-unused-vars |
| function expandAllDirectories(): void { |
| collapsedDirectoryPaths.clear(); |
| refreshExpandedState(); |
| } |
| |
| // Triggered when the user chose a data file. Reads the file contents and loads |
| // the contents. |
| function fileSelected(file: File): void { |
| // 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 as string); |
| document.getElementById('loading')!.hidden = true; |
| document.getElementById('viewer')!.hidden = false; |
| |
| allStatistics = statistics; |
| reloadStatistics(); |
| }); |
| fileReader.readAsText(file); |
| } |
| |
| function fileInputValueChanged(event: Event) { |
| if (!event.target || !(event.target instanceof HTMLInputElement)) { |
| return; |
| } |
| const fileList = event.target.files; |
| if (fileList && fileList.length > 0) { |
| fileSelected(fileList[0] as File); |
| } |
| } |
| |
| function dragoverEvent(event: DragEvent) { |
| 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: DragEvent) { |
| event.stopPropagation(); |
| event.preventDefault(); |
| if (!event.dataTransfer) { |
| return; |
| } |
| const fileList = event.dataTransfer.files; |
| if (fileList && fileList.length > 0) { |
| fileSelected(fileList[0] as File); |
| } |
| } |
| |
| function searchBarTextChanged(event: 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); |
| }); |