Story 4.1: Tasks 3-6 Complete - Director's Board Integration & Settings Menu
- Task 3: Extended FoundryAdapter with user flag access methods
- Added getFlag(userId, scope, key) method
- Added setFlag(userId, scope, key, value) method
- Added getFlagModule(userId, key) convenience method
- Added setFlagModule(userId, key, value) convenience method
- Task 4: Integrated Privacy Settings with Director's Board
- Updated participant-card.hbs to show Reaction Cam badge
- Modified boardUtils.js to pass playerPrivacyManager through context
- Updated DirectorsBoard to accept and pass playerPrivacyManager
- Added CSS styles for Reaction Cam badge (SP accent color)
- Task 5: Registered PlayerPrivacyPanel in module settings
- Added settings menu registration in module.js Hooks.once('ready')
- Available to all users (restricted: false)
- Uses localized labels and hints
- Task 6: Added all localization strings
- Added SCRYING_POOL.PrivacyPanel.* strings for panel UI
- Added SCRYING_POOL.Settings.* strings for settings menu
- Updated story file with task completion status
Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
@@ -58,14 +58,16 @@ export class DirectorsBoard extends _AppBase {
|
||||
* @param {object} controller
|
||||
* @param {object} adapter
|
||||
* @param {import('../core/ScenePresetManager.js').ScenePresetManager} scenePresetManager
|
||||
* @param {import('../../core/PlayerPrivacyManager.js').PlayerPrivacyManager} playerPrivacyManager
|
||||
* @param {object} [options]
|
||||
*/
|
||||
constructor(stateStore, controller, adapter, scenePresetManager, options = {}) {
|
||||
constructor(stateStore, controller, adapter, scenePresetManager, playerPrivacyManager, options = {}) {
|
||||
super(options);
|
||||
this._stateStore = stateStore;
|
||||
this._controller = controller;
|
||||
this._adapter = adapter;
|
||||
this._scenePresetManager = scenePresetManager;
|
||||
this._playerPrivacyManager = playerPrivacyManager;
|
||||
this._hookId = null;
|
||||
/** @type {Map<string, string>|null} Pre-bulk-action snapshot for single-step undo */
|
||||
this._undoSnapshot = null;
|
||||
@@ -337,7 +339,7 @@ export class DirectorsBoard extends _AppBase {
|
||||
|
||||
/** @inheritdoc */
|
||||
async _prepareContext() {
|
||||
const base = buildBoardContext(this._stateStore, this._controller, this._adapter);
|
||||
const base = buildBoardContext(this._stateStore, this._controller, this._adapter, this._playerPrivacyManager);
|
||||
const presetCount = this._scenePresetManager?.list?.().length ?? 0;
|
||||
|
||||
// Get auto-apply config for current scene (Story 3.2)
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
// @ts-nocheck
|
||||
|
||||
// Conditional base class — test environment lacks foundry globals.
|
||||
// At module load time in tests, foundry is undefined → fallback class is used.
|
||||
|
||||
/** @private */
|
||||
const _AppBase =
|
||||
typeof foundry !== 'undefined' &&
|
||||
foundry.applications?.api?.HandlebarsApplicationMixin &&
|
||||
foundry.applications?.api?.ApplicationV2
|
||||
? foundry.applications.api.HandlebarsApplicationMixin(
|
||||
foundry.applications.api.ApplicationV2
|
||||
)
|
||||
: class _FallbackApp {
|
||||
static DEFAULT_OPTIONS = {};
|
||||
static PARTS = {};
|
||||
get rendered() { return this._rendered ?? false; }
|
||||
set rendered(v) { this._rendered = v; }
|
||||
get element() { return this._element ?? null; }
|
||||
set element(v) { this._element = v; }
|
||||
async render() { this._rendered = true; }
|
||||
async close() { this._rendered = false; }
|
||||
async _prepareContext() { return {}; }
|
||||
_onRender() {}
|
||||
_onClose() {}
|
||||
_onPosition() {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Player Privacy Panel — UI component for players to control automation opt-ins.
|
||||
*
|
||||
* Extends ApplicationV2 via HandlebarsApplicationMixin.
|
||||
* Displays privacy settings and allows toggling (for own user) or read-only view (for other users).
|
||||
*
|
||||
* Import rule: may import from src/core/, src/foundry/, src/contracts/.
|
||||
* Constructors are side-effect free.
|
||||
*
|
||||
* @module ui/player/PlayerPrivacyPanel
|
||||
*/
|
||||
|
||||
/**
|
||||
* Player Privacy Panel dialog for managing automation opt-in settings.
|
||||
*/
|
||||
export class PlayerPrivacyPanel extends _AppBase {
|
||||
static DEFAULT_OPTIONS = {
|
||||
id: 'scrying-pool-player-privacy-panel',
|
||||
classes: ['scrying-pool', 'player-privacy-panel'],
|
||||
window: {
|
||||
title: 'Player Privacy Panel',
|
||||
resizable: false,
|
||||
width: 380,
|
||||
height: 'auto',
|
||||
},
|
||||
position: {},
|
||||
};
|
||||
|
||||
static PARTS = {
|
||||
dialog: {
|
||||
template: 'modules/video-view-manager/templates/player-privacy-panel.hbs',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {import('../../foundry/FoundryAdapter.js').FoundryAdapter} adapter
|
||||
* Injected FoundryAdapter surface.
|
||||
* @param {import('../../core/PlayerPrivacyManager.js').PlayerPrivacyManager} playerPrivacyManager
|
||||
* Injected PlayerPrivacyManager for privacy settings operations.
|
||||
* @param {string} targetUserId - The user ID whose settings are being viewed/edited.
|
||||
* @param {object} [options]
|
||||
* @throws {TypeError} If adapter or playerPrivacyManager is invalid.
|
||||
*/
|
||||
constructor(adapter, playerPrivacyManager, targetUserId, options = {}) {
|
||||
// Validate dependencies
|
||||
if (!adapter || typeof adapter !== 'object') {
|
||||
throw new TypeError(
|
||||
'PlayerPrivacyPanel: adapter argument is required and must be an object'
|
||||
);
|
||||
}
|
||||
if (!playerPrivacyManager || typeof playerPrivacyManager !== 'object') {
|
||||
throw new TypeError(
|
||||
'PlayerPrivacyPanel: playerPrivacyManager argument is required and must be an object'
|
||||
);
|
||||
}
|
||||
if (typeof targetUserId !== 'string' || targetUserId.length === 0) {
|
||||
throw new TypeError(
|
||||
'PlayerPrivacyPanel: targetUserId must be a non-empty string'
|
||||
);
|
||||
}
|
||||
|
||||
super(options);
|
||||
|
||||
this._adapter = adapter;
|
||||
this._playerPrivacyManager = playerPrivacyManager;
|
||||
this._targetUserId = targetUserId;
|
||||
|
||||
// Cache for DOM elements
|
||||
/** @type {HTMLElement|null} */
|
||||
this._reactionCamToggle = null;
|
||||
/** @type {HTMLElement|null} */
|
||||
this._hpReactiveCamToggle = null;
|
||||
|
||||
// Current settings state
|
||||
/** @type {import('../../contracts/privacy-settings.js').PrivacySettings|null} */
|
||||
this._currentSettings = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the template context with i18n labels and current settings.
|
||||
* @returns {Promise<object>} Template context.
|
||||
*/
|
||||
async _prepareContext() {
|
||||
const i18n = this._adapter.i18n;
|
||||
|
||||
// Get current settings for the target user
|
||||
const settings = this._playerPrivacyManager.getSettings(this._targetUserId);
|
||||
this._currentSettings = settings;
|
||||
|
||||
// Determine if this is the current user (editable) or another user (read-only)
|
||||
const currentUserId = this._adapter.users.current?.()?.id;
|
||||
const isOwnUser = this._targetUserId === currentUserId;
|
||||
const isReadOnly = !isOwnUser;
|
||||
|
||||
return {
|
||||
// Panel metadata
|
||||
title: i18n.localize('SCRYING_POOL.PrivacyPanel.title'),
|
||||
sectionHeader: i18n.localize('SCRYING_POOL.PrivacyPanel.sectionHeader'),
|
||||
readOnlyNotice: i18n.localize('SCRYING_POOL.PrivacyPanel.readOnlyNotice'),
|
||||
|
||||
// Automation effects
|
||||
automationEffects: [
|
||||
{
|
||||
key: 'reactionCam',
|
||||
label: i18n.localize('SCRYING_POOL.PrivacyPanel.reactionCamLabel'),
|
||||
description: i18n.localize('SCRYING_POOL.PrivacyPanel.reactionCamDescription'),
|
||||
enabled: settings.reactionCamEnabled,
|
||||
settingKey: 'reactionCamEnabled',
|
||||
},
|
||||
{
|
||||
key: 'hpReactiveCamStyling',
|
||||
label: i18n.localize('SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingLabel'),
|
||||
description: i18n.localize('SCRYING_POOL.PrivacyPanel.hpReactiveCamStylingDescription'),
|
||||
enabled: settings.hpReactiveCamStylingEnabled,
|
||||
settingKey: 'hpReactiveCamStylingEnabled',
|
||||
},
|
||||
],
|
||||
|
||||
// Toggle labels
|
||||
toggleOnLabel: i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOn'),
|
||||
toggleOffLabel: i18n.localize('SCRYING_POOL.PrivacyPanel.toggleOff'),
|
||||
|
||||
// State
|
||||
isReadOnly,
|
||||
isOwnUser,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event handlers after rendering.
|
||||
* @param {HTMLElement} element - The dialog element.
|
||||
*/
|
||||
_onRender(element) {
|
||||
// Cache toggle elements
|
||||
this._reactionCamToggle = element.querySelector('[data-setting="reactionCamEnabled"]');
|
||||
this._hpReactiveCamToggle = element.querySelector('[data-setting="hpReactiveCamStylingEnabled"]');
|
||||
|
||||
// Set up toggle change handlers
|
||||
this._setupToggleHandlers(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up event handlers for toggle switches.
|
||||
* @param {HTMLElement} element - The dialog element.
|
||||
*/
|
||||
_setupToggleHandlers(element) {
|
||||
const toggles = element.querySelectorAll('.player-privacy-panel__toggle input[type="checkbox"]');
|
||||
|
||||
for (const toggle of toggles) {
|
||||
toggle.addEventListener('change', (event) => this._onToggleChange(event));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles toggle change events.
|
||||
* @param {Event} event - The change event from the checkbox.
|
||||
*/
|
||||
async _onToggleChange(event) {
|
||||
const checkbox = event.target;
|
||||
if (!checkbox || checkbox.type !== 'checkbox') return;
|
||||
|
||||
const settingKey = checkbox.getAttribute('data-setting');
|
||||
const newValue = checkbox.checked;
|
||||
|
||||
// Don't allow changes in read-only mode
|
||||
if (this._isReadOnlyMode()) {
|
||||
checkbox.checked = !newValue; // Revert the change
|
||||
return;
|
||||
}
|
||||
|
||||
if (!settingKey) {
|
||||
console.warn('[ScryingPool] PlayerPrivacyPanel: toggle missing data-setting attribute');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Update the setting via PlayerPrivacyManager
|
||||
await this._playerPrivacyManager.setSetting(
|
||||
this._targetUserId,
|
||||
settingKey,
|
||||
newValue
|
||||
);
|
||||
|
||||
// Update our cached settings
|
||||
if (this._currentSettings) {
|
||||
this._currentSettings[settingKey] = newValue;
|
||||
}
|
||||
|
||||
// Show success notification
|
||||
this._adapter.notifications.info(
|
||||
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.savedNotification')
|
||||
);
|
||||
} catch (err) {
|
||||
// Revert the checkbox if the update failed
|
||||
checkbox.checked = !newValue;
|
||||
|
||||
// Log error
|
||||
console.error('[ScryingPool] PlayerPrivacyPanel: failed to update setting:', err);
|
||||
|
||||
// Show error notification
|
||||
if (err instanceof TypeError) {
|
||||
this._adapter.notifications.error(err.message);
|
||||
} else {
|
||||
this._adapter.notifications.error(
|
||||
this._adapter.i18n.localize('SCRYING_POOL.PrivacyPanel.saveError')
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the panel is in read-only mode.
|
||||
* @returns {boolean} True if in read-only mode.
|
||||
* @private
|
||||
*/
|
||||
_isReadOnlyMode() {
|
||||
const currentUserId = this._adapter.users.current?.()?.id;
|
||||
return this._targetUserId !== currentUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up event listeners.
|
||||
*/
|
||||
_onClose() {
|
||||
// Clear cached elements
|
||||
this._reactionCamToggle = null;
|
||||
this._hpReactiveCamToggle = null;
|
||||
this._currentSettings = null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user