6 Commits

Author SHA1 Message Date
uberwald a9dbb9306a Various enhancements, restyling and new options
CI / ci (push) Successful in 44s
Release Creation / build (release) Successful in 44s
2026-05-27 12:10:44 +02:00
uberwald 602b1fc8e7 Various enhancements, restyling and new options
CI / ci (push) Successful in 49s
2026-05-27 12:10:19 +02:00
uberwald daed472b46 Add CC BY-NC-SA 4.0 license
CI / ci (push) Successful in 40s
2026-05-27 12:09:20 +02:00
uberwald d9eda8c725 Add comprehensive README in French and English
CI / ci (push) Successful in 37s
2026-05-27 12:05:06 +02:00
uberwald 156f786448 Spotlight: double focused participant size (2x widget width + window)
CI / ci (push) Successful in 38s
Release Creation / build (release) Successful in 42s
- _prepareContext doubles widgetWidth when _focusedUserId is set
- _onRender multiplies strip window dimensions by 2 in spotlight mode
- --sp-widget-width CSS var uses doubled value so tile renders at 2x
2026-05-27 11:57:34 +02:00
uberwald 9e80c2c028 Add re-order, spotlight/focus, and auto-position-snapshots features
- HTML5 drag-and-drop reordering of strip participants (per-GM flag)
- Shift+click toggles spotlight focus on a participant (gold ring indicator)
- Escape exits focus mode
- Auto-save strip position on drag end + every 30s with viewport validation
- Reset strip position button in Director's Board
- French locale strings for reset button
2026-05-27 11:44:24 +02:00
48 changed files with 2415 additions and 14 deletions
@@ -0,0 +1,76 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the module.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: 'module.json'
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip
# Set up Node.js
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# Create a zip file with all files required by the module to add to the release
- run: |
apt update -y
apt install -y zip
- run: node scripts/package.mjs
- name: Upload release assets
run: |
# Upload module.zip
curl -s -X POST \
-H "Content-Type: application/octet-stream" \
-H "Authorization: token ${{secrets.ALLOW_PUSH_RELEASE}}" \
--data-binary @./module.zip \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.zip"
# Upload module.json
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${{secrets.ALLOW_PUSH_RELEASE}}" \
--data-binary @./module.json \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.json"
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: 'scrying-pool'
version: ${{gitea.event.release.tag_name}}
manifest: 'https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json'
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip'
compatibility-minimum: '14'
compatibility-verified: '14'
@@ -0,0 +1,76 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the module.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: 'module.json'
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip
# Set up Node.js
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# Create a zip file with all files required by the module to add to the release
- run: |
apt update -y
apt install -y zip
- run: node scripts/package.mjs
- name: Upload release assets
run: |
# Upload module.zip
curl -s -X POST \
-H "Content-Type: application/octet-stream" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.zip \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.zip"
# Upload module.json
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${{secrets.ALLOW_PUSH_RELEASE}}" \
--data-binary @./module.json \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.json"
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: 'scrying-pool'
version: ${{gitea.event.release.tag_name}}
manifest: 'https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json'
notes: 'https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip'
compatibility-minimum: '14'
compatibility-verified: '14'
@@ -0,0 +1,76 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the module.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: "module.json"
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip
# Set up Node.js
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# Create a zip file with all files required by the module to add to the release
- run: |
apt update -y
apt install -y zip
- run: node scripts/package.mjs
- name: Upload release assets
run: |
# Upload module.zip
curl -s -X POST \
-H "Content-Type: application/octet-stream" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.zip \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.zip"
# Upload module.json
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.json \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.json"
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: "scrying-pool"
version: ${{gitea.event.release.tag_name}}
manifest: "https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json"
notes: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip"
compatibility-minimum: "14"
compatibility-verified: "14"
@@ -0,0 +1,76 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the module.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: "module.json"
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip
# Set up Node.js
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# Create a zip file with all files required by the module to add to the release
- run: |
apt update -y
apt install -y zip
- run: node scripts/package.mjs
- name: Upload release assets
run: |
# Upload module.zip
curl -s -X POST \
-H "Content-Type: application/octet-stream" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.zip \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.zip"
# Upload module.json
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.json \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.json"
- name: Publish to Foundry server
uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
with:
token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
id: "scrying-pool"
version: ${{gitea.event.release.tag_name}}
manifest: "https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json"
notes: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip"
compatibility-minimum: "14"
compatibility-verified: "14"
@@ -0,0 +1,76 @@
name: Release Creation
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo "💡 The ${{ gitea.repository }} repository will be cloned to the runner."
- uses: RouxAntoine/checkout@v3.5.4
# get part of the tag after the `v`
- name: Extract tag version number
id: get_version
uses: battila7/get-version-action@v2
# Substitute the Manifest and Download URLs in the module.json
- name: Substitute Manifest and Download Links For Versioned Ones
id: sub_manifest_link_version
uses: microsoft/variable-substitution@v1
with:
files: "module.json"
env:
version: ${{steps.get_version.outputs.version-without-v}}
url: https://www.uberwald.me/gitea/${{gitea.repository}}
manifest: https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json
download: https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip
# Set up Node.js
- name: Setup Node.js
uses: https://github.com/actions/setup-node@v4
with:
node-version: "20"
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
# Create a zip file with all files required by the module to add to the release
- run: |
apt update -y
apt install -y zip
- run: node scripts/package.mjs
- name: Upload release assets
run: |
# Upload module.zip
curl -s -X POST \
-H "Content-Type: application/octet-stream" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.zip \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.zip"
# Upload module.json
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Authorization: token ${{secrets.RELEASE_SCRYING_POOL}}" \
--data-binary @./module.json \
"https://www.uberwald.me/api/v1/repos/${{gitea.repository}}/releases/${{gitea.event.release.id}}/assets?name=module.json"
# - name: Publish to Foundry server
# uses: https://github.com/djlechuck/foundryvtt-publish-package-action@v1
# with:
# token: ${{ secrets.FOUNDRYVTT_RELEASE_TOKEN }}
# id: "scrying-pool"
# version: ${{gitea.event.release.tag_name}}
# manifest: "https://www.uberwald.me/gitea/public/${{gitea.repository}}/releases/download/latest/module.json"
# notes: "https://www.uberwald.me/gitea/${{gitea.repository}}/releases/download/${{gitea.event.release.tag_name}}/module.zip"
# compatibility-minimum: "14"
# compatibility-verified: "14"
+32
View File
@@ -0,0 +1,32 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+32
View File
@@ -0,0 +1,32 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14 — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+34
View File
@@ -0,0 +1,34 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14+ — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
}
],
"packs": [
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+37
View File
@@ -0,0 +1,37 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14+ — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
},
{
"lang": "es",
"name": "Spanish",
"path": "lang/es.json"
}
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+37
View File
@@ -0,0 +1,37 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14+ — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
},
{
"lang": "fr",
"name": "French",
"path": "lang/fr.json"
}
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+37
View File
@@ -0,0 +1,37 @@
{
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"description": "GM camera visibility control for FoundryVTT v14+ — hide, show, and manage participant feeds in real time.",
"authors": [
{
"name": "Morr"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
"esmodules": [
"module.js"
],
"styles": [
"styles/scrying-pool.css"
],
"languages": [
{
"lang": "en",
"name": "English",
"path": "lang/en.json"
},
{
"lang": "fr",
"name": "French",
"path": "lang/fr.json"
}
],
"url": "${url}",
"manifest": "${manifest}",
"download": "${download}",
"flags": {}
}
+86
View File
@@ -0,0 +1,86 @@
# Scrying Pool Documentation Pack
**Compendium is now built and ready!**
## Structure
- `scrying-pool-docs.db` - **Pre-built SQLite compendium database** (20KB)
- `scrying-pool-guide.json` - Source JournalEntry content (English & French)
- `assets/` - Screenshot images (5 JPG files)
## What's Included
The `scrying-pool-docs.db` file contains a **JournalEntry** with:
- **2 pages**: English and French documentation
- **Professional styling** with CSS
- **5 screenshot references** (screenshot-main.jpg, screenshot-directors-board.jpg, etc.)
## Current Setup
The `module.json` defines the pack:
```json
{
"packs": [
{
"name": "scrying-pool-documentation",
"label": "Scrying Pool Documentation",
"path": "packs/scrying-pool-docs.db",
"system": "",
"type": "JournalEntry"
}
]
}
```
When the module is installed in FoundryVTT, the **"Scrying Pool Documentation"** compendium will appear under the **Modules** tab with the JournalEntry already populated.
## Updating the Documentation
To update the documentation content:
### Option 1: Using the build script (Recommended)
```bash
# Edit the JSON source
nano packs/scrying-pool-guide.json
# Rebuild the database
node scripts/build-compendium.cjs
```
### Option 2: Manual SQLite editing
```bash
sqlite3 packs/scrying-pool-docs.db
```
### Option 3: Using fvtt-cli (Alternative)
```bash
npm install -g @foundryvtt/fvtt-cli
fvtt-cli pack create \
--input packs/scrying-pool-guide.json \
--output packs/scrying-pool-docs.db \
--type JournalEntry
```
## File Structure
```
packs/
├── scrying-pool-docs.db # SQLite database (compendium)
├── scrying-pool-guide.json # Source JSON (for editing)
├── README.md # This file
└── assets/
├── README.md
├── screenshot-main.jpg
├── screenshot-directors-board.jpg
├── screenshot-player-view.jpg
├── screenshot-presets.jpg
└── screenshot-badge-states.jpg
```
## Technical Details
- **Database**: SQLite3 with WAL mode enabled
- **Table**: `JournalEntry` with FoundryVTT v14 schema
- **JournalEntry ID**: `scrying-pool-guide`
- **Name**: "Scrying Pool - User Guide / Guide de l'utilisateur"
- **Pages**: 2 (English and French)
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+439
View File
@@ -0,0 +1,439 @@
Attribution-NonCommercial-ShareAlike 4.0 International
=======================================================================
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public
licenses, a licensor grants the public permission to use the
licensed material under specified terms and conditions. If
the licensor's permission is not necessary for any reason--for
example, because of any applicable exception or limitation to
copyright--then that use is not regulated by the license. Our
licenses grant only permissions under copyright and certain
other rights that a licensor has authority to grant. Use of
the licensed material may still be restricted for other
reasons, including because others have copyright or other
rights in the material. A licensor may make special requests,
such as asking that all changes be marked or described.
Although not required by our licenses, you are encouraged to
refer to those standards and best practices.
More considerations for the public:
wiki.creativecommons.org/Considerations_for_licensees
=======================================================================
Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International
Public License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-NonCommercial-ShareAlike 4.0 International Public License
("Public License"). To the extent this Public License may be
interpreted as a contract, You are granted the Licensed Rights in
consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the
Licensor receives from making the Licensed Material available under
these terms and conditions.
Section 1 -- Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. BY-NC-SA Compatible License means a license listed at
creativecommons.org/compatiblelicenses, approved by Creative
Commons as essentially the equivalent of this Public License.
d. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
performance, broadcast, sound recording, and Sui Generis Database
Rights, without regard to how the rights are labeled or
categorized. For purposes of this Public License, the rights
specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights.
e. Effective Technological Measures means those measures that, in the
absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright
Treaty adopted on December 20, 1996, and/or similar international
agreements.
f. Exceptions and Limitations means fair use, fair dealing, and/or
any other exception or limitation to Copyright and Similar Rights
that applies to Your use of the Licensed Material.
g. License Elements means the license attributes listed in the name
of a Creative Commons Public License. The License Elements of this
Public License are Attribution, NonCommercial, and ShareAlike.
h. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
i. Licensed Rights means the rights granted to You subject to the
terms and conditions of this Public License, which are limited to
all Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
j. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
k. NonCommercial means not primarily intended for or directed towards
commercial advantage or monetary compensation. For purposes of
this Public License, the exchange of the Licensed Material for
other material subject to Copyright and Similar Rights by digital
file-sharing or similar means is NonCommercial provided there is
no payment of monetary compensation in connection with the
exchange.
l. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
m. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of
the Council of 11 March 1996 on the legal protection of databases,
as amended and/or succeeded, as well as other essentially
equivalent rights anywhere in the world.
n. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 -- Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part, for NonCommercial purposes only; and
b. produce, reproduce, and Share Adapted Material for
NonCommercial purposes only.
2. Exceptions and Limitations. For the avoidance of doubt, where
Exceptions and Limitations apply to Your use, this Public
License does not apply, and You do not need to comply with
its terms and conditions.
3. Term. The term of this Public License is specified in Section
6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor -- Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor -- Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You apply.
c. No downstream restrictions. You may not offer or impose
any additional or different terms or conditions on, or
apply any Effective Technological Measures to, the
Licensed Material if doing so restricts exercise of the
Licensed Rights by any recipient.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not
licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to
the extent possible, the Licensor waives and/or agrees not to
assert any such rights held by the Licensor to the limited
extent necessary to allow You to exercise the Licensed
Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this
Public License.
3. To the extent possible, the Licensor waives any right to
collect royalties from You for the exercise of the Licensed
Rights, whether directly or through a collecting society
under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly
reserves any right to collect such royalties, including when
the Licensed Material is used other than for NonCommercial
purposes.
Section 3 -- License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the Licensor
with the Licensed Material:
i. identification of the creator(s) of the Licensed
Material and any others designated to receive
attribution, in any reasonable manner requested by
the Licensor (including by pseudonym if
designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to the
extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or
hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may be
reasonable to satisfy the conditions by providing a URI or
hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-NC-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the
Adapter's License You apply. You may satisfy this condition
in any reasonable manner based on the medium, means, and
context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 -- Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database for NonCommercial purposes
only;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material,
including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 -- Disclaimer of Warranties and Limitation of Liability.
a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE
EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS
AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF
ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS,
IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION,
WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS,
ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT
KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT
FULLY ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY
TO YOU.
b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE
TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION,
NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT,
INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES,
COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR
USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN
ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR
DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT FULLY ALLOWED IN
FULL OR IN PART, THIS LIMITATION MAY NOT APPLY TO YOU.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 -- Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply with
this Public License, then Your rights under this Public License
terminate automatically.
b. Where Your right to use the Licensed Material has terminated under
Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided
it is cured within 30 days of Your discovery of the
violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 -- Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 -- Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is
deemed unenforceable, it shall be automatically reformed to the
minimum extent necessary to make it enforceable. If the provision
cannot be reformed, it shall be severed from this Public License
without affecting the enforceability of the remaining terms and
conditions.
c. No term or condition of this Public License will be waived and no
failure to comply consented to unless expressly agreed to by the
Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public
licenses. Notwithstanding, Creative Commons may elect to apply one of
its public licenses to material it publishes and in those instances
will be considered the "Licensor." The text of the Creative Commons
public licenses is dedicated to the public domain under the CC0 Public
Domain Dedication. Except for the limited purpose of indicating that
material is shared under a Creative Commons public license or as
otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark "Creative Commons" or any other trademark or logo
of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements,
understandings, or agreements concerning use of licensed material.
For the avoidance of doubt, this paragraph does not form part of the
public licenses.
=======================================================================
+195
View File
@@ -0,0 +1,195 @@
<p align="center">
<img src="https://raw.githubusercontent.com/morr/scrying-pool/main/.github/logo.png" alt="Scrying Pool" width="128" />
</p>
<h1 align="center">Scrying Pool</h1>
<p align="center">
<em>GM camera visibility control for FoundryVTT v14+</em>
<br />
<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/"><img src="https://img.shields.io/badge/license-CC%20BY--NC--SA%204.0-lightgrey" alt="License: CC BY-NC-SA 4.0" /></a>
<br />
<sub>Français · <a href="#english">English</a></sub>
</p>
---
## Français
**Scrying Pool** est un module FoundryVTT qui donne au MJ un contrôle total sur la visibilité des caméras des participants. Fini les flux vidéo désordonnés — gérez qui voit quoi depuis une interface flottante et discrète.
### Fonctionnalités
| Fonction | Description |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Barre du MJ** | Palette flottante listant tous les participants connectés, avec leur état caméra en temps réel |
| **Director's Board** | Fenêtre de pilotage complète : grille de participants, actions groupées, mise en avant |
| **Masquer / Afficher** | Cache ou révèle un flux à la table — par clic droit ou popover |
| **Actions groupées** | Tout afficher, tout masquer, annuler la dernière action en un clic |
| **Mise en avant (Spotlight)** | `Shift+clic` sur un participant → sa vidéo s'agrandit (×2), les autres disparaissent. `Échap` pour quitter |
| **Réorganisation** | Faites glisser les participants dans la barre pour les réorganiser. Double-clic sur la poignée pour réinitialiser |
| **Mosaïque / Grille** | 6 dispositions : vertical/horizontal/mosaïque, chacune en taille S ou L |
| **Formes des tuiles** | Rond, arrondi, hexagone, octogone — appliqué aux avatars et aux vidéos |
| **Bordures** | Largeur et couleur paramétrables pour les tuiles vidéo |
| **Préréglages** | Sauvegardez et chargez des dispositions de visibilité, avec application automatique par scène |
| **Portrait personnalisé** | Les joueurs peuvent choisir une image de remplacement quand leur caméra est indisponible |
| **Confidentialité** | Panneau de consentement pour les automatismes (caméra de réaction, etc.) |
| **Notifications** | Notifications configurables (toutes, MJ seulement, silencieux) |
| **Rétablissement position** | La position de la barre est sauvegardée automatiquement toutes les 30s et au relâché du glisser |
### Installation
1. Dans FoundryVTT, allez dans la **Modules**
2. Recherchez **Scrying Pool**
3. Cliquez **Installer**
### Utilisation
#### Barre du MJ (strip)
La barre flottante apparaît automatiquement quand vous vous connectez en tant que MJ. Elle liste tous les participants avec leur état :
- **Clic** → popover pour masquer/afficher ce participant
- **Clic droit** → menu contextuel (masquer/afficher)
- **Shift+clic** → mode Spotlight (vidéo ×2)
- **Glisser** la poignée ⟞ pour déplacer la barre
- **Glisser** un participant pour le réorganiser
- **Double-clic** sur la poignée → réinitialiser l'ordre des participants
- **Bouton `[+]`** → basculer taille S/L
- **Bouton `[⊞]`** → ouvrir le Director's Board
La barre se fondu dans le décor — pas de chrome visible tant que vous ne la survolez pas.
#### Director's Board
Ouvrable depuis la barre (bouton `[⊞]`) ou via `Ctrl+Shift+V`.
- Grille de tous les participants avec statut et indicateur d'opération en attente
- Boutons **Tout afficher** / **Tout masquer**
- **Undo** (apparaît après une action groupée)
- **Rétablir** (après une mise en avant Spotlight)
- Sélecteur de **disposition** (vertical/horizontal/mosaïque, S/L)
- Sélecteur de **forme** des tuiles (cercle, arrondi, hexagone, octogone)
- Contrôles de **bordure** (largeur, couleur)
- Réglages de **taille des widgets** (petite 60200px, grande 60400px)
- Sauvegarde/chargement/export/import de **préréglages**
- **Application automatique** par scène
- Activation/désactivation A/V
- **Bouton Réinitialiser la barre** (rétablit la position par défaut)
#### Raccourcis clavier
| Raccourci | Action |
| -------------- | ------------------------------------------ |
| `Ctrl+Shift+V` | Ouvrir/Fermer le Director's Board |
| `Ctrl+Shift+S` | Tout afficher |
| `Ctrl+Shift+H` | Tout masquer |
| `Ctrl+Shift+P` | Mettre en avant le participant sélectionné |
### Configuration
Module accessible depuis **Paramètres → Gérer les modules → Scrying Pool**.
| Option | Description |
| --------------------------- | ---------------------------------------------------------------------- |
| Afficher le flux du MJ | Quand activé, la propre caméra du MJ est visible dans la barre |
| Verbosité des notifications | Toutes / MJ seulement / Silencieux |
| Application automatique | Activer/désactiver globalement l'application des préréglages par scène |
### Support
Pour signaler un bug ou proposer une amélioration : [ouvrir un ticket](https://github.com/morr/scrying-pool/issues).
---
## English
**Scrying Pool** is a FoundryVTT module that gives GMs full control over participant camera visibility. No more messy video feeds — manage who sees what from a discreet floating interface.
### Features
| Feature | Description |
| --------------------- | ---------------------------------------------------------------------------------------- |
| **GM Strip** | Floating palette listing all connected participants with real-time camera state |
| **Director's Board** | Full command window: participant grid, bulk actions, spotlight |
| **Hide / Show** | Hide or reveal a feed to the table — via right-click or popover |
| **Bulk actions** | Show all, hide all, undo last bulk action in one click |
| **Spotlight** | `Shift+click` a participant → their video enlarges (×2), others disappear. `Esc` to exit |
| **Re-order** | Drag participants in the strip to rearrange them. Double-click the grip to reset |
| **Mosaic / Grid** | 6 layouts: vertical/horizontal/mosaic, each in S or L size |
| **Tile shapes** | Circle, rounded, hexagon, octagon — applied to avatars and video feeds |
| **Borders** | Configurable width and color for video tiles |
| **Presets** | Save and load visibility layouts, with per-scene auto-apply |
| **Custom portrait** | Players can set a fallback image when their camera is unavailable |
| **Privacy** | Consent panel for automation features (reaction cam, etc.) |
| **Notifications** | Configurable notification verbosity (all, GM only, silent) |
| **Position recovery** | Strip position auto-saved every 30s and on drag release |
### Installation
1. In FoundryVTT, go to the **Modules**
2. Search for **Scrying Pool**
3. Click **Install**
### Usage
#### GM Strip
The floating strip appears automatically when you log in as GM. It lists all participants with their state:
- **Click** → popover to hide/show that participant
- **Right-click** → context menu (hide/show)
- **Shift+click** → Spotlight mode (×2 video)
- **Drag** the ⟞ handle to move the strip
- **Drag** a participant to reorder them
- **Double-click** the handle → reset participant order
- **`[+]` button** → toggle S/L size
- **`[⊞]` button** → open the Director's Board
The strip is stealthy — no chrome visible until hover.
#### Director's Board
Open from the strip (`[⊞]` button) or via `Ctrl+Shift+V`.
- Grid of all participants with status and pending-operation indicator
- **Show All** / **Hide All** buttons
- **Undo** (appears after a bulk action)
- **Restore** (after a Spotlight)
- **Layout** selector (vertical/horizontal/mosaic, S/L)
- **Shape** selector (circle, rounded, hexagon, octagon)
- **Border** controls (width, color)
- **Widget size** settings (small 60200px, large 60400px)
- **Preset** save/load/export/import
- Per-scene **auto-apply**
- A/V toggle
- **Reset Strip** button (restores default position)
#### Keyboard Shortcuts
| Shortcut | Action |
| -------------- | ----------------------------- |
| `Ctrl+Shift+V` | Open/Close Director's Board |
| `Ctrl+Shift+S` | Show All |
| `Ctrl+Shift+H` | Hide All |
| `Ctrl+Shift+P` | Spotlight focused participant |
### Settings
Access from **Settings → Manage Modules → Scrying Pool**.
| Option | Description |
| ---------------------- | ------------------------------------------------------------ |
| Show GM Self Feed | When enabled, the GM's own camera feed is shown in the strip |
| Notification Verbosity | All / GM Only / Silent |
| Auto-Apply | Globally enable/disable per-scene preset auto-apply |
### Support
Report bugs or request features: [open an issue](https://github.com/morr/scrying-pool/issues).
---
<p align="center">
<sub>Made for FoundryVTT v14+ · <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a></sub>
</p>
@@ -0,0 +1,121 @@
---
title: 'Strip: Reorder, Spotlight & Auto-Snapshots'
type: 'feature'
created: '2026-05-27'
status: 'done'
baseline_commit: '816b7951fb88353b43c66e7b9f898701ea65ad2b'
context: []
---
<frozen-after-approval reason="human-owned intent — do not modify unless human renegotiates">
## Intent
**Problem:** Strip participants have a fixed order, no way to focus a single participant, and position is only saved on close.
**Approach:** Add HTML5 drag-and-drop reordering (per-GM flag), Shift+click spotlight (in-memory, one-tile focus mode), and periodic auto-save of strip position (debounced + 30s interval with viewport validation).
## Boundaries & Constraints
**Always:**
- Re-order: persists to `game.user.setFlag('scrying-pool', 'participantOrder', string[])`, per-GM only
- Spotlight: in-memory `_focusedUserId` only, no socket/persistence
- Auto-snapshots: extends existing `stripState` flag (`left, top, width, height, savedAt`), backward-compatible
- All three features are strip-only (not Director's Board)
- French locale strings in `scrying-pool.lang.fr.json`
**Ask First:** None
**Never:**
- No external drag-and-drop libraries
- No socket broadcasts for re-order or spotlight
- No CSS animation framework changes
## I/O & Edge-Case Matrix
| Scenario | Input / State | Expected Output / Behavior | Error Handling |
|----------|--------------|---------------------------|----------------|
| Re-order: drag participant | Drag start on tile | Tile shows `opacity: 0.3` during drag | N/A |
| Re-order: drop between tiles | Drop at new position | Participant order updates, flag saved, strip re-renders | Save failure silently caught |
| Re-order: double-click grip | Double-click grip area | Order resets to connection order | N/A |
| Spotlight: Shift+click | Shift+click participant | Only that participant visible, others `display:none` in DOM, strip resized | N/A |
| Spotlight: exit via Escape | Press Escape | Full list restored, `_focusedUserId` cleared | N/A |
| Spotlight: exit via button | Click exit-focus button (replaces DB icon) | Same as Escape | N/A |
| Auto-snapshot: drag ends | mouseup after drag | Debounced save to `stripState` flag | Save failure silently caught |
| Auto-snapshot: 30s timer | Interval fires | Save current position to `stripState` | Save failure silently caught |
| Auto-snapshot: position off-screen | Saved position outside viewport | Fall back to default position | Silent fallback, no error |
</frozen-after-approval>
## Code Map
- `src/ui/gm/ScryingPoolStrip.js` — All three features: drag handlers, spotlight state, auto-save timer
- `styles/components/_roster-strip.less` — Drag feedback (`opacity: 0.3`), spotlight visual state
- `templates/directors-board.hbs` — Reset strip position button
- `src/ui/gm/DirectorsBoard.js` — Reset position handler
- `module.js` — (no changes needed, instantiates the class)
- `scrying-pool.lang.fr.json` — French locale strings
## Tasks & Acceptance
**Execution:**
- [x] `src/ui/gm/ScryingPoolStrip.js` — Add `_focusedUserId`, drag handlers, _savePosition, re-order in _prepareContext
- [x] `styles/components/_roster-strip.less``.sp-state-focused` gold ring, drag ghost opacity
- [x] `templates/directors-board.hbs` — Reset strip position button
- [x] `src/ui/gm/DirectorsBoard.js``_onResetStripPosition()` handler
**Acceptance Criteria:**
- Given a strip with 3+ participants, when GM drags a participant tile to a new position, then the participant order updates and persists across re-renders
- Given any layout, when GM Shift+clicks a participant, then other participants collapse and the focused tile shows gold state ring
- Given spotlight mode is active, when GM presses Escape, then all participants return to normal
- Given the strip is open, when GM drags the strip to a new position and releases, then the position is saved
- Given a saved off-screen position, when the strip re-renders, then it appears at default coordinates
## Suggested Review Order
**Re-order + Spotlight core logic**
- Entry point: participant filtering, DnD handlers, focus toggle, position save
[`ScryingPoolStrip.js:149`](../../src/ui/gm/ScryingPoolStrip.js#L149)
- Drag-drop splice with `fromIdx < toIdx` adjustment + null guard on element
[`ScryingPoolStrip.js:1013`](../../src/ui/gm/ScryingPoolStrip.js#L1013)
- Focus toggle toggles `_focusedUserId`, re-renders; Escape exits via document listener
[`ScryingPoolStrip.js:1076`](../../src/ui/gm/ScryingPoolStrip.js#L1076)
- Auto-save: called on grip mouseup + 30s interval; cleanup on teardown
[`ScryingPoolStrip.js:1094`](../../src/ui/gm/ScryingPoolStrip.js#L1094)
- Viewport-validated position restore with negative-value guard
[`ScryingPoolStrip.js:178`](../../src/ui/gm/ScryingPoolStrip.js#L178)
**Template changes**
- `isFocused` conditional class for gold ring on focused participant
[`roster-strip.hbs:54`](../../templates/roster-strip.hbs#L54)
- Reset strip position button in Director's Board footer
[`directors-board.hbs:140`](../../templates/directors-board.hbs#L140)
**CSS**
- Gold state ring for `.sp-state-focused`, drag ghost opacity, drop target indicator
[`_roster-strip.less:517`](../../styles/components/_roster-strip.less#L517)
**Director's Board**
- Reset position handler clears stripState flag
[`DirectorsBoard.js:861`](../../src/ui/gm/DirectorsBoard.js#L861)
**Locales**
- Reset strip button strings (EN + FR)
[`en.json:82`](../../lang/en.json#L82) · [`fr.json:82`](../../lang/fr.json#L82)
## Verification
**Commands:**
- `npm run lint` -- expected: 0 errors
- `npm run typecheck` -- expected: 0 errors
- `npm run test` -- expected: all tests pass
@@ -12,7 +12,12 @@ inputDocuments:
workflowType: 'architecture'
project_name: 'video-view-manager'
user_name: 'Morr'
date: '2026-05-20'
date: '2026-05-27'
extendedAt: '2026-05-27'
extendedFeatures:
- reorder-participants
- spotlight-focus
- auto-position-snapshots
---
# Architecture Decision Document: Video View Manager (Scrying Pool)
@@ -1091,3 +1096,82 @@ both critical gaps found during validation were resolved inline without reopenin
Story 0 scaffold — `module.json` + `tsconfig.json` + `vitest.config.js` + `.eslintrc.js` +
`scripts/package.mjs` + `src/contracts/` (with validators + frozen fixtures) +
`.gitea/workflows/ci.yml` — all Story 0 ACs green before any Story 1 code.
---
## Feature Addendum (2026-05-27): Re-order Participants
### Problem
Strip participant order is fixed to user connection order. GM cannot rearrange participants to match table seating, spotlight priority, or personal preference.
### Decision: Drag-and-drop via native HTML5 API
| Decision | Choice | Rationale |
|---|---|---|
| Mechanism | HTML5 Drag & Drop (`draggable`, `dragstart`/`dragover`/`drop` events) | Zero dependencies; works in Foundry's embedded Chromium; no external lib needed |
| Persistence | User flag: `game.user.setFlag('scrying-pool', 'participantOrder', string[])` | Per-GM order; does not affect other GMs or players |
| Storage format | Ordered array of user IDs | Minimal, sortable, forward-compatible |
| Visual feedback | `opacity: 0.3` on dragged tile + `box-shadow` drop indicator on target gap | Lightweight; no layout shift |
| Reset | Double-click the grip area → reset to connection order | Escape hatch if order gets confusing |
| Scope | Strip only (not Director's Board) | Strip is the primary real-estate; DB reorder adds complexity with no clear need |
### Implementation Notes
- `ScryingPoolStrip` gets `_onDragStart`, `_onDragOver`, `_onDrop`, `_onDragEnd` handlers
- Attached in `_onRender` via `el.addEventListener`
- On drop: reorder participant list in `_prepareContext`, persist to user flag
- `_prepareContext` reads flag, applies order before filtering hidden participants
- No socket broadcast — order is per-GM/local-only
---
## Feature Addendum (2026-05-27): Spotlight / Focus
### Problem
In all layouts, participants share equal visual weight. GM cannot temporarily focus on one participant's video feed (e.g., a player speaking, a dramatic reveal).
### Decision: One-tile-expand mode within the strip
| Decision | Choice | Rationale |
|---|---|---|
| Activation | Click on participant avatar while holding `Shift` (or context menu → "Focus") | Intentional action; avoids accidental triggers |
| Visual | Selected tile expands to fill strip content area; other tiles collapse to `height: 0; overflow: hidden` (preserved in DOM for instant restore) | No layout reflow on restore; preserved DOM state |
| Restore | Click `Shift+click` again, or click an "Exit focus" button that replaces the toolbar, or press `Escape` | Multiple escape hatches |
| State | In-memory only: `_focusedUserId: string|null` on `ScryingPoolStrip` | Ephemeral — no persistence needed |
| Indicator | Focused tile gets `.sp-state-focused` class → gold state ring (`--sp-urgency-director`) | Visual consistency with existing state ring system |
| Strip sizing | `setPosition` recomputed with `1` participant during focus | Window snaps to single-tile dimensions |
| Layout compatibility | Works in all layouts (vertical, horizontal, mosaic) | Tile fills available space via same CSS that handles single-participant edge case |
| Director's Board | Unaffected — spotlight is strip-only | DB maintains overview while strip focuses |
### Implementation Notes
- `_focusedUserId` field on `ScryingPoolStrip`
- `_prepareContext` filters or transforms participant list: if `_focusedUserId` set, only that participant is visible; others have `hidden: true` (but preserved in DOM)
- Restore clears `_focusedUserId` → re-render → full list visible
- CSS: `.sp-participant-avatar.sp-state-focused` inherits existing state ring pattern (green → gold via `--sp-urgency-director`)
- No socket broadcast — purely local UI state
---
## Feature Addendum (2026-05-27): Auto Position Snapshots
### Problem
Strip position is saved only on close. If the browser window is resized, display changed, or the strip is accidentally dragged off-screen, there is no way to restore a known-good position without relaunching.
### Decision: Periodic auto-save + explicit save/restore
| Decision | Choice | Rationale |
|---|---|---|
| Auto-save trigger | `debounced` save on `mouseup` after drag ends | Saves only when user finishes moving; no save storm during drag |
| Auto-save interval | Also every 30s via `setInterval` while strip is open | Safety net if drag event fails to fire |
| Storage | User flag: `game.user.setFlag('scrying-pool', 'stripState')` | Already partially used (saves on close); extend to include timestamp |
| Save payload | `{ left, top, width, height, savedAt }` | Position + dimensions + timestamp for diagnostics |
| Restore trigger | On `_onRender` — if saved position exists and strip has no explicit position yet | First render gets saved position |
| Reset | Director's Board button "Reset strip position" → clears flag + re-renders at default position | Manual escape hatch |
| Multi-monitor safety | Validate `saved.left` and `saved.top` are within available viewport (`window.screen.availWidth/Height`) before applying | Prevents strip from loading off-screen after monitor config change |
### Implementation Notes
- Extend existing `_loadPosition()` in `ScryingPoolStrip` — already reads `stripState` flag
- Add `_savePosition()` called on `mouseup` after drag + every 30s interval
- Viewport validation: `saved.left < screen.availWidth - 50 && saved.top < screen.availHeight - 50`
- On validation failure: silently fall back to default position (no error notification)
- Director's Board: add "Reset strip position" button (minor template change)
- Extends existing `stripState` flag — no new flags needed, backward-compatible with old saves
+2 -1
View File
@@ -79,7 +79,8 @@
"avModeEnable": "Enable A/V",
"avModeDisable": "Disable A/V",
"avConfig": "A/V Settings…",
"avConfigTitle": "Open Foundry A/V server settings (signaling server, LiveKit, etc.)"
"avConfigTitle": "Open Foundry A/V server settings (signaling server, LiveKit, etc.)",
"resetStrip": "Reset Strip Position"
},
"bulk": {
"showAll": "Show All",
+2 -1
View File
@@ -79,7 +79,8 @@
"avModeEnable": "Activer A/V",
"avModeDisable": "Désactiver A/V",
"avConfig": "Paramètres A/V...",
"avConfigTitle": "Ouvrir les paramètres du serveur A/V de Foundry (serveur de signalisation, LiveKit, etc.)"
"avConfigTitle": "Ouvrir les paramètres du serveur A/V de Foundry (serveur de signalisation, LiveKit, etc.)",
"resetStrip": "Réinitialiser la position de la barre"
},
"bulk": {
"showAll": "Tout afficher",
+1
View File
@@ -2,6 +2,7 @@
"id": "scrying-pool",
"title": "Scrying Pool",
"version": "0.1.0",
"license": "CC-BY-NC-SA-4.0",
"description": "GM camera visibility control for FoundryVTT v14+ — hide, show, and manage participant feeds in real time.",
"authors": [
{
+1
View File
@@ -1,6 +1,7 @@
{
"name": "scrying-pool",
"version": "0.1.0",
"license": "CC-BY-NC-SA-4.0",
"description": "FoundryVTT v14 module — Scrying Pool camera visibility control",
"type": "module",
"scripts": {
+31 -2
View File
@@ -382,13 +382,29 @@ export class DirectorsBoard extends _AppBase {
// Defaults match the settings registration in module.js
const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '83';
const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '150';
const WIDTH_OPTIONS = [
const SM_WIDTH_OPTIONS = [
{ value: '60', label: '60px' },
{ value: '70', label: '70px' },
{ value: '80', label: '80px' },
{ value: '90', label: '90px' },
{ value: '100', label: '100px' },
{ value: '120', label: '120px' },
{ value: '140', label: '140px' },
{ value: '160', label: '160px' },
{ value: '180', label: '180px' },
{ value: '200', label: '200px' },
];
const MD_WIDTH_OPTIONS = [
{ value: '60', label: '60px' },
{ value: '80', label: '80px' },
{ value: '100', label: '100px' },
{ value: '120', label: '120px' },
{ value: '150', label: '150px' },
{ value: '200', label: '200px' },
{ value: '250', label: '250px' },
{ value: '300', label: '300px' },
{ value: '350', label: '350px' },
{ value: '400', label: '400px' },
];
// Tile shape selector
@@ -433,7 +449,8 @@ export class DirectorsBoard extends _AppBase {
tileBorderColor: currentTileBorderColor,
tileBorderWidths: TILE_BORDER_WIDTHS,
// Story 5.2: Video widget width customization
widthOptions: WIDTH_OPTIONS,
smWidthOptions: SM_WIDTH_OPTIONS,
mdWidthOptions: MD_WIDTH_OPTIONS,
widgetWidthSm,
widgetWidthMd,
};
@@ -496,6 +513,7 @@ export class DirectorsBoard extends _AppBase {
case 'set-dock-layout': this._onSetDockLayout(btn.dataset.layout); break;
case 'set-tile-shape': this._onSetTileShape(btn.dataset.shape); break;
case 'set-tile-border-width': this._onSetTileBorderWidth(parseInt(btn.dataset.width, 10)); break;
case 'reset-strip-position': this._onResetStripPosition(); break;
case 'close': this.close(); break;
}
};
@@ -840,6 +858,17 @@ export class DirectorsBoard extends _AppBase {
if (this.rendered) this.render({ force: true });
}
/**
* Clears the saved strip position flag so the strip defaults to its original position.
*/
_onResetStripPosition() {
try {
game.user?.setFlag?.('scrying-pool', 'stripState', null);
} catch (err) {
console.error('[ScryingPool] Failed to reset strip position:', err);
}
}
/**
* Opens the PresetSaveDialog for saving the current visibility matrix as a preset.
*/
+245 -4
View File
@@ -149,9 +149,26 @@ export class ScryingPoolStrip extends _AppBase {
this._healthCheckInterval = null;
/** @type {number|string|null} */
this._userConnectedHookId = null;
/** @type {string|null} */
this._focusedUserId = null;
/** @type {number|null} */
this._positionSaveTimer = null;
// Load saved position from user flags
this._loadPosition();
/** Bound keydown handler for Escape-to-exit-focus */
this._onDocumentKeydown = (e) => {
if (e.key === 'Escape' && this._focusedUserId) {
this._focusedUserId = null;
if (typeof this.render === 'function') {
this.render({ force: false });
}
}
};
if (typeof document?.addEventListener === 'function') {
document.addEventListener('keydown', this._onDocumentKeydown);
}
}
/** Loads saved window position from GM user flag. */
@@ -159,10 +176,15 @@ export class ScryingPoolStrip extends _AppBase {
try {
const saved = game.user?.getFlag?.('scrying-pool', 'stripState');
if (saved?.left != null && saved?.top != null) {
if (saved.left < 0 || saved.top < 0) return;
const screenW = typeof window !== 'undefined' ? (window.screen?.availWidth ?? Infinity) : Infinity;
const screenH = typeof window !== 'undefined' ? (window.screen?.availHeight ?? Infinity) : Infinity;
if (saved.left < screenW - 50 && saved.top < screenH - 50) {
if (this.options?.position) {
Object.assign(this.options.position, { left: saved.left, top: saved.top });
}
}
}
} catch (_e) { /* no-op in test environment */ }
}
@@ -195,6 +217,22 @@ export class ScryingPoolStrip extends _AppBase {
? userIds
: userIds.filter(id => id !== currentUserId);
// Re-order participants per GM's saved order (drag-and-drop)
try {
const savedOrder = game.user?.getFlag?.('scrying-pool', 'participantOrder');
if (Array.isArray(savedOrder) && savedOrder.length > 0) {
const orderMap = new Map(savedOrder.map((id, idx) => [id, idx]));
filteredUserIds.sort((a, b) => {
const ai = orderMap.get(a);
const bi = orderMap.get(b);
if (ai !== undefined && bi !== undefined) return ai - bi;
if (ai !== undefined) return -1;
if (bi !== undefined) return 1;
return 0;
});
}
} catch (_e) { /* no-op in test environment */ }
// Check if we have stream access for video replacement (full AV replacement mode)
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
@@ -207,8 +245,16 @@ export class ScryingPoolStrip extends _AppBase {
this._portraitFallbackHandler
);
// Mark focused participant for gold ring visual
participants.forEach(p => { p.isFocused = this._focusedUserId === p.userId; });
// Remove hidden participants from the strip — they are managed via the Directors Board.
const visibleParticipants = participants.filter(p => p.state !== 'hidden');
let visibleParticipants = participants.filter(p => p.state !== 'hidden');
// Spotlight: if a participant is focused, only show that one
if (this._focusedUserId) {
visibleParticipants = visibleParticipants.filter(p => p.userId === this._focusedUserId);
}
// Dock layout: world setting gives direction+canonical size; client override only applies if user toggled
const rawLayout = this._adapter.settings?.get?.('dockLayout');
@@ -228,6 +274,8 @@ export class ScryingPoolStrip extends _AppBase {
const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '150';
const isLarge = effectiveSize === 'md';
const effectiveWidth = isLarge ? widgetWidthMd : widgetWidthSm;
const isSpotlightActive = !!this._focusedUserId;
const widgetWidth = isSpotlightActive ? String(parseInt(effectiveWidth, 10) * 2) : effectiveWidth;
const isGM = this._adapter.users.isGM?.() ?? false;
@@ -248,7 +296,8 @@ export class ScryingPoolStrip extends _AppBase {
hasStreamAccess,
isGM,
// Story 5.2: Video widget width customization
widgetWidth: effectiveWidth,
isSpotlightActive,
widgetWidth,
// Tile shape
tileShape,
// Tile border
@@ -266,6 +315,11 @@ export class ScryingPoolStrip extends _AppBase {
el.querySelectorAll('[data-action="open-popover"]').forEach(btn => {
const userId = btn.dataset.userId;
btn.addEventListener('click', e => {
if (e.shiftKey) {
e.stopPropagation();
this._toggleFocus(userId);
return;
}
e.stopPropagation();
this._openPopover(userId, btn);
});
@@ -276,6 +330,33 @@ export class ScryingPoolStrip extends _AppBase {
});
});
// Drag-and-drop reordering of participants
// Remove old DnD listeners first to prevent duplicates on re-render
el.querySelectorAll('.sp-strip__participant-item').forEach(item => {
const existing = item._dndHandlers;
if (existing) {
item.removeEventListener('dragstart', existing.dragstart);
item.removeEventListener('dragover', existing.dragover);
item.removeEventListener('drop', existing.drop);
item.removeEventListener('dragend', existing.dragend);
item.removeEventListener('dragleave', existing.dragleave);
}
const handlers = {
dragstart: e => this._onDragStart(e),
dragover: e => this._onDragOver(e),
drop: e => this._onDrop(e),
dragend: e => this._onDragEnd(e),
dragleave: e => this._onDragLeave(e),
};
item._dndHandlers = handlers;
item.draggable = true;
item.addEventListener('dragstart', handlers.dragstart);
item.addEventListener('dragover', handlers.dragover);
item.addEventListener('drop', handlers.drop);
item.addEventListener('dragend', handlers.dragend);
item.addEventListener('dragleave', handlers.dragleave);
});
const toggle = el.querySelector('[data-action="toggle-expanded"]');
if (toggle) {
toggle.addEventListener('click', () => this._toggleExpanded());
@@ -298,8 +379,13 @@ export class ScryingPoolStrip extends _AppBase {
}
// Drag grip — custom drag implementation (Foundry v14 ApplicationV1 does not expose its drag handler)
// Double-click grip resets participant order to connection order
const grip = el.querySelector('[data-action="drag-grip"]');
if (grip) {
grip.addEventListener('dblclick', e => {
e.preventDefault();
this._resetParticipantOrder();
});
grip.addEventListener('mousedown', e => {
if (e.button !== 0) return;
e.preventDefault();
@@ -316,6 +402,7 @@ export class ScryingPoolStrip extends _AppBase {
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
this._savePosition();
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
@@ -352,12 +439,15 @@ export class ScryingPoolStrip extends _AppBase {
setVar('--sp-tile-border-active', bw > 0 ? '1' : '0');
// Sync the outer Application window width with the selected dock layout.
// Spotlight mode doubles the strip window to fit the enlarged tile.
if (typeof this.setPosition === 'function') {
const layout = context?.dockLayout ?? 'vertical-sm';
const n = context?.participants?.length ?? 0;
const width = this._computeStripWidth(layout, n);
const spotlightMultiplier = context?.isSpotlightActive ? 2 : 1;
const width = this._computeStripWidth(layout, n) * spotlightMultiplier;
const height = this._computeStripHeight(layout, n);
this.setPosition(height === 'auto' ? { width, height: 'auto' } : { width, height });
const adjustedHeight = height === 'auto' ? 'auto' : height * spotlightMultiplier;
this.setPosition(adjustedHeight === 'auto' ? { width, height: 'auto' } : { width, height: adjustedHeight });
}
}
@@ -445,6 +535,11 @@ export class ScryingPoolStrip extends _AppBase {
this._activePopover = null;
}
// Remove document-level keydown listener
if (typeof document?.removeEventListener === 'function') {
document.removeEventListener('keydown', this._onDocumentKeydown);
}
// Tear down stream monitoring
this._teardownStreamMonitoring();
@@ -708,6 +803,9 @@ export class ScryingPoolStrip extends _AppBase {
// Periodic health check every 15 seconds
this._healthCheckInterval = setInterval(() => this._checkVideoStreamHealth(), 15000);
// Auto-save strip position every 30 seconds
this._positionSaveTimer = setInterval(() => this._savePosition(), 30000);
// Watch for user connection changes to refresh streams
if (typeof Hooks !== 'undefined') {
this._userConnectedHookId = Hooks.on('userConnected', (userId, connected) => {
@@ -728,6 +826,12 @@ export class ScryingPoolStrip extends _AppBase {
this._healthCheckInterval = null;
}
// Clear position save timer
if (this._positionSaveTimer !== null) {
clearInterval(this._positionSaveTimer);
this._positionSaveTimer = null;
}
// Remove userConnected hook
if (this._userConnectedHookId !== null && typeof Hooks !== 'undefined') {
Hooks.off('userConnected', this._userConnectedHookId);
@@ -905,6 +1009,143 @@ export class ScryingPoolStrip extends _AppBase {
});
}
// ── Re-order participants (Drag & Drop) ─────────────────────────────────
/**
* Handles drag start on a participant item.
* @param {DragEvent} e
*/
_onDragStart(e) {
const item = e.target.closest('.sp-strip__participant-item');
if (!item) return;
const userId = item.querySelector('[data-user-id]')?.dataset?.userId;
if (!userId) return;
e.dataTransfer.setData('text/plain', userId);
e.dataTransfer.effectAllowed = 'move';
item.classList.add('sp-dragging');
}
/**
* Handles drag over on a participant item — shows drop indicator.
* @param {DragEvent} e
*/
_onDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
const item = e.target.closest('.sp-strip__participant-item');
if (!item) return;
item.classList.add('sp-drag-over');
}
/**
* Handles drop — reorders participants and persists the order.
* @param {DragEvent} e
*/
_onDrop(e) {
e.preventDefault();
const fromUserId = e.dataTransfer.getData('text/plain');
const toItem = e.target.closest('.sp-strip__participant-item');
if (!toItem || !fromUserId) return;
const toUserId = toItem.querySelector('[data-user-id]')?.dataset?.userId;
if (!toUserId || fromUserId === toUserId) return;
const el = this.element;
if (!el) return;
const items = [...el.querySelectorAll('.sp-strip__participant-item')];
const userIds = items.map(item => item.querySelector('[data-user-id]')?.dataset?.userId).filter(Boolean);
const fromIdx = userIds.indexOf(fromUserId);
const toIdx = userIds.indexOf(toUserId);
if (fromIdx === -1 || toIdx === -1) return;
userIds.splice(fromIdx, 1);
const adjustedTo = fromIdx < toIdx ? toIdx - 1 : toIdx;
userIds.splice(adjustedTo, 0, fromUserId);
this._saveParticipantOrder(userIds);
if (typeof this.render === 'function') {
this.render({ force: false });
}
}
/**
* Handles drag end — removes visual indicators.
*/
_onDragEnd() {
this.element?.querySelectorAll('.sp-strip__participant-item')?.forEach(item => {
item.classList.remove('sp-dragging', 'sp-drag-over');
});
}
/**
* Handles drag leave — removes drop indicator from the exited item.
* @param {DragEvent} e
*/
_onDragLeave(e) {
const item = e.target.closest('.sp-strip__participant-item');
if (item) item.classList.remove('sp-drag-over');
}
/**
* Persists participant order to GM user flag.
* @param {string[]} orderedUserIds
*/
_saveParticipantOrder(orderedUserIds) {
if (typeof game === 'undefined') return;
try {
game.user?.setFlag?.('scrying-pool', 'participantOrder', orderedUserIds);
} catch (_e) { /* no-op */ }
}
/**
* Clears saved order — next render uses connection order.
*/
_resetParticipantOrder() {
if (typeof game === 'undefined') return;
try {
game.user?.unsetFlag?.('scrying-pool', 'participantOrder');
} catch (_e) { /* no-op */ }
if (typeof this.render === 'function') {
this.render({ force: false });
}
}
// ── Spotlight / Focus ────────────────────────────────────────────────────
/**
* Toggles focus on a participant. If already focused, exits focus mode.
* @param {string} userId
*/
_toggleFocus(userId) {
if (this._focusedUserId === userId) {
this._focusedUserId = null;
} else {
this._focusedUserId = userId;
}
if (typeof this.render === 'function') {
this.render({ force: false });
}
}
// ── Auto-save position ───────────────────────────────────────────────────
/**
* Saves current strip position to GM user flag.
*/
_savePosition() {
if (typeof game === 'undefined') return;
try {
const pos = this.position;
if (pos?.left == null || pos?.top == null) return;
game.user?.setFlag?.('scrying-pool', 'stripState', {
left: pos.left,
top: pos.top,
width: pos.width ?? 240,
height: pos.height ?? 'auto',
savedAt: Date.now(),
});
} catch (_e) { /* no-op */ }
}
/**
* Shows a notification with i18n support and safe fallback for test environments.
* @param {'info'|'warn'|'error'} level
+32
View File
@@ -136,6 +136,17 @@
}
}
// ── Small tile shell sizing (all S layouts) ─────────────────────────────────
// Base .sp-avatar__shell is 60×60 fixed — override to follow --sp-widget-width
.scrying-pool.scrying-pool-strip.sp-layout-vertical-sm,
.scrying-pool.scrying-pool-strip.sp-layout-horizontal-sm,
.scrying-pool.scrying-pool-strip.sp-layout-mosaic-sm {
.sp-participant-avatar .sp-avatar__shell {
width: calc(var(--sp-widget-width) - 12px);
height: calc(var(--sp-widget-width) - 12px);
}
}
// ── Toolbar: grip + toggle + DB + close on one line ─────────────────────────
// All chrome lives here; hidden at rest, revealed on strip hover.
.sp-strip__toolbar {
@@ -501,6 +512,27 @@
}
}
// ============================================================
// Spotlight focus state — gold ring (uses urgency-director token)
// ============================================================
.sp-participant-avatar.sp-state-focused {
.sp-avatar__shell::after {
box-shadow: inset 0 0 0 2px var(--sp-urgency-director);
}
}
// ============================================================
// Drag-and-drop reorder feedback
// ============================================================
.sp-strip__participant-item.sp-dragging {
opacity: 0.3;
}
.sp-strip__participant-item.sp-drag-over {
box-shadow: inset 0 0 0 2px var(--sp-urgency-director);
border-radius: 4px;
}
// ============================================================
// StateRing animations — gated under no-preference (AC-16)
// ============================================================
+6
View File
@@ -689,6 +689,12 @@
.scrying-pool.scrying-pool-strip.sp-layout-mosaic-md .sp-participant-avatar::after {
display: none;
}
.scrying-pool.scrying-pool-strip.sp-layout-vertical-sm .sp-participant-avatar .sp-avatar__shell,
.scrying-pool.scrying-pool-strip.sp-layout-horizontal-sm .sp-participant-avatar .sp-avatar__shell,
.scrying-pool.scrying-pool-strip.sp-layout-mosaic-sm .sp-participant-avatar .sp-avatar__shell {
width: calc(var(--sp-widget-width) - 12px);
height: calc(var(--sp-widget-width) - 12px);
}
.sp-strip__toolbar {
display: flex;
flex-direction: row;
+9 -2
View File
@@ -120,7 +120,7 @@
<div class="directors-board__widget-width-row">
<span>{{localize "scrying-pool.directorsBoard.widgetWidth.small"}}</span>
<select class="directors-board__widget-width-select" data-action="set-widget-width-sm" data-selected="{{widgetWidthSm}}">
{{#each widthOptions}}
{{#each smWidthOptions}}
<option value="{{this.value}}">{{this.label}}</option>
{{/each}}
</select>
@@ -128,7 +128,7 @@
<div class="directors-board__widget-width-row">
<span>{{localize "scrying-pool.directorsBoard.widgetWidth.large"}}</span>
<select class="directors-board__widget-width-select" data-action="set-widget-width-md" data-selected="{{widgetWidthMd}}">
{{#each widthOptions}}
{{#each mdWidthOptions}}
<option value="{{this.value}}">{{this.label}}</option>
{{/each}}
</select>
@@ -137,6 +137,13 @@
</div>
<footer class="directors-board__footer">
<div class="directors-board__footer-group directors-board__footer-group--strip">
<button type="button" class="directors-board__footer-btn directors-board__footer-btn--secondary" data-action="reset-strip-position"
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.resetStrip"}}">
<i class="fas fa-undo-alt" aria-hidden="true"></i>
{{localize "scrying-pool.directorsBoard.footer.resetStrip"}}
</button>
</div>
<div class="directors-board__footer-group directors-board__footer-group--presets">
<button type="button" class="directors-board__footer-btn" data-action="save-preset"
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.savePreset"}}">
+1 -1
View File
@@ -51,7 +51,7 @@
{{#each participants}}
<li class="sp-strip__participant-item" role="listitem">
{{!-- ParticipantAvatar (44×44px container) --}}
<button class="sp-participant-avatar sp-state-{{state}}{{#if hasPendingOp}} sp-state-pending{{/if}} sp-shape-{{../tileShape}}"
<button class="sp-participant-avatar sp-state-{{state}}{{#if hasPendingOp}} sp-state-pending{{/if}}{{#if isFocused}} sp-state-focused{{/if}} sp-shape-{{../tileShape}}"
data-user-id="{{userId}}"
data-action="open-popover"
role="button"