// ==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;
}
`;