Story 4.1 completed

This commit is contained in:
2026-05-23 23:00:07 +02:00
parent fd0a7868f3
commit de1b33c453
10 changed files with 574 additions and 25 deletions
+261
View File
@@ -0,0 +1,261 @@
/**
* GM Player Privacy Selector Menu
*
* A settings menu entry that allows the GM to select a player and view their privacy settings.
* This provides read-only access to all players' privacy panels.
*/
import { PlayerPrivacyPanel } from '../player/PlayerPrivacyPanel.js';
/**
* Static references to DI dependencies (set during module initialization).
*/
let _adapter = null;
let _playerPrivacyManager = null;
/**
* Flag to track if dependencies have been initialized.
*/
let _isInitialized = false;
/**
* Initialize static dependencies.
* Called once during module initialization.
* @param {import('../../foundry/FoundryAdapter.js').FoundryAdapter} adapter
* @param {import('../../core/PlayerPrivacyManager.js').PlayerPrivacyManager} playerPrivacyManager
*/
export function initGMPlayerPrivacySelector(adapter, playerPrivacyManager) {
if (!adapter || typeof adapter !== 'object') {
throw new TypeError('initGMPlayerPrivacySelector: adapter is required');
}
if (!playerPrivacyManager || typeof playerPrivacyManager !== 'object') {
throw new TypeError('initGMPlayerPrivacySelector: playerPrivacyManager is required');
}
_adapter = adapter;
_playerPrivacyManager = playerPrivacyManager;
_isInitialized = true;
// Register the settings menu
_registerSettingsMenu();
}
/**
* Check if dependencies have been initialized.
* @returns {boolean}
*/
export function isInitialized() {
return _isInitialized;
}
/**
* Register the GM-only settings menu.
*/
function _registerSettingsMenu() {
_adapter.settings.registerMenu('scrying-pool', 'gmPlayerPrivacySelector', {
name: 'SCRYING_POOL.Settings.GMPlayerPrivacySelector',
label: 'SCRYING_POOL.Settings.GMPlayerPrivacySelectorLabel',
hint: 'SCRYING_POOL.Settings.GMPlayerPrivacySelectorHint',
icon: 'fa-solid fa-users',
type: GMPlayerPrivacySelectorMenu,
restricted: true, // GM only
});
}
/**
* GM Player Privacy Selector Menu class.
* When instantiated by Foundry, it creates a user selector dialog.
*/
export class GMPlayerPrivacySelectorMenu {
/**
* @param {object} [options] - Foundry options (unused, but required by settings menu API)
*/
constructor(options = {}) {
this._adapter = _adapter;
this._playerPrivacyManager = _playerPrivacyManager;
this._options = options;
this._rendered = false;
}
/**
* Get rendered state.
* @returns {boolean}
*/
get rendered() {
return this._rendered;
}
/**
* Get the element.
* @returns {HTMLElement|null}
*/
get element() {
return this._element ?? null;
}
/**
* Set the element.
* @param {HTMLElement} v - The element to set
*/
set element(v) {
this._element = v;
}
/**
* Store references to created panels for cleanup.
* @type {PlayerPrivacyPanel[]}
*/
_panels = [];
/**
* Store reference to close button element.
* @type {HTMLElement|null}
*/
_closeButton = null;
/**
* Render the user selection dialog.
* @param {boolean|object} [_force] - Render options (unused by ApplicationV2)
* @returns {Promise<void>}
*/
async render(_force) {
// Prevent double rendering
if (this._rendered) {
return;
}
try {
if (!_isInitialized) {
throw new Error('GMPlayerPrivacySelector: Dependencies not initialized. Call initGMPlayerPrivacySelector first.');
}
const users = this._adapter.users.all?.() ?? [];
// Escape user data to prevent XSS
const escapeHtml = (str) => {
if (str == null) return '';
const div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
};
const html = `
<div class="sp-gm-privacy-selector">
<div class="sp-dialog-header">
<h2>${this._adapter.i18n.localize('SCRYING_POOL.Settings.GMPlayerPrivacySelector')}</h2>
<button type="button" class="sp-close-button" aria-label="Close">
<i class="fas fa-times"></i>
</button>
</div>
<p>${this._adapter.i18n.localize('SCRYING_POOL.Settings.GMPlayerPrivacySelectorHint')}</p>
<div class="sp-user-list">
${users.map(user => {
const name = escapeHtml(user.name ?? '');
const id = escapeHtml(user.id ?? '');
const role = user.isGM ? 'GM' : 'Player';
return `
<div class="sp-user-item" data-user-id="${id}">
<span class="sp-user-name">${name}</span>
<span class="sp-user-role">${role}</span>
</div>
`;}).join('')}
</div>
</div>
`;
// Create a dialog element
this._element = document.createElement('div');
this._element.innerHTML = html;
this._element.classList.add('scrying-pool', 'gm-privacy-selector-dialog');
// Use CSS classes instead of inline styles
this._element.style.position = 'fixed';
this._element.style.top = '50%';
this._element.style.left = '50%';
this._element.style.transform = 'translate(-50%, -50%)';
this._element.style.zIndex = '1000';
// Add to DOM
if (document.body) {
document.body.appendChild(this._element);
}
// Cache close button and add handler
this._closeButton = this._element.querySelector('.sp-close-button');
if (this._closeButton) {
this._closeButton.addEventListener('click', () => this.close());
}
// Add click handlers for each user
this._element.querySelectorAll('.sp-user-item').forEach(item => {
item.addEventListener('click', () => {
const userId = item.dataset.userId;
if (userId) {
this._openPrivacyPanel(userId);
}
});
});
this._rendered = true;
} catch (err) {
console.error('[ScryingPool] GMPlayerPrivacySelector render failed:', err);
throw err;
}
}
/**
* Close the dialog and clean up resources.
* @returns {Promise<void>}
*/
async close() {
// Remove all event listeners by cloning and replacing the element
if (this._element && this._element.parentNode) {
this._element.parentNode.removeChild(this._element);
}
// Clean up panel references
for (const panel of this._panels) {
try {
await panel.close();
} catch (err) {
console.error('[ScryingPool] Error closing privacy panel:', err);
}
}
this._panels = [];
this._closeButton = null;
this._element = null;
this._rendered = false;
}
/**
* Open the PlayerPrivacyPanel for a specific user.
* @param {string} userId - The user ID to view
*/
_openPrivacyPanel(userId) {
if (!_isInitialized) {
console.error('[ScryingPool] GMPlayerPrivacySelector: Cannot open panel, dependencies not initialized');
return;
}
if (!userId || typeof userId !== 'string') {
console.error('[ScryingPool] GMPlayerPrivacySelector: Invalid userId');
return;
}
try {
const panel = new PlayerPrivacyPanel(
this._adapter,
this._playerPrivacyManager,
userId
);
this._panels.push(panel);
panel.render(true);
} catch (err) {
console.error('[ScryingPool] GMPlayerPrivacySelector: Failed to create privacy panel:', err);
}
}
}
// Legacy export for backwards compatibility
export { GMPlayerPrivacySelectorMenu as GMPlayerPrivacySelector };