Spaces:
Paused
Paused
| import { getRequestHeaders } from '../script.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'; | |
| /** | |
| * @typedef {object} DataMaidReportResult | |
| * @property {import('../../src/endpoints/data-maid.js').DataMaidSanitizedReport} report - The sanitized report of the Data Maid. | |
| * @property {string} token - The token to use for the Data Maid report. | |
| */ | |
| /** | |
| * Data Maid Dialog class for managing the cleanup dialog interface. | |
| */ | |
| 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.`, | |
| }, | |
| chatBackups: { | |
| name: t`Chat Backups`, | |
| description: t`Automatically generated chat backups.`, | |
| }, | |
| settingsBackups: { | |
| name: t`Settings Backups`, | |
| description: t`Automatically generated settings backups.`, | |
| }, | |
| }; | |
| } | |
| /** | |
| * Returns a promise that resolves to the Data Maid report. | |
| * @returns {Promise<DataMaidReportResult>} | |
| * @private | |
| */ | |
| 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(); | |
| } | |
| /** | |
| * Finalizes the Data Maid process by sending a request to the server. | |
| * @returns {Promise<void>} | |
| * @private | |
| */ | |
| 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}`); | |
| } | |
| } | |
| /** | |
| * Sets up the dialog UI elements and event listeners. | |
| * @private | |
| */ | |
| 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()); | |
| } | |
| /** | |
| * Handles the scan button click event. | |
| * @private | |
| */ | |
| 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; | |
| } | |
| } | |
| /** | |
| * Shows the loading spinner and hides the placeholder. | |
| * @private | |
| */ | |
| showSpinner() { | |
| const spinner = this.container.querySelector('.dataMaidSpinner'); | |
| const placeholder = this.container.querySelector('.dataMaidPlaceholder'); | |
| placeholder.classList.add('displayNone'); | |
| spinner.classList.remove('displayNone'); | |
| } | |
| /** | |
| * Hides the loading spinner. | |
| * @private | |
| */ | |
| hideSpinner() { | |
| const spinner = this.container.querySelector('.dataMaidSpinner'); | |
| spinner.classList.add('displayNone'); | |
| } | |
| /** | |
| * Renders the Data Maid report into the results list. | |
| * @param {DataMaidReportResult} report | |
| * @param {Element} resultsList | |
| * @private | |
| */ | |
| 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(); | |
| } | |
| /** | |
| * Displays a placeholder message if no items are found in the results list. | |
| * @private | |
| */ | |
| 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!`; | |
| } | |
| } | |
| /** | |
| * Renders a single Data Maid category into a DOM element. | |
| * @param {string} prop Property name for the category | |
| * @param {string} name Name of the category | |
| * @param {string} description Description of the category | |
| * @param {import('../../src/endpoints/data-maid.js').DataMaidSanitizedRecord[]} items List of items in the category | |
| * @return {Promise<Element|null>} A promise that resolves to a DOM element containing the rendered category | |
| * @private | |
| */ | |
| 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'); | |
| if (hash) { | |
| await this.view(prop, hash); | |
| } | |
| }); | |
| }); | |
| 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; | |
| } | |
| /** | |
| * Constructs the URL for viewing an item by its hash. | |
| * @param {string} hash Hash of the item to view | |
| * @returns {string} URL to view the item | |
| * @private | |
| */ | |
| getViewUrl(hash) { | |
| return `/api/data-maid/view?hash=${encodeURIComponent(hash)}&token=${encodeURIComponent(this.token)}`; | |
| } | |
| /** | |
| * Downloads an item by its hash. | |
| * @param {import('../../src/endpoints/data-maid.js').DataMaidSanitizedRecord[]} items List of items in the category | |
| * @param {string} hash Hash of the item to download | |
| * @private | |
| */ | |
| 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); | |
| } | |
| /** | |
| * Opens the item view for a specific hash. | |
| * @param {string} prop Property name for the category | |
| * @param {string} hash Item hash to view | |
| * @private | |
| */ | |
| async view(prop, hash) { | |
| const url = this.getViewUrl(hash); | |
| const isImage = ['images', 'avatarThumbnails', 'backgroundThumbnails'].includes(prop); | |
| const element = isImage | |
| ? await this.getViewElement(url) | |
| : await this.getTextViewElement(url); | |
| await callGenericPopup(element, POPUP_TYPE.DISPLAY, '', { large: true, wide: true }); | |
| } | |
| /** | |
| * Deletes an item by its file path hash. | |
| * @param {string[]} hashes Hashes of items to delete | |
| * @return {Promise<boolean>} True if the deletion was successful, false otherwise | |
| * @private | |
| */ | |
| 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; | |
| } | |
| } | |
| /** | |
| * Gets an image element for viewing images. | |
| * @param {string} url View URL | |
| * @returns {Promise<HTMLElement>} Image element | |
| * @private | |
| */ | |
| async getViewElement(url) { | |
| const img = document.createElement('img'); | |
| img.src = url; | |
| img.classList.add('dataMaidImageView'); | |
| return img; | |
| } | |
| /** | |
| * Gets an iframe element for viewing text content. | |
| * @param {string} url View URL | |
| * @returns {Promise<HTMLTextAreaElement>} Frame element | |
| * @private | |
| */ | |
| 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; | |
| } | |
| /** | |
| * Opens the Data Maid dialog and handles the interaction. | |
| */ | |
| 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()); | |
| } | |