breadcrumbs

This commit is contained in:
Evert Prants 2023-07-30 08:42:38 +03:00
parent 0f6a5c3a9e
commit 24badd5cb8
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
2 changed files with 153 additions and 47 deletions

View File

@ -132,6 +132,41 @@ button .tooltip {
.timestamp .original { .timestamp .original {
color: gray; color: gray;
} }
#dirname {
padding: 1rem;
}
.directory#dirname {
padding: 0;
display: flex;
flex-wrap: wrap;
margin-bottom: 0.5rem;
}
.directory-link {
padding: 0.5rem 1rem 0.5rem 3rem;
background-color: #05275b;
display: flex;
position: relative;
text-decoration: none;
white-space: nowrap;
}
.directory-link:nth-child(2n+1) {
background-color: #0b3a80;
}
.directory-link::after {
content: '';
width: 0;
height: 0;
border-top: 29px solid transparent;
border-bottom: 29px solid transparent;
border-left: 29px solid #05275b;
position: absolute;
right: -29px;
top: 0;
z-index: 10;
}
.directory-link:nth-child(2n+1)::after {
border-left-color: #0b3a80;
}
@media screen and (max-width: 1200px) { @media screen and (max-width: 1200px) {
.inner { .inner {
width: 100%; width: 100%;

View File

@ -1,6 +1,7 @@
const allLinks = [...document.getElementsByTagName('a')]; const allLinks = [...document.getElementsByTagName('a')];
const bodyInner = document.querySelector('.inner'); const bodyInner = document.querySelector('.inner');
const tableElem = document.querySelector('table'); const tableElem = document.querySelector('table');
const dirnameElem = document.querySelector('#dirname');
const infoRegex = /(-thumb)?\.(jpg|nfo|srt)$/; const infoRegex = /(-thumb)?\.(jpg|nfo|srt)$/;
let infoFiles = 0; let infoFiles = 0;
let infoFilesAccounted = 0; let infoFilesAccounted = 0;
@ -31,19 +32,21 @@ function getHref(tag) {
function findByHref(href) { function findByHref(href) {
return allLinks.filter((link) => { return allLinks.filter((link) => {
return reencodeURI(getHref(link)) === reencodeURI(href) return reencodeURI(getHref(link)) === reencodeURI(href);
}); });
} }
function findWholeNumberInText(text, matchNumber) { function findWholeNumberInText(text, matchNumber) {
const numbers = text.match(/\d+/g); const numbers = text.match(/\d+/g);
return numbers return (
&& numbers.length numbers &&
&& numbers.map((x) => parseInt(x, 10)).some((number) => number === matchNumber); numbers.length &&
numbers.map((x) => parseInt(x, 10)).some((number) => number === matchNumber)
);
} }
function findByInfoFile(infoFile) { function findByInfoFile(infoFile) {
const found = ['mkv', 'mp4', 'avi'].reduce((last, current) => { const found = ['mkv', 'mp4', 'avi', 'webm'].reduce((last, current) => {
const element = findByHref(infoFile.replace(infoRegex, '.' + current))[0]; const element = findByHref(infoFile.replace(infoRegex, '.' + current))[0];
return element || last; return element || last;
}, null); }, null);
@ -53,9 +56,20 @@ function findByInfoFile(infoFile) {
const pZ = (n) => n.toString().padStart(2, '0'); const pZ = (n) => n.toString().padStart(2, '0');
function restampDateTime(datetime, includeTime = true) { function restampDateTime(datetime, includeTime = true) {
const date = new Date(datetime); const date = new Date(datetime);
const [month, day, year] = [date.getMonth() + 1, date.getDate(), date.getFullYear()]; const [month, day, year] = [
const [hour, minutes, seconds] = [date.getHours(), date.getMinutes(), date.getSeconds()]; date.getMonth() + 1,
return `${pZ(day)}/${pZ(month)}/${year}${includeTime ? ` ${pZ(hour)}:${pZ(minutes)}:${pZ(seconds)}` : ''}`; date.getDate(),
date.getFullYear(),
];
const [hour, minutes, seconds] = [
date.getHours(),
date.getMinutes(),
date.getSeconds(),
];
return `${year}/${pZ(month)}/${pZ(day)}${
includeTime ? ` ${pZ(hour)}:${pZ(minutes)}:${pZ(seconds)}` : ''
}`;
// return `${pZ(day)}/${pZ(month)}/${year}${includeTime ? ` ${pZ(hour)}:${pZ(minutes)}:${pZ(seconds)}` : ''}`;
} }
function deduplicateJointEpisode(textContent) { function deduplicateJointEpisode(textContent) {
@ -86,7 +100,12 @@ async function createXMLRequest(nfo) {
// Fill metadata boxes, generate buttons // Fill metadata boxes, generate buttons
function fillMeta( function fillMeta(
content, original, nfo, movieTitle, movieDescription, movieButtons content,
original,
nfo,
movieTitle,
movieDescription,
movieButtons
) { ) {
infoFilesAccounted += 1; infoFilesAccounted += 1;
if (!content) { if (!content) {
@ -105,10 +124,14 @@ function fillMeta(
} }
if (airedEl || premieredEl) { if (airedEl || premieredEl) {
const timestamp = original.parentElement.parentElement.querySelector('.timestamp'); const timestamp =
original.parentElement.parentElement.querySelector('.timestamp');
if (timestamp) { if (timestamp) {
const original = timestamp.innerText; const original = timestamp.innerText;
const airdate = restampDateTime((airedEl || premieredEl).textContent, false); const airdate = restampDateTime(
(airedEl || premieredEl).textContent,
false
);
const wrapper = `<span class="airdate">${airdate}</span>`; const wrapper = `<span class="airdate">${airdate}</span>`;
const originalWrapper = ` <span class="original">(${original})</span>`; const originalWrapper = ` <span class="original">(${original})</span>`;
timestamp.innerHTML = wrapper + originalWrapper; timestamp.innerHTML = wrapper + originalWrapper;
@ -117,7 +140,7 @@ function fillMeta(
const episode = content.querySelector('episode'); const episode = content.querySelector('episode');
const season = content.querySelector('season'); const season = content.querySelector('season');
let title = titleEl.textContent let title = titleEl.textContent;
if (episode && season) { if (episode && season) {
title = `(S${season.textContent} E${episode.textContent}) ` + title; title = `(S${season.textContent} E${episode.textContent}) ` + title;
original.parentElement.setAttribute('data-episode', episode.textContent); original.parentElement.setAttribute('data-episode', episode.textContent);
@ -144,10 +167,10 @@ function fillMeta(
// Add "copy direct url" button // Add "copy direct url" button
if ( if (
getHref(nfo) !== 'tvshow.nfo' getHref(nfo) !== 'tvshow.nfo' &&
&& navigator navigator &&
&& navigator.clipboard navigator.clipboard &&
&& navigator.clipboard.writeText navigator.clipboard.writeText
) { ) {
const copybutton = document.createElement('button'); const copybutton = document.createElement('button');
copybutton.innerText = 'Copy direct URL'; copybutton.innerText = 'Copy direct URL';
@ -169,7 +192,9 @@ function fillMeta(
// Replace list entry with a metadata box // Replace list entry with a metadata box
// Either create or populate // Either create or populate
function createOrImproveMovieMeta(original, thumbnail, nfo) { function createOrImproveMovieMeta(original, thumbnail, nfo) {
let imgTag = `<img class="thumbnail" src="${thumbnail ? reencodeURI(getHref(thumbnail)) : ''}"/>`; let imgTag = `<img class="thumbnail" src="${
thumbnail ? reencodeURI(getHref(thumbnail)) : ''
}"/>`;
let movieTitle = `<div class="movie-title">${original.innerText}</div>`; let movieTitle = `<div class="movie-title">${original.innerText}</div>`;
let movieDescription = `<div class="movie-description"></div>`; let movieDescription = `<div class="movie-description"></div>`;
let movieButtons = `<div class="movie-buttons"></div>`; let movieButtons = `<div class="movie-buttons"></div>`;
@ -188,7 +213,7 @@ function createOrImproveMovieMeta(original, thumbnail, nfo) {
} }
} else { } else {
original.classList.add('movie'); original.classList.add('movie');
original.innerHTML = imgTag + '<div class="meta-wrap">' + movieTitle + movieDescription + movieButtons + '</div>'; original.innerHTML = `${imgTag}<div class="meta-wrap">${movieTitle}${movieDescription}${movieButtons}</div>`;
original.parentElement.classList.add('enhanced'); original.parentElement.classList.add('enhanced');
// quick innerHTML hack to create elements here lol // quick innerHTML hack to create elements here lol
imgTag = original.querySelector('.thumbnail'); imgTag = original.querySelector('.thumbnail');
@ -200,7 +225,16 @@ function createOrImproveMovieMeta(original, thumbnail, nfo) {
// Fetch metadata from the nfo file and populate the info box // Fetch metadata from the nfo file and populate the info box
if (nfo) { if (nfo) {
createXMLRequest(reencodeURI(getHref(nfo))) createXMLRequest(reencodeURI(getHref(nfo)))
.then((content) => fillMeta(content, original, nfo, movieTitle, movieDescription, movieButtons)) .then((content) =>
fillMeta(
content,
original,
nfo,
movieTitle,
movieDescription,
movieButtons
)
)
.catch((e) => { .catch((e) => {
infoFilesAccounted += 1; infoFilesAccounted += 1;
console.error(e); console.error(e);
@ -236,20 +270,25 @@ function waitUntilInfoComplete() {
// Get episode name or name of file // Get episode name or name of file
const getCellValue = (tr, idx) => const getCellValue = (tr, idx) =>
tr.children[idx].getAttribute('data-episode') tr.children[idx].getAttribute('data-episode') ||
|| tr.children[idx].innerText tr.children[idx].innerText ||
|| tr.children[idx].textContent; tr.children[idx].textContent;
// Compare numeric or (locale-aware, numeric-aware) by string values // Compare numeric or (locale-aware, numeric-aware) by string values
const comparer = (idx, asc) => (a, b) => ((v1, v2) => const comparer = (idx, asc) => (a, b) =>
v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, 'et', { numeric: true }) ((v1, v2) =>
)(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx)); v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2)
? v1 - v2
: v1.toString().localeCompare(v2, 'et', { numeric: true }))(
getCellValue(asc ? a : b, idx),
getCellValue(asc ? b : a, idx)
);
// Table sort by column index // Table sort by column index
const sortTableValues = (index) => const sortTableValues = (index) =>
Array.from(tableElem.querySelectorAll('tr:nth-child(n+2):not(.btn-back)')) Array.from(tableElem.querySelectorAll('tr:nth-child(n+2):not(.btn-back)'))
.sort(comparer(index, this.asc = !this.asc)) .sort(comparer(index, (this.asc = !this.asc)))
.forEach((tr) => tableElem.appendChild(tr)); .forEach((tr) => tableElem.appendChild(tr));
let tvShow; let tvShow;
function accountForMetadata() { function accountForMetadata() {
@ -260,10 +299,13 @@ function accountForMetadata() {
if (url.match(infoRegex)) { if (url.match(infoRegex)) {
// Hide metadata file // Hide metadata file
link.parentElement.parentElement.style.display = 'none'; link.parentElement.parentElement.style.display = 'none';
let findOriginal let findOriginal;
// Place TV show information in the beginning of the table // Place TV show information in the beginning of the table
if (url === 'tvshow.nfo' || url === 'poster.jpg') { if (url === 'tvshow.nfo' || url === 'poster.jpg') {
if (url === 'poster.jpg' && !allLinks.find((found) => getHref(found) === 'tvshow.nfo')) { if (
url === 'poster.jpg' &&
!allLinks.find((found) => getHref(found) === 'tvshow.nfo')
) {
findOriginal = document.querySelector('a:not([href=".."])'); findOriginal = document.querySelector('a:not([href=".."])');
} else { } else {
if (tvShow) { if (tvShow) {
@ -278,25 +320,29 @@ function accountForMetadata() {
findOriginal = tvShow; findOriginal = tvShow;
} }
} }
// Give posters to seasons // Give posters to seasons
} else if (url.startsWith('season') && url.includes('poster')) { } else if (url.startsWith('season') && url.includes('poster')) {
// Find season by number // Find season by number
const sn = url.match(/season(\d+)/); const sn = url.match(/season(\d+)/);
if (sn) { if (sn) {
findOriginal = allLinks.find( findOriginal = allLinks.find(({ innerText }) => {
({ innerText }) => { return (
return innerText.endsWith('/') innerText.endsWith('/') &&
&& findWholeNumberInText(innerText, parseInt(sn[1], 10)); findWholeNumberInText(innerText, parseInt(sn[1], 10))
}); );
// Find specials folder });
// Find specials folder
} else if (url.includes('specials')) { } else if (url.includes('specials')) {
findOriginal = allLinks.find(({ innerText }) => { findOriginal = allLinks.find(({ innerText }) => {
const matchAgainst = innerText.toLowerCase(); const matchAgainst = innerText.toLowerCase();
return (matchAgainst.startsWith('specials') || matchAgainst.startsWith('extras')) return (
&& innerText.endsWith('/'); (matchAgainst.startsWith('specials') ||
matchAgainst.startsWith('extras')) &&
innerText.endsWith('/')
);
}); });
} }
// Find the video file associated with this info file // Find the video file associated with this info file
} else { } else {
findOriginal = findByInfoFile(url); findOriginal = findByInfoFile(url);
} }
@ -306,9 +352,10 @@ function accountForMetadata() {
if (url.match(/.nfo$/)) { if (url.match(/.nfo$/)) {
infoFiles += 1; infoFiles += 1;
} }
createOrImproveMovieMeta(findOriginal, createOrImproveMovieMeta(
findOriginal,
url.match(/.jpg$/) ? link : undefined, url.match(/.jpg$/) ? link : undefined,
url.match(/.nfo$/) ? link : undefined, url.match(/.nfo$/) ? link : undefined
); );
} }
} }
@ -328,11 +375,11 @@ function createToggleCheckbox(field, value, description) {
const label = document.createElement('label'); const label = document.createElement('label');
form.className = 'form ' + field; form.className = 'form ' + field;
checkbox.type = 'checkbox'; checkbox.type = 'checkbox';
checkbox.id = field checkbox.id = field;
label.innerText = description; label.innerText = description;
label.setAttribute('for', field); label.setAttribute('for', field);
form.appendChild(checkbox); form.appendChild(checkbox);
form.appendChild(label) form.appendChild(label);
bodyInner.appendChild(form); bodyInner.appendChild(form);
checkbox.addEventListener('change', (e) => { checkbox.addEventListener('change', (e) => {
@ -346,18 +393,42 @@ function createToggleCheckbox(field, value, description) {
checkbox.checked = value === 'true'; checkbox.checked = value === 'true';
} }
function createDirTree() {
dirnameElem.classList.add('directory');
const dirSplit = dirnameElem.innerText.split('/').slice(1).filter((item) => !!item);
dirSplit.unshift('');
dirnameElem.innerHTML = '';
let lastPath = '';
for (const dir of dirSplit) {
lastPath += dir + '/';
const atag = document.createElement('a');
atag.innerText = dir;
atag.classList.add('directory-link');
atag.href = lastPath;
dirnameElem.appendChild(atag);
}
}
if ('localStorage' in window) { if ('localStorage' in window) {
const plainMode = window.localStorage.getItem('plainMode'); const plainMode = window.localStorage.getItem('plainMode');
if (plainMode !== 'true') { if (plainMode !== 'true') {
timestampify(); timestampify();
accountForMetadata(); accountForMetadata();
} }
createToggleCheckbox('plainMode', plainMode, 'Disable metadata / display plain index listing (less bandwidth required)'); createToggleCheckbox(
'plainMode',
plainMode,
'Disable metadata / display plain index listing (less bandwidth required)'
);
} else { } else {
timestampify(); timestampify();
accountForMetadata(); accountForMetadata();
} }
document.querySelectorAll('th').forEach((th) => th.addEventListener('click', (() => { document.querySelectorAll('th').forEach((th) =>
sortTableValues(Array.from(th.parentNode.children).indexOf(th)); th.addEventListener('click', () => {
}))); sortTableValues(Array.from(th.parentNode.children).indexOf(th));
})
);
createDirTree();