|
(function(){ |
|
let WRAPPER = document.querySelector('#driversUpdatesDiv'); |
|
|
|
const INITIAL_SORTING_PROPERTIES = { |
|
'name': 'asc', |
|
'version': 'desc', |
|
'posted date': 'desc', |
|
'contentid': 'desc', |
|
'size': 'desc' |
|
}; |
|
let sortingProperties = {...INITIAL_SORTING_PROPERTIES}; |
|
const parseDrivers = (() => { |
|
const parseSize = (rawSize) => parseInt(rawSize) * ({ |
|
'b': 1, |
|
'k': 1000, |
|
'm': 1000 * 1000, |
|
'g': 1000 * 1000 * 1000, |
|
}[rawSize[rawSize.length - 1]] || 1); |
|
|
|
return () => WRAPPER ? [...WRAPPER.querySelectorAll('dt')].map(dt => { |
|
const [contenttype, contentid, cipherkey, sor] = dt.firstChild.getAttribute('onclick').split('(')[1].split(')')[0].split(',') |
|
.map(arg => arg.trim().slice(1, -1).toLowerCase()) |
|
|
|
const detailMap = dt.nextSibling.textContent.split('|') |
|
.map(rawDetail => rawDetail.trim().split(':').map(part => part.trim().toLowerCase())) |
|
.reduce((map, [key, rawValue]) => ({ |
|
...map, |
|
[key]: key === 'size' ? parseSize(rawValue) : rawValue |
|
}), {}); |
|
|
|
return { |
|
dt, contenttype, contentid, cipherkey, sor, |
|
name: dt.textContent, |
|
checked: false, |
|
...detailMap, |
|
sortables: [ |
|
'name', |
|
...Object.keys(detailMap), |
|
'contentid', |
|
] |
|
}; |
|
}) : []; |
|
})(); |
|
let drivers = parseDrivers(); |
|
let detailsOpen = false; |
|
|
|
let ALL_SORTABLES = drivers.flatMap(driver => driver.sortables).filter((sortable, i, arr) => arr.indexOf(sortable) === i); |
|
|
|
const [state, load] = (() => { |
|
const state = { |
|
loading: false |
|
}; |
|
return [ |
|
state, |
|
async callback => { |
|
state.loading = true; |
|
await Promise.resolve(callback()).catch(console.error); |
|
state.loading = false; |
|
} |
|
]; |
|
})(); |
|
|
|
|
|
const isNullOrUndefined = target => target === undefined || target === null; |
|
|
|
const sortDrivers = () => drivers.sort((a, b) => { |
|
let result = 0; |
|
for (const [prop, direction] of Object.entries(sortingProperties)){ |
|
const values = [a, b].map(v => v[prop]); |
|
if (values[0] === values[1]) continue; |
|
|
|
const missingValues = values.map(isNullOrUndefined); |
|
if (missingValues.some(Boolean)){ |
|
result = isNullOrUndefined(missingValues[0]) ? 1 : -1; |
|
} |
|
else{ |
|
result = values[0] === values.slice().sort(typeof values[0] === 'number' ? (a, b) => a - b : undefined)[0] ? -1 : 1; |
|
} |
|
|
|
if (direction === 'desc') result = -result; |
|
if (result) return result; |
|
} |
|
return result; |
|
}); |
|
|
|
const titleize = string => string.length ? string[0].toUpperCase() + string.slice(1) : string; |
|
|
|
|
|
const htmlToElement = html => { |
|
const template = document.createElement('template'); |
|
template.innerHTML = html.trim(); |
|
return template.content.firstChild; |
|
}; |
|
|
|
const generateTable = allSortables => ` |
|
<table style="table-layout: fixed; width: 650px; border-collapse: collapse !important;"> |
|
<thead> |
|
<tr> |
|
<th>Download</th> |
|
${allSortables.map(sortable => ` |
|
<th class="sortable" data-sortable="${sortable}"> |
|
${titleize(sortable)} |
|
<br/> |
|
${({'asc': '▲', 'desc': '▼'}[sortingProperties[sortable]] || '').repeat(Object.keys(sortingProperties).indexOf(sortable) + 1)} |
|
</th> |
|
`).join('\n')} |
|
</tr> |
|
</thead> |
|
<tbody> |
|
${drivers.map(driver => ` |
|
<tr data-contentid="${driver.contentid}"> |
|
<td style="border: 1px solid black; vertical-align: middle;"> |
|
<input type="checkbox" style="width: 100%" ${driver.checked ? 'checked' : ''}/> |
|
<button class="driver-action">${driver.url ? 'Download' : 'Generate'}</button> |
|
</td> |
|
${allSortables.map(sortable => `<td style="border: 1px solid black; word-wrap: break-word; vertical-align: middle;">${driver[sortable] || ''}</td>`).join('\n')} |
|
</tr> |
|
`).join('\n')} |
|
</tbody> |
|
</table> |
|
`; |
|
|
|
|
|
const handleClick = ({ target }) => { |
|
if (state.loading) return; |
|
|
|
|
|
const th = target.closest('th.sortable'); |
|
if (th) return handleSortingChange(th, th.dataset.sortable); |
|
|
|
const summary = target.closest('summary'); |
|
if (summary) return detailsOpen = !detailsOpen; |
|
|
|
const wrapper = target.closest('[data-contentid]') |
|
const contentid = wrapper ? wrapper.dataset.contentid : null; |
|
const driver = contentid ? drivers.find(driver => driver.contentid === contentid) : null; |
|
|
|
const anchor = target.closest('a'); |
|
if (anchor) { |
|
driver.downloaded = true; |
|
return anchor.style.textDecoration = 'line-through'; |
|
} |
|
|
|
const checkbox = target.closest('input[type="checkbox"]'); |
|
if (checkbox) return driver.checked = !driver.checked; |
|
|
|
const button = target.closest('button'); |
|
if (!button) return; |
|
return load(() => ({ |
|
'driver-action': () => { |
|
const driver = drivers.find(driver => driver.contentid === contentid); |
|
if (driver.url) downloadURLs(driver.url); |
|
else generateURLs(driver); |
|
}, |
|
'generate-selected': () => generateURLs(...drivers.filter(driver => driver.checked)), |
|
'download-selected': () => downloadURLs(...drivers.filter(driver => driver.checked && driver.url).map(driver => driver.url)), |
|
'select-all': () => { |
|
drivers.forEach(driver => driver.checked = true); |
|
renderLayout(); |
|
}, |
|
'unselect-all': () => { |
|
drivers.forEach(driver => driver.checked = false); |
|
renderLayout(); |
|
}, |
|
'invert-selection': () => { |
|
drivers.forEach(driver => driver.checked = !driver.checked); |
|
renderLayout(); |
|
}, |
|
'select-recommended': () => { |
|
const ids = new Set(calculateRecommendedIDs()); |
|
drivers.forEach(driver => driver.checked = ids.has(driver.contentid)); |
|
renderLayout(); |
|
} |
|
}[button.className] || (() => undefined))()); |
|
}; |
|
|
|
const generateControls = () => { |
|
const generatedDrivers = drivers.filter(driver => driver.url); |
|
return ` |
|
<div> |
|
<button class="generate-selected">Generate Selected URLs</button> |
|
<br/> |
|
<!--<button class="download-selected">Download Selected</button> |
|
<br/>--> |
|
<button class="select-all">Select All</button> |
|
<br/> |
|
<button class="unselect-all">Unselect All</button> |
|
<br/> |
|
<button class="invert-selection">Invert Selection</button> |
|
<br/> |
|
<button class="select-recommended">Select Recommended</button> |
|
<br/> |
|
<details${detailsOpen ? ' open="true"' : ''}> |
|
<summary>${generatedDrivers.length} URLs</summary> |
|
|
|
<ul style="text-align: left;"> |
|
${generatedDrivers.map(driver => `<li data-contentid="${driver.contentid}"><a download target="_blank" href="${driver.url}"${driver.downloaded ? ' style="text-decoration: line-through;"' : ''}>${driver.url}</a></li>`).join('\n')} |
|
</ul> |
|
</details> |
|
</div> |
|
`; |
|
}; |
|
|
|
const renderLayout = () => load(() => { |
|
const target = document.getElementById('driversUpdatesDiv'); |
|
if (!target) return false; |
|
target.innerHTML = ''; |
|
|
|
const container = htmlToElement(` |
|
<div id="driversUpdatesDiv" style="text-align: center;"> |
|
${generateControls()} |
|
${generateTable(ALL_SORTABLES)} |
|
${generateControls()} |
|
</div> |
|
`) |
|
container.addEventListener('click', handleClick); |
|
target.replaceWith(container); |
|
|
|
return true; |
|
}) |
|
|
|
const directions = [undefined, 'asc', 'desc']; |
|
|
|
const handleSortingChange = (th, sortable) => load(() => { |
|
const currentDirection = sortingProperties[sortable]; |
|
const nextDirection = directions[(directions.indexOf(currentDirection) + 1) % directions.length]; |
|
if (!nextDirection) delete sortingProperties[sortable]; |
|
else sortingProperties[sortable] = nextDirection; |
|
sortDrivers(); |
|
renderLayout(); |
|
}) |
|
|
|
const getURLFilename = url => url.split('/').slice(-1)[0]; |
|
const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); |
|
|
|
// Doesn't work on 100% of URLs when passed multiple |
|
const downloadURLs = (...urls) => load(async () => { |
|
for (const url of urls){ |
|
const anchor = document.createElement('a'); |
|
document.body.appendChild(anchor); |
|
anchor.download = getURLFilename(url); |
|
anchor.href = url; |
|
anchor.click(); |
|
await delay(1000); |
|
anchor.remove(); |
|
} |
|
}); |
|
|
|
const generateURLs = (...drivers) => load(async () => { |
|
for (const driver of drivers){ |
|
const json = await fetch(`https://support.dynabook.com/support/contentDetail?contentType=${driver.contenttype}&contentId=${driver.contentid}&cipherKey=${driver.cipherkey}&sor=${driver.sor}`).then(r => r.json()); |
|
driver.url = json.contentFile; |
|
await renderLayout(); |
|
} |
|
return renderLayout(); |
|
}) |
|
|
|
const calculateRecommendedIDs = () => Object.values(drivers.reduce((map, driver) => ({ |
|
...map, |
|
[driver.name]: [...(map[driver.name] || []), driver] |
|
}), {})).map(options => options[0].contentid); |
|
|
|
|
|
(async () => { |
|
while(true){ |
|
await delay(500); |
|
WRAPPER = document.querySelector('#driversUpdatesDiv'); |
|
if (!WRAPPER) continue; |
|
|
|
await delay(500); |
|
drivers = parseDrivers(); |
|
if (!drivers.length) continue; |
|
ALL_SORTABLES = drivers.flatMap(driver => driver.sortables).filter((sortable, i, arr) => arr.indexOf(sortable) === i); |
|
|
|
await delay(500); |
|
sortDrivers(); |
|
calculateRecommendedIDs().forEach(id => drivers.find(driver => driver.contentid === id).checked = true); |
|
if (!renderLayout()) continue; |
|
|
|
const filterDiv = document.getElementById('driversFilterOptionDiv') |
|
if (filterDiv) filterDiv.remove(); |
|
|
|
break; |
|
} |
|
})().catch(console.error); |
|
})(); |