Story 4.1 completed
This commit is contained in:
@@ -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 };
|
||||
Reference in New Issue
Block a user