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
This commit is contained in:
@@ -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"
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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
@@ -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'
|
workflowType: 'architecture'
|
||||||
project_name: 'video-view-manager'
|
project_name: 'video-view-manager'
|
||||||
user_name: 'Morr'
|
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)
|
# 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` +
|
Story 0 scaffold — `module.json` + `tsconfig.json` + `vitest.config.js` + `.eslintrc.js` +
|
||||||
`scripts/package.mjs` + `src/contracts/` (with validators + frozen fixtures) +
|
`scripts/package.mjs` + `src/contracts/` (with validators + frozen fixtures) +
|
||||||
`.gitea/workflows/ci.yml` — all Story 0 ACs green before any Story 1 code.
|
`.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
@@ -79,7 +79,8 @@
|
|||||||
"avModeEnable": "Enable A/V",
|
"avModeEnable": "Enable A/V",
|
||||||
"avModeDisable": "Disable A/V",
|
"avModeDisable": "Disable A/V",
|
||||||
"avConfig": "A/V Settings…",
|
"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": {
|
"bulk": {
|
||||||
"showAll": "Show All",
|
"showAll": "Show All",
|
||||||
|
|||||||
+2
-1
@@ -79,7 +79,8 @@
|
|||||||
"avModeEnable": "Activer A/V",
|
"avModeEnable": "Activer A/V",
|
||||||
"avModeDisable": "Désactiver A/V",
|
"avModeDisable": "Désactiver A/V",
|
||||||
"avConfig": "Paramètres 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": {
|
"bulk": {
|
||||||
"showAll": "Tout afficher",
|
"showAll": "Tout afficher",
|
||||||
|
|||||||
@@ -382,13 +382,29 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
// Defaults match the settings registration in module.js
|
// Defaults match the settings registration in module.js
|
||||||
const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '83';
|
const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '83';
|
||||||
const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '150';
|
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: '60', label: '60px' },
|
||||||
{ value: '80', label: '80px' },
|
{ value: '80', label: '80px' },
|
||||||
{ value: '100', label: '100px' },
|
{ value: '100', label: '100px' },
|
||||||
{ value: '120', label: '120px' },
|
{ value: '120', label: '120px' },
|
||||||
{ value: '150', label: '150px' },
|
{ value: '150', label: '150px' },
|
||||||
{ value: '200', label: '200px' },
|
{ value: '200', label: '200px' },
|
||||||
|
{ value: '250', label: '250px' },
|
||||||
|
{ value: '300', label: '300px' },
|
||||||
|
{ value: '350', label: '350px' },
|
||||||
|
{ value: '400', label: '400px' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Tile shape selector
|
// Tile shape selector
|
||||||
@@ -433,7 +449,8 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
tileBorderColor: currentTileBorderColor,
|
tileBorderColor: currentTileBorderColor,
|
||||||
tileBorderWidths: TILE_BORDER_WIDTHS,
|
tileBorderWidths: TILE_BORDER_WIDTHS,
|
||||||
// Story 5.2: Video widget width customization
|
// Story 5.2: Video widget width customization
|
||||||
widthOptions: WIDTH_OPTIONS,
|
smWidthOptions: SM_WIDTH_OPTIONS,
|
||||||
|
mdWidthOptions: MD_WIDTH_OPTIONS,
|
||||||
widgetWidthSm,
|
widgetWidthSm,
|
||||||
widgetWidthMd,
|
widgetWidthMd,
|
||||||
};
|
};
|
||||||
@@ -496,6 +513,7 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
case 'set-dock-layout': this._onSetDockLayout(btn.dataset.layout); break;
|
case 'set-dock-layout': this._onSetDockLayout(btn.dataset.layout); break;
|
||||||
case 'set-tile-shape': this._onSetTileShape(btn.dataset.shape); 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 'set-tile-border-width': this._onSetTileBorderWidth(parseInt(btn.dataset.width, 10)); break;
|
||||||
|
case 'reset-strip-position': this._onResetStripPosition(); break;
|
||||||
case 'close': this.close(); break;
|
case 'close': this.close(); break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -840,6 +858,17 @@ export class DirectorsBoard extends _AppBase {
|
|||||||
if (this.rendered) this.render({ force: true });
|
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.
|
* Opens the PresetSaveDialog for saving the current visibility matrix as a preset.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -149,9 +149,26 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
this._healthCheckInterval = null;
|
this._healthCheckInterval = null;
|
||||||
/** @type {number|string|null} */
|
/** @type {number|string|null} */
|
||||||
this._userConnectedHookId = null;
|
this._userConnectedHookId = null;
|
||||||
|
/** @type {string|null} */
|
||||||
|
this._focusedUserId = null;
|
||||||
|
/** @type {number|null} */
|
||||||
|
this._positionSaveTimer = null;
|
||||||
|
|
||||||
// Load saved position from user flags
|
// Load saved position from user flags
|
||||||
this._loadPosition();
|
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. */
|
/** Loads saved window position from GM user flag. */
|
||||||
@@ -159,10 +176,15 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
try {
|
try {
|
||||||
const saved = game.user?.getFlag?.('scrying-pool', 'stripState');
|
const saved = game.user?.getFlag?.('scrying-pool', 'stripState');
|
||||||
if (saved?.left != null && saved?.top != null) {
|
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) {
|
if (this.options?.position) {
|
||||||
Object.assign(this.options.position, { left: saved.left, top: saved.top });
|
Object.assign(this.options.position, { left: saved.left, top: saved.top });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (_e) { /* no-op in test environment */ }
|
} catch (_e) { /* no-op in test environment */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +217,22 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
? userIds
|
? userIds
|
||||||
: userIds.filter(id => id !== currentUserId);
|
: 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)
|
// Check if we have stream access for video replacement (full AV replacement mode)
|
||||||
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
|
const hasStreamAccess = this._adapter.webrtc?.getMediaStreamForUser !== undefined;
|
||||||
|
|
||||||
@@ -207,8 +245,16 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
this._portraitFallbackHandler
|
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.
|
// 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
|
// Dock layout: world setting gives direction+canonical size; client override only applies if user toggled
|
||||||
const rawLayout = this._adapter.settings?.get?.('dockLayout');
|
const rawLayout = this._adapter.settings?.get?.('dockLayout');
|
||||||
@@ -266,6 +312,11 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
el.querySelectorAll('[data-action="open-popover"]').forEach(btn => {
|
el.querySelectorAll('[data-action="open-popover"]').forEach(btn => {
|
||||||
const userId = btn.dataset.userId;
|
const userId = btn.dataset.userId;
|
||||||
btn.addEventListener('click', e => {
|
btn.addEventListener('click', e => {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this._toggleFocus(userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
this._openPopover(userId, btn);
|
this._openPopover(userId, btn);
|
||||||
});
|
});
|
||||||
@@ -276,6 +327,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"]');
|
const toggle = el.querySelector('[data-action="toggle-expanded"]');
|
||||||
if (toggle) {
|
if (toggle) {
|
||||||
toggle.addEventListener('click', () => this._toggleExpanded());
|
toggle.addEventListener('click', () => this._toggleExpanded());
|
||||||
@@ -298,8 +376,13 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drag grip — custom drag implementation (Foundry v14 ApplicationV1 does not expose its drag handler)
|
// 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"]');
|
const grip = el.querySelector('[data-action="drag-grip"]');
|
||||||
if (grip) {
|
if (grip) {
|
||||||
|
grip.addEventListener('dblclick', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
this._resetParticipantOrder();
|
||||||
|
});
|
||||||
grip.addEventListener('mousedown', e => {
|
grip.addEventListener('mousedown', e => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -316,6 +399,7 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
const onUp = () => {
|
const onUp = () => {
|
||||||
document.removeEventListener('mousemove', onMove);
|
document.removeEventListener('mousemove', onMove);
|
||||||
document.removeEventListener('mouseup', onUp);
|
document.removeEventListener('mouseup', onUp);
|
||||||
|
this._savePosition();
|
||||||
};
|
};
|
||||||
document.addEventListener('mousemove', onMove);
|
document.addEventListener('mousemove', onMove);
|
||||||
document.addEventListener('mouseup', onUp);
|
document.addEventListener('mouseup', onUp);
|
||||||
@@ -445,6 +529,11 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
this._activePopover = null;
|
this._activePopover = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove document-level keydown listener
|
||||||
|
if (typeof document?.removeEventListener === 'function') {
|
||||||
|
document.removeEventListener('keydown', this._onDocumentKeydown);
|
||||||
|
}
|
||||||
|
|
||||||
// Tear down stream monitoring
|
// Tear down stream monitoring
|
||||||
this._teardownStreamMonitoring();
|
this._teardownStreamMonitoring();
|
||||||
|
|
||||||
@@ -708,6 +797,9 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
// Periodic health check every 15 seconds
|
// Periodic health check every 15 seconds
|
||||||
this._healthCheckInterval = setInterval(() => this._checkVideoStreamHealth(), 15000);
|
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
|
// Watch for user connection changes to refresh streams
|
||||||
if (typeof Hooks !== 'undefined') {
|
if (typeof Hooks !== 'undefined') {
|
||||||
this._userConnectedHookId = Hooks.on('userConnected', (userId, connected) => {
|
this._userConnectedHookId = Hooks.on('userConnected', (userId, connected) => {
|
||||||
@@ -728,6 +820,12 @@ export class ScryingPoolStrip extends _AppBase {
|
|||||||
this._healthCheckInterval = null;
|
this._healthCheckInterval = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear position save timer
|
||||||
|
if (this._positionSaveTimer !== null) {
|
||||||
|
clearInterval(this._positionSaveTimer);
|
||||||
|
this._positionSaveTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove userConnected hook
|
// Remove userConnected hook
|
||||||
if (this._userConnectedHookId !== null && typeof Hooks !== 'undefined') {
|
if (this._userConnectedHookId !== null && typeof Hooks !== 'undefined') {
|
||||||
Hooks.off('userConnected', this._userConnectedHookId);
|
Hooks.off('userConnected', this._userConnectedHookId);
|
||||||
@@ -905,6 +1003,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.
|
* Shows a notification with i18n support and safe fallback for test environments.
|
||||||
* @param {'info'|'warn'|'error'} level
|
* @param {'info'|'warn'|'error'} level
|
||||||
|
|||||||
@@ -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 ─────────────────────────
|
// ── Toolbar: grip + toggle + DB + close on one line ─────────────────────────
|
||||||
// All chrome lives here; hidden at rest, revealed on strip hover.
|
// All chrome lives here; hidden at rest, revealed on strip hover.
|
||||||
.sp-strip__toolbar {
|
.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)
|
// StateRing animations — gated under no-preference (AC-16)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -689,6 +689,12 @@
|
|||||||
.scrying-pool.scrying-pool-strip.sp-layout-mosaic-md .sp-participant-avatar::after {
|
.scrying-pool.scrying-pool-strip.sp-layout-mosaic-md .sp-participant-avatar::after {
|
||||||
display: none;
|
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 {
|
.sp-strip__toolbar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|||||||
@@ -120,7 +120,7 @@
|
|||||||
<div class="directors-board__widget-width-row">
|
<div class="directors-board__widget-width-row">
|
||||||
<span>{{localize "scrying-pool.directorsBoard.widgetWidth.small"}}</span>
|
<span>{{localize "scrying-pool.directorsBoard.widgetWidth.small"}}</span>
|
||||||
<select class="directors-board__widget-width-select" data-action="set-widget-width-sm" data-selected="{{widgetWidthSm}}">
|
<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>
|
<option value="{{this.value}}">{{this.label}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
<div class="directors-board__widget-width-row">
|
<div class="directors-board__widget-width-row">
|
||||||
<span>{{localize "scrying-pool.directorsBoard.widgetWidth.large"}}</span>
|
<span>{{localize "scrying-pool.directorsBoard.widgetWidth.large"}}</span>
|
||||||
<select class="directors-board__widget-width-select" data-action="set-widget-width-md" data-selected="{{widgetWidthMd}}">
|
<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>
|
<option value="{{this.value}}">{{this.label}}</option>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</select>
|
</select>
|
||||||
@@ -137,6 +137,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer class="directors-board__footer">
|
<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">
|
<div class="directors-board__footer-group directors-board__footer-group--presets">
|
||||||
<button type="button" class="directors-board__footer-btn" data-action="save-preset"
|
<button type="button" class="directors-board__footer-btn" data-action="save-preset"
|
||||||
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.savePreset"}}">
|
data-tooltip="{{localize "scrying-pool.directorsBoard.footer.savePreset"}}">
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
{{#each participants}}
|
{{#each participants}}
|
||||||
<li class="sp-strip__participant-item" role="listitem">
|
<li class="sp-strip__participant-item" role="listitem">
|
||||||
{{!-- ParticipantAvatar (44×44px container) --}}
|
{{!-- 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-user-id="{{userId}}"
|
||||||
data-action="open-popover"
|
data-action="open-popover"
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
Reference in New Issue
Block a user