empornium better filelist

Shows filelist as expandable tree structure

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name         empornium better filelist
// @version      2.3
// @description  Shows filelist as expandable tree structure
// @author       ephraim
// @namespace    empornium
// @match        https://www.empornium.is/torrents.php?id=*
// @match        https://www.empornium.me/torrents.php?id=*
// @match        https://www.empornium.sx/torrents.php?id=*
// @grant        none
// ==/UserScript==


function tree(folder) {
    var folders = [];
    var files = [];
    folder.files.forEach(f => {
        if (/\//.test(f.name)) {
            var levels = f.name.split('/');
            var currentLevel = levels.shift();
            f.name = levels.join('/');
            var existing = folders.find(fold => {
                return fold.name == currentLevel;
            });
            if (existing) {
                existing.files.push(f);
            } else {
                var newFolder = {};
                newFolder.name = currentLevel;
                newFolder.files = [f];
                folders.push(newFolder);
            }
        } else {
            files.push(f);
        }
    });
    folder.folders = folders;
    folder.files = files;
    folders.forEach(tree);
    folder.byteSize = folderSize(folder);
    return folder;
}


function folderSize(folder) {
    var fileSize = folder.files.reduce((currentSize, file) => {
        return currentSize + file.byteSize;
    }, 0);
    if (folder.folders.length) {
        return fileSize + folder.folders.reduce((currentSize, folder) => {
            return currentSize + folderSize(folder);
        }, 0);
    } else {
        return fileSize;
    }
}


function sizeInBytes(ssize) {
    ssize = ssize.replace(',', '');
    var number, unit;
    [number, unit] = ssize.split(' ');
    number = +number;
    var suffixes = {
        KiB: 1024,
        MiB: 1024 * 1024,
        GiB: 1024 * 1024 * 1024,
        TiB: 1024 * 1024 * 1024 * 1024
    };
    return number * suffixes[unit] || number;
}


function formatBytes(bytes) {
    if (bytes == 0) return '0 Bytes';
    var k = 1024;
    var sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];
    var i = Math.floor(Math.log(bytes) / Math.log(k));
    return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}


function ce(type, className) {
    var e = document.createElement(type);
    e.className = className || '';
    return e;
}


function getFileType(fileName) {
    var type;
    type = fileName.match(/\.(jpg|jpeg|png|gif|bmp)$/i);
    if (type) return `icon_files_image file_type_${type[1]}`;
    type = fileName.match(/\.(mp4|avi|m4v|mpg|mpeg|mkv|mov|wmv|flv)$/i);
    if (type) return `icon_files_video file_type_${type[1]}`;
    type = fileName.match(/\.(txt|srt)$/i);
    if (type) return `icon_files_text file_type_${type[1]}`;
    type = fileName.match(/\.(zip|rar|7z)$/i);
    if (type) return `icon_files_compressed file_type_${type[1]}`;
    type = fileName.match(/\.(iso|vob)$/i);
    if (type) return `icon_files_disc file_type_${type[1]}`;
    type = fileName.match(/\.(mp3|wav|flac|m4a|wma|aac)$/i);
    if (type) return `icon_files_audio file_type_${type[1]}`;
    type = fileName.match(/\.(exe|apk)$/i);
    if (type) return `icon_files_executable file_type_${type[1]}`;

    return 'icon_files_unknown';
}


function makeFolderDom(folder) {
    var folderElement = ce('div', 'folder');
    var folderDetails = ce('div', 'folder_details folder_closed tree_item');
    var contains = '';
    if (folder.files.length > 1) {
      contains = `${folder.files.length} files`;
    } else if (folder.files.length == 1) {
      contains = '1 file';
    } else if (!folder.files.length && !folder.folders.length) {
      contains = 'empty';
    }
    folderDetails.innerHTML = `<span class="folder_name">${folder.name}</span>
        <span class="folder_files">${contains}</span>
        <span class="folder_size">${formatBytes(folder.byteSize)}</span>`;
    folderElement.append(folderDetails);
    var container = ce('div', 'folder_container');
    folderDetails.addEventListener('click', toggleCollapsed);
    if (folder.folders.length) {
        var folderList = ce('ul', 'folder_list');
        for (var f of folder.folders) {
            var foldi = ce('li', 'folder_item');
            foldi.appendChild(makeFolderDom(f));
            folderList.append(foldi);
        }
        container.append(folderList);
    }
    if (folder.files.length) {
        var fileList = ce('ul', 'file_list');
        for (var file of folder.files) {
            var filei = ce('li', 'file_item tree_item');
            filei.innerHTML = `<div class="icon_stack">
                <i class="font_icon file_icons ${getFileType(file.name)}"></i>
                </div><span class="file_name">${file.name}</span>
                <span class="file_size">${file.size}</span>`;
            fileList.append(filei);
        }
        container.append(fileList);
    }
    folderElement.append(container);
    return folderElement;
}


function toggleCollapsed(e) {
    this.classList.toggle('folder_open');
    this.classList.toggle('folder_closed');
}


function createTree() {
    var treeContainer = ce('div', 'tree_container');
    treeContainer.append(makeFolderDom(root));
    var firstFolder = treeContainer.querySelector('.folder_closed');
    firstFolder.classList.remove('folder_closed');
    firstFolder.classList.add('folder_open');

    return treeContainer;
}

function clearFilter(e) {
    if (e.key != "Escape") return;
    this.value = '';
    filterList(e);
}

function filterList(e) {
    var container = document.querySelector('.tree_container');
    container.classList.add('hidden'); // temporary hide  when hiding children
    if (e.target.value.length < 1) {
        container.querySelectorAll('.hidden, .folder_force_open, .file_found').forEach(f => {
            f.classList.remove('hidden', 'folder_force_open', 'file_found');
        });
        container.querySelectorAll('.filter_match').forEach(m => {
            m.outerHTML = m.textContent;
        });
        container.classList.remove('hidden');
        return false;
    }

    var needle = new RegExp(this.value, 'i');
    container.querySelectorAll('.file_name').forEach(f => {
        var hit = f.textContent.match(needle);
        var fileItem = f.parentElement;
        if (hit) {
            f.innerHTML = wrapMatch(f.textContent, hit);
            fileItem.classList.remove('hidden');
            fileItem.classList.add('file_found');
        } else {
            fileItem.classList.add('hidden');
            fileItem.classList.remove('file_found');
        }
    });

    container.querySelectorAll('.folder').forEach(folder => {
        var hit = folder.textContent.match(needle);
        var found = folder.querySelector('.file_found');
        if (hit || found) {
            folder.classList.remove('hidden');
            folder.classList.add('file_found');
            if (found) {
                folder.querySelector('.folder_details').classList.add('folder_force_open');
            } else {
                folder.querySelector('.folder_details').classList.remove('folder_force_open');
            }
            if (hit) {
                var folderName = folder.querySelector('.folder_name');
                folderName.innerHTML = wrapMatch(folderName.textContent, hit);
            }
        } else {
            folder.classList.remove('file_found');
            folder.classList.add('hidden');
        }
    });

    container.querySelector('.folder').classList.remove('hidden');
    container.classList.remove('hidden');
}

function expandAllFolders(e) {
    e.preventDefault();
    var closedFolders = document.querySelectorAll('.folder_closed');
    var openFolders = [...document.querySelectorAll('.folder_open')].slice(1);
    if (this.dataset.collapsed == 'collapsed') {
        closedFolders.forEach(f => {
            f.classList.add('folder_open');
            f.classList.remove('folder_closed');
        });
        this.dataset.collapsed = 'expanded';
        this.innerText = this.innerText.replace('📁Expand', '📂Collapse');
    } else if (this.dataset.collapsed == 'expanded') {
        openFolders.forEach(f => {
            f.classList.add('folder_closed');
            f.classList.remove('folder_open');
        });
        this.dataset.collapsed = 'collapsed';
        this.innerText = this.innerText.replace('📂Collapse', '📁Expand');
    }
}

function list2Tree() {
    var tabl = fileList.querySelector('table');
    var rows = [...tabl.rows];

    root.name = rows[0].innerText.trim();
    root.files = rows.slice(2).map(r => {
        var tdata = r.querySelectorAll('td');
        return {
            name: tdata[0].innerText.trim(),
            size: tdata[1].innerText.trim(),
            byteSize: sizeInBytes(tdata[1].innerText.trim())
        };
    });

    root = tree(root);
    tabl.style.display = 'none';
    var header = ce('div', 'tree_header colhead');
    var headerName = ce('span', 'header_name sort_ascending header_item');
    headerName.innerText = 'Name';
    headerName.addEventListener('click', sortTree);
    var headerFiles = ce('span', 'header_files header_item');
    headerFiles.innerText = 'Files';
    headerFiles.addEventListener('click', sortTree);
    var headerSize = ce('span', 'header_size header_item');
    headerSize.innerText = 'Size';
    headerSize.addEventListener('click', sortTree);
    headerName.dataset.type = 'header_name';
    headerFiles.dataset.type = 'header_files';
    headerSize.dataset.type = 'header_size';
    var tools = ce('span', 'header_tools');
    var expand = ce('a', 'header_expand');
    var filterInput = ce('input', 'header_filter');
    expand.text = '(📁Expand all)';
    expand.href = '#';
    expand.title = 'Expand all folders';
    expand.dataset.collapsed = 'collapsed';
    filterInput.placeholder = '🔍Filter list';
    filterInput.type = 'search';
    filterInput.addEventListener('input', filterList);
    filterInput.addEventListener('keyup', clearFilter);
    expand.addEventListener('click', expandAllFolders);
    tools.append(expand, filterInput);
    var headerLeft = ce('span', 'header_left')
    var headerRight = ce('span', 'header_right')
    headerLeft.append(headerName, tools)
    headerRight.append(headerFiles, headerSize)
    header.append(headerLeft, headerRight);
    fileList.append(header);

    var treeContainer = createTree();
    fileList.append(treeContainer);
    fileList.classList.remove('hidden');
}

function sortFolderSize(folder, ascending) {
    var direction = ascending ? 1 : -1;
    folder.files.sort((a, b) => {
        return direction * (b.byteSize - a.byteSize);
    });
    folder.folders.sort((a, b) => {
        return direction * (b.byteSize - a.byteSize);
    });
    folder.folders.forEach(f => {
        sortFolderSize(f, ascending);
    });
}

function sortFolderFiles(folder, ascending) {
    var direction = ascending ? 1 : -1;
    folder.folders.sort((a, b) => {
        return direction * (b.files.length - a.files.length);
    });
    folder.folders.forEach(f => {
        sortFolderFiles(f, ascending);
    });
}

function sortFolderName(folder, ascending) {
    var direction = ascending ? -1 : 1;
    folder.files.sort((a, b) => {
        return direction * (a.name.localeCompare(b.name));
    });
    folder.folders.sort((a, b) => {
        return direction * (a.name.localeCompare(b.name));
    });
    folder.folders.forEach(f => {
        sortFolderName(f, ascending);
    });
}


function sortTree() {
    var isAscending = this.classList.contains('sort_ascending');
    if (isAscending) {
        this.classList.add('sort_descending');
        this.classList.remove('sort_ascending');
    } else {
        this.classList.add('sort_ascending');
        this.classList.remove('sort_descending');
    }    

    var others = this.parentElement.querySelectorAll(`.header_item:not(.${this.dataset.type})`)
    for (var other of others) {
        other.classList.remove('sort_ascending');
        other.classList.remove('sort_descending');
    }

    document.querySelector('.tree_container').remove();

    if (this.classList.contains('header_name')) {
        sortFolderName(root, isAscending);
    } else if (this.classList.contains('header_files')) {
        sortFolderFiles(root, isAscending);
    } else if (this.classList.contains('header_size')) {
        sortFolderSize(root, isAscending);
    }


    var treeContainer = createTree();
    fileList.append(treeContainer);
}

function wrapMatch(text, match) {
    var matchElement = ce('span', 'filter_match');
    matchElement.textContent = match[0];
    return text.replaceAll(match, matchElement.outerHTML);
}


var fileList = document.querySelector('div[id^="files_"]');
var fileListToggle = document.querySelector('a[onclick^="show_files"]');
fileListToggle.text = '(Show file tree)';
var root = {};
fileListToggle.onclick = function toggleTree() {
    if (this.classList.contains('open_tree')) {
        this.text = '(Show file tree)';
    } else {
        this.text = '(Hide file tree)';
    }
    this.classList.toggle('open_tree');
    fileList.classList.toggle('hidden');
    if (!document.querySelector('.tree_container')) {
        list2Tree();
    }
    return false;
};

var oldListItemOdd = fileList.querySelector('.rowa');
var oldStyleOdd = getComputedStyle(oldListItemOdd);
var treeStyle = ce('style');
document.head.append(treeStyle);
document.head.append(treeStyle);
treeStyle.innerHTML = `
.tree_container * {
    margin: 0;
}
.tree_container {
    max-height: 600px;
    overflow-y: scroll;
    resize: vertical;
    contain: content;
}
.folder_container {
    margin-left: 1.5em;
    border-left: dashed thin #8FC5E0;
}
.tree_header {
    display: flex;
    padding: 0.5em 2em 0.3em 2em;
    justify-content: space-between;
    align-items: baseline;
}
.sort_ascending:after {
    content: '🡩';
    margin-left: 0.3em;
    font-size: 10pt;
}
.sort_descending:after {
    content: '🡫';
    margin-left: 0.3em;
    font-size: 10pt;
}
.header_item {
    cursor: pointer;
}
.header_left {
    display: flex;
    justify-content: start;
    gap: 150px;
    flex-grow: 4;
}
.header_right {
    display: flex;
    justify-content: end;
    gap: 3.5em;
    flex-grow: 1;
}
.header_tools {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    width: 400px;
}
.header_expand {
    margin-right: 1em;
    font-weight: normal;
    font-size: 10pt;
}
.header_filter {
    border: none;
    border-radius: 5px;
    background: #29374F;
    color: #bcd;
    width: 20em;
    padding: 4px;
}
.file_list {
    padding-left: 0.5em;
}
.folder_list {
    margin-bottom: 10px;
}
.folder li {
    list-style-type: none;
}
.file_item:nth-child(odd) {
    background-color: ${oldStyleOdd.backgroundColor};
}
.folder_details  {
    display: flex;
    flex-direction: row;
    align-items: center;
    padding: 2px 0 2px 5px;
    margin-left: 0.5em;
    cursor: pointer;
}
.folder_open:before {
    content: '◢​📂';
    font-size: 12pt;
}
.folder_closed:before {
    content: '▷​📁';
    font-size: 12pt;
}
.folder_closed + div {
    display: none;
}
.folder_force_open + div {
    display:block;
}
.folder_details:before {
    margin-right: 0.3em;
  }
.folder_item:nth-child(odd) .folder_details {
    background-color: ${oldStyleOdd.backgroundColor};
}
.folder_name {
    flex: 1;
}
.folder_files {
    font-size: 9pt;
    min-width: 7em;
    text-align: end;
}
.folder_size {
    padding-right: 1em;
    font-size: 9pt;
    min-width: 7em;
    text-align: end;
}
.file_item {
    display: flex;
    align-items: center;
    font-size: 8pt;
    padding: 3px;
    cursor: default;
}
.file_name {
    flex: 1;
    margin-left: 0.5em;
}
.file_size {
    padding-right: 1em;
}
.filter_match {
    font-weight: bold;
    background-color: yellow;
}
.tree_item:hover {
    transform: scale(1.002);
    box-shadow: 2px 1px 8px #0006;
}
.file_item .font_icon {
    font-size: 10pt;
}
.file_item .icon_files_compressed {
    color:#F5C438;
    -webkit-text-stroke: 0.5px black;
}
.file_item .icon_files_executable {
    color:#f318bc;
}
.file_type_jpg, .file_type_jpeg {
    color:#a88526;
}
.file_type_mp4, .file_type_m4v {
    color:#7406a1;
}
.file_type_avi, .file_type_gif {
    color:#026102;
}
.file_type_mpg, .file_type_mpeg, .file_type_png {
    color:#740000;
}
.file_type_mkv, .file_type_mov, .file_type_bmp {
    color:#003cac;
}
.file_type_wmv {
    color:#694d00;
}
`;