(function initTitleWorkspace(root, factory) { const api = factory(); if (typeof module === 'object' && module.exports) module.exports = api; if (root) root.chimeraTitleWorkspace = api; })(typeof window !== 'undefined' ? window : globalThis, function createTitleWorkspace() { 'use strict'; const array = value => Array.isArray(value) ? value : []; const text = value => value == null || value === '' ? null : String(value); const first = (...values) => values.map(text).find(Boolean) || null; const lower = value => String(value || '').trim().toLowerCase(); const missingStates = new Set(['missing', 'unavailable', 'disconnected', 'offline', 'not_mounted']); function deviceState(item) { return first( item && item.deviceAvailabilityState, item && item.availabilityState, item && item.device && item.device.availabilityState, item && item.storageDevice && item.storageDevice.availabilityState, item && item.volume && item.volume.device && item.volume.device.availabilityState ); } function locationLabel(item, fallback) { return first( item && item.volume && item.volume.label, item && item.volume && item.volume.name, item && item.volumeLabel, item && item.device && item.device.label, item && item.storageDevice && item.storageDevice.label, item && item.console && item.console.name, item && item.consoleName, item && item.path, fallback ); } function isConfirmedInstall(install) { const state = lower(first(install && install.presenceState, install && install.state, install && install.installState)); return state === 'present' || state === 'installed' || state === 'confirmed'; } function isMissing(item) { return missingStates.has(lower(deviceState(item))); } function collectActivity(title, locations) { const entries = []; const add = (kind, value, label) => { if (!value) return; const timestamp = new Date(value); if (Number.isNaN(timestamp.getTime())) return; entries.push({ kind, label, timestamp: timestamp.toISOString() }); }; add('title', title.updatedAt || title.updated_at, 'Title metadata updated'); add('title', title.createdAt || title.created_at, 'Title first recorded'); locations.forEach(location => { add(location.kind, location.raw.lastSeenAt || location.raw.last_seen_at, `${location.label} last seen`); add(location.kind, location.raw.verifiedAt || location.raw.verified_at, `${location.label} verified`); add(location.kind, location.raw.detectedAt || location.raw.detected_at, `${location.label} detected`); }); array(title.media).forEach(item => add('media', item.importedAt || item.provenance?.importedAt, `Imported ${item.kind || 'media'}: ${item.originalName || item.name || 'asset'}`)); return entries.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); } function project(title = {}) { const localCopies = array(title.localCopies); const consoleInstalls = array(title.consoleInstalls); const packages = array(title.packages); const saves = array(title.saves); const trophies = array(title.trophies); const media = array(title.media); const locations = [ ...localCopies.map((raw, index) => ({ kind: 'local-copy', label: locationLabel(raw, `Local copy ${index + 1}`), state: deviceState(raw) || 'unknown', version: first(raw.version && raw.version.value, raw.version, raw.appVersion), format: first(raw.format, raw.copyType, raw.type), path: first(raw.path, raw.relativePath), missing: isMissing(raw), raw })), ...consoleInstalls.map((raw, index) => ({ kind: 'console-install', label: locationLabel(raw, `Console install ${index + 1}`), state: first(raw.presenceState, raw.state, raw.installState, 'unknown'), version: first(raw.version && raw.version.value, raw.version, raw.appVersion), format: first(raw.format, raw.packageType), path: first(raw.path, raw.installPath), missing: isMissing(raw), confirmed: isConfirmedInstall(raw), raw })) ]; const missingDrive = Number(title.availability && title.availability.missingDeviceCount || 0) > 0 || locations.some(location => location.missing); const installed = consoleInstalls.some(isConfirmedInstall) || Number(title.availability && title.availability.confirmedInstallCount || 0) > 0; const protectionState = lower(first(title.protection && title.protection.state, title.protection && title.protection.status)); const protectedCopy = ['protected', 'healthy', 'verified', 'complete'].includes(protectionState) || Number(title.protection && title.protection.protectedCopyCount || 0) > 0; const badges = []; if (installed) badges.push({ code: 'installed', tone: 'positive' }); if (localCopies.length) badges.push({ code: 'local-copy', tone: 'info' }); if (missingDrive) badges.push({ code: 'missing-drive', tone: 'warning' }); if (protectedCopy) badges.push({ code: 'protected', tone: 'positive' }); if ((installed || localCopies.length) && !protectedCopy) badges.push({ code: 'needs-protection', tone: 'danger' }); if (!locations.length) badges.push({ code: 'metadata-only', tone: 'neutral' }); return Object.freeze({ title, badges, locations, localCopies, consoleInstalls, packages, saves, trophies, media, missingDrive, installed, protectedCopy, activity: collectActivity(title, locations) }); } function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function tr(key, fallback) { if (typeof window === 'undefined' || typeof window.t !== 'function') return fallback; const translated = window.t(key); return translated && translated !== key ? translated : fallback; } function setCount(id, count) { const node = document.getElementById(id); if (!node) return; node.textContent = count ? String(count) : ''; node.hidden = !count; } function emptyState(key, fallback, action) { return `

${escapeHtml(tr(key, fallback))}

${action || ''}
`; } function renderLocation(location) { const kind = location.kind === 'console-install' ? tr('library.workspace.consoleInstall', 'Console install') : tr('library.workspace.localCopy', 'Local copy'); const details = [location.version && `v${location.version}`, location.format, location.path].filter(Boolean); return `
${escapeHtml(kind)}

${escapeHtml(location.label)}

${escapeHtml(location.missing ? tr('library.workspace.driveMissing', 'Drive unavailable') : location.state)} ${details.length ? `

${details.map(escapeHtml).join(' · ')}

` : ''}
`; } function renderList(id, items, emptyKey, emptyFallback, mapper, action) { const node = document.getElementById(id); if (!node) return; node.innerHTML = items.length ? items.map(mapper).join('') : emptyState(emptyKey, emptyFallback, action); } function render(title) { if (typeof document === 'undefined') return project(title); const workspace = project(title); const labels = { installed: tr('library.badge.installed', 'Installed'), 'local-copy': tr('library.badge.localCopy', 'Local copy'), 'missing-drive': tr('library.badge.missingDrive', 'Drive unavailable'), protected: tr('library.badge.protected', 'Protected'), 'needs-protection': tr('library.badge.needsProtection', 'Needs protection'), 'metadata-only': tr('library.badge.metadataOnly', 'Metadata only') }; const badges = document.getElementById('modal-derived-badges'); if (badges) badges.innerHTML = workspace.badges.map(badge => `${escapeHtml(labels[badge.code])}`).join(''); renderList('modal-copies-content', workspace.locations, 'library.workspace.noCopies', 'No local copies or confirmed console installations recorded.', renderLocation); renderList('modal-packages-content', workspace.packages, 'library.workspace.noPackages', 'No package evidence linked to this title.', item => `
${escapeHtml(first(item.name, item.label, item.packageId, 'Package'))}${escapeHtml(first(item.state, item.status, item.type, 'recorded'))}
`); window.chimeraRecipeEditor?.mount?.({ title, workspace }); renderList('modal-saves-content', workspace.saves, 'library.workspace.noSaves', 'No save evidence linked to this title.', item => `
${escapeHtml(first(item.name, item.accountLabel, 'Save'))}${escapeHtml(first(item.state, item.status, 'recorded'))}
`, ``); renderList('modal-trophies-content', workspace.trophies, 'library.workspace.noTrophies', 'No verified read-only trophy evidence imported.', item => `
${escapeHtml(first(item.name, item.label, 'Trophy evidence'))}${escapeHtml(first(item.state, item.status, 'read-only'))}
`); renderList('modal-media-content', workspace.media, 'library.workspace.noMedia', 'No media assets linked to this title.', item => `
${escapeHtml(first(item.originalName, item.name, item.label, item.kind, 'Media'))}${escapeHtml(first(item.provenance?.provider, item.state, item.status, 'available'))}${item.preferred ? ` · ${escapeHtml(tr('media.import.preferredActive', 'Preferred'))}` : ''}
`, ``); renderList('modal-activity-content', workspace.activity, 'library.workspace.noActivity', 'No timestamped evidence recorded yet.', item => `
${escapeHtml(item.label)}
`); const compatibility = document.getElementById('modal-compatibility-content'); if (compatibility) { const versions = workspace.locations.map(item => item.version).filter(Boolean); const formats = workspace.locations.map(item => item.format).filter(Boolean); compatibility.innerHTML = `
${escapeHtml(tr('library.workspace.availability', 'Availability'))}
${escapeHtml(workspace.missingDrive ? tr('library.workspace.partialAvailability', 'Partial - drive unavailable') : workspace.locations.length ? tr('library.workspace.availableEvidence', 'Evidence available') : tr('library.workspace.unknown', 'Unknown'))}
${escapeHtml(tr('library.workspace.versions', 'Observed versions'))}
${escapeHtml([...new Set(versions)].join(', ') || tr('library.workspace.unknown', 'Unknown'))}
${escapeHtml(tr('library.workspace.formats', 'Observed formats'))}
${escapeHtml([...new Set(formats)].join(', ') || tr('library.workspace.unknown', 'Unknown'))}
${escapeHtml(tr('library.workspace.protection', 'Protection'))}
${escapeHtml(workspace.protectedCopy ? tr('library.workspace.protected', 'Protected copy recorded') : tr('library.workspace.unprotected', 'No protected copy recorded'))}
`; window.chimeraCompatibilityGuidance?.load?.(compatibility, { titleId: first(title.externalTitleId, title.game_id, title.gameId), version: versions[0] || '', firmware: first(title.consoleFirmware, title.firmware), loader: first(title.loader, title.hen), format: formats[0] || '' }); } setCount('mtab-copies-count', workspace.locations.length); setCount('mtab-packages-count', workspace.packages.length); setCount('mtab-saves-count', workspace.saves.length); setCount('mtab-trophies-count', workspace.trophies.length); setCount('mtab-media-count', workspace.media.length); setCount('mtab-activity-count', workspace.activity.length); return workspace; } return Object.freeze({ project, render }); });