|
|
import { getRequestHeaders } from '../script.js'; |
|
|
import { VIDEO_EXTENSIONS } from './constants.js'; |
|
|
import { t } from './i18n.js'; |
|
|
import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js'; |
|
|
import { renderTemplateAsync } from './templates.js'; |
|
|
import { humanFileSize, timestampToMoment } from './utils.js'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DataMaidDialog { |
|
|
constructor() { |
|
|
this.token = null; |
|
|
this.container = null; |
|
|
this.isScanning = false; |
|
|
|
|
|
this.DATA_MAID_CATEGORIES = { |
|
|
files: { |
|
|
name: t`Files`, |
|
|
description: t`Files that are not associated with chat messages or Data Bank. WILL DELETE MANUAL UPLOADS!`, |
|
|
}, |
|
|
images: { |
|
|
name: t`Images`, |
|
|
description: t`Images that are not associated with chat messages. WILL DELETE MANUAL UPLOADS!`, |
|
|
}, |
|
|
chats: { |
|
|
name: t`Chats`, |
|
|
description: t`Chat files associated with deleted characters.`, |
|
|
}, |
|
|
groupChats: { |
|
|
name: t`Group Chats`, |
|
|
description: t`Chat files associated with deleted groups.`, |
|
|
}, |
|
|
avatarThumbnails: { |
|
|
name: t`Avatar Thumbnails`, |
|
|
description: t`Thumbnails for avatars of missing or deleted characters.`, |
|
|
}, |
|
|
backgroundThumbnails: { |
|
|
name: t`Background Thumbnails`, |
|
|
description: t`Thumbnails for missing or deleted backgrounds.`, |
|
|
}, |
|
|
personaThumbnails: { |
|
|
name: t`Persona Thumbnails`, |
|
|
description: t`Thumbnails for missing or deleted personas.`, |
|
|
}, |
|
|
chatBackups: { |
|
|
name: t`Chat Backups`, |
|
|
description: t`Automatically generated chat backups.`, |
|
|
}, |
|
|
settingsBackups: { |
|
|
name: t`Settings Backups`, |
|
|
description: t`Automatically generated settings backups.`, |
|
|
}, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getReport() { |
|
|
const response = await fetch('/api/data-maid/report', { |
|
|
method: 'POST', |
|
|
headers: getRequestHeaders(), |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`Error fetching Data Maid report: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
return await response.json(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async finalize() { |
|
|
const response = await fetch('/api/data-maid/finalize', { |
|
|
method: 'POST', |
|
|
headers: getRequestHeaders(), |
|
|
body: JSON.stringify({ token: this.token }), |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`Error finalizing Data Maid: ${response.statusText}`); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async setupDialogUI() { |
|
|
const template = await renderTemplateAsync('dataMaidDialog'); |
|
|
this.container = document.createElement('div'); |
|
|
this.container.classList.add('dataMaidDialogContainer'); |
|
|
this.container.innerHTML = template; |
|
|
|
|
|
const startButton = this.container.querySelector('.dataMaidStartButton'); |
|
|
startButton.addEventListener('click', () => this.handleScanClick()); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async handleScanClick() { |
|
|
if (this.isScanning) { |
|
|
toastr.warning(t`The scan is already running. Please wait for it to finish.`); |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const resultsList = this.container.querySelector('.dataMaidResultsList'); |
|
|
resultsList.innerHTML = ''; |
|
|
this.showSpinner(); |
|
|
this.isScanning = true; |
|
|
|
|
|
const report = await this.getReport(); |
|
|
|
|
|
this.hideSpinner(); |
|
|
await this.renderReport(report, resultsList); |
|
|
this.token = report.token; |
|
|
} catch (error) { |
|
|
this.hideSpinner(); |
|
|
toastr.error(t`An error has occurred. Check the console for details.`); |
|
|
console.error('Error generating Data Maid report:', error); |
|
|
} finally { |
|
|
this.isScanning = false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showSpinner() { |
|
|
const spinner = this.container.querySelector('.dataMaidSpinner'); |
|
|
const placeholder = this.container.querySelector('.dataMaidPlaceholder'); |
|
|
placeholder.classList.add('displayNone'); |
|
|
spinner.classList.remove('displayNone'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
hideSpinner() { |
|
|
const spinner = this.container.querySelector('.dataMaidSpinner'); |
|
|
spinner.classList.add('displayNone'); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async renderReport(report, resultsList) { |
|
|
for (const [prop, data] of Object.entries(this.DATA_MAID_CATEGORIES)) { |
|
|
const category = await this.renderCategory(prop, data.name, data.description, report.report[prop]); |
|
|
if (!category) { |
|
|
continue; |
|
|
} |
|
|
resultsList.appendChild(category); |
|
|
} |
|
|
this.displayEmptyPlaceholder(); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
displayEmptyPlaceholder() { |
|
|
const resultsList = this.container.querySelector('.dataMaidResultsList'); |
|
|
if (resultsList.children.length === 0) { |
|
|
const placeholder = this.container.querySelector('.dataMaidPlaceholder'); |
|
|
placeholder.classList.remove('displayNone'); |
|
|
placeholder.textContent = t`No items found to clean up. Come back later!`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async renderCategory(prop, name, description, items) { |
|
|
if (!Array.isArray(items) || items.length === 0) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
const viewModel = { |
|
|
name: name, |
|
|
description: description, |
|
|
totalSize: humanFileSize(items.reduce((sum, item) => sum + item.size, 0)), |
|
|
totalItems: items.length, |
|
|
items: items.sort((a, b) => b.mtime - a.mtime).map(item => ({ |
|
|
...item, |
|
|
size: humanFileSize(item.size), |
|
|
date: timestampToMoment(item.mtime).format('L LT'), |
|
|
})), |
|
|
}; |
|
|
|
|
|
const template = await renderTemplateAsync('dataMaidCategory', viewModel); |
|
|
const categoryElement = document.createElement('div'); |
|
|
categoryElement.innerHTML = template; |
|
|
categoryElement.querySelectorAll('.dataMaidItemView').forEach(button => { |
|
|
button.addEventListener('click', async () => { |
|
|
const item = button.closest('.dataMaidItem'); |
|
|
const hash = item?.getAttribute('data-hash'); |
|
|
const itemName = items.find(i => i.hash === hash)?.name; |
|
|
if (hash) { |
|
|
await this.view(prop, hash, itemName); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
categoryElement.querySelectorAll('.dataMaidItemDownload').forEach(button => { |
|
|
button.addEventListener('click', async () => { |
|
|
const item = button.closest('.dataMaidItem'); |
|
|
const hash = item?.getAttribute('data-hash'); |
|
|
if (hash) { |
|
|
await this.download(items, hash); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
categoryElement.querySelectorAll('.dataMaidDeleteAll').forEach(button => { |
|
|
button.addEventListener('click', async (event) => { |
|
|
event.stopPropagation(); |
|
|
const confirm = await Popup.show.confirm(t`Are you sure?`, t`This will permanently delete all files in this category. THIS CANNOT BE UNDONE!`); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
|
|
|
const hashes = items.map(item => item.hash).filter(hash => hash); |
|
|
await this.delete(hashes); |
|
|
|
|
|
categoryElement.remove(); |
|
|
this.displayEmptyPlaceholder(); |
|
|
}); |
|
|
|
|
|
}); |
|
|
categoryElement.querySelectorAll('.dataMaidItemDelete').forEach(button => { |
|
|
button.addEventListener('click', async () => { |
|
|
const item = button.closest('.dataMaidItem'); |
|
|
const hash = item?.getAttribute('data-hash'); |
|
|
if (hash) { |
|
|
const confirm = await Popup.show.confirm(t`Are you sure?`, t`This will permanently delete the file. THIS CANNOT BE UNDONE!`); |
|
|
if (!confirm) { |
|
|
return; |
|
|
} |
|
|
if (await this.delete([hash])) { |
|
|
item.remove(); |
|
|
items.splice(items.findIndex(i => i.hash === hash), 1); |
|
|
if (items.length === 0) { |
|
|
categoryElement.remove(); |
|
|
this.displayEmptyPlaceholder(); |
|
|
} |
|
|
} |
|
|
} |
|
|
}); |
|
|
}); |
|
|
return categoryElement; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
getViewUrl(hash) { |
|
|
return `/api/data-maid/view?hash=${encodeURIComponent(hash)}&token=${encodeURIComponent(this.token)}`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async download(items, hash) { |
|
|
const item = items.find(i => i.hash === hash); |
|
|
if (!item) { |
|
|
return; |
|
|
} |
|
|
const url = this.getViewUrl(hash); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = item?.name || hash; |
|
|
document.body.appendChild(a); |
|
|
a.click(); |
|
|
document.body.removeChild(a); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async view(prop, hash, name) { |
|
|
const url = this.getViewUrl(hash); |
|
|
const isImage = ['images', 'avatarThumbnails', 'backgroundThumbnails'].includes(prop); |
|
|
const element = isImage |
|
|
? await this.getViewElement(url, name) |
|
|
: await this.getTextViewElement(url); |
|
|
await callGenericPopup(element, POPUP_TYPE.DISPLAY, '', { large: true, wide: true }); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async delete(hashes) { |
|
|
try { |
|
|
const response = await fetch('/api/data-maid/delete', { |
|
|
method: 'POST', |
|
|
headers: getRequestHeaders(), |
|
|
body: JSON.stringify({ hashes: hashes, token: this.token }), |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
throw new Error(`Error deleting item: ${response.statusText}`); |
|
|
} |
|
|
|
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error('Error deleting item:', error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getViewElement(url, name) { |
|
|
const isVideo = VIDEO_EXTENSIONS.includes(name.split('.').pop()); |
|
|
const mediaElement = document.createElement(isVideo ? 'video' : 'img'); |
|
|
if (mediaElement instanceof HTMLVideoElement) { |
|
|
mediaElement.controls = true; |
|
|
} |
|
|
mediaElement.src = url; |
|
|
mediaElement.classList.add('dataMaidImageView'); |
|
|
return mediaElement; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async getTextViewElement(url) { |
|
|
const response = await fetch(url); |
|
|
const text = await response.text(); |
|
|
const element = document.createElement('textarea'); |
|
|
element.classList.add('dataMaidTextView'); |
|
|
element.readOnly = true; |
|
|
element.textContent = text; |
|
|
return element; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async open() { |
|
|
await this.setupDialogUI(); |
|
|
await callGenericPopup(this.container, POPUP_TYPE.TEXT, '', { wide: true, large: true }); |
|
|
|
|
|
if (this.token) { |
|
|
await this.finalize(); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
export function initDataMaid() { |
|
|
const dataMaidButton = document.getElementById('data_maid_button'); |
|
|
if (!dataMaidButton) { |
|
|
console.warn('Data Maid button not found'); |
|
|
return; |
|
|
} |
|
|
|
|
|
dataMaidButton.addEventListener('click', () => new DataMaidDialog().open()); |
|
|
} |
|
|
|