50 Commits

Author SHA1 Message Date
gitea
1dc4940d8c [no ci] prepare new Version 2025-02-11 23:06:46 +00:00
gitea
1cefb117fe [no ci] release 1.0.2 2025-02-11 23:06:40 +00:00
ba48da80ed Merge pull request 'upload-to-packages' (#24) from upload-to-packages into main
All checks were successful
CI / deploy (push) Successful in 4m55s
Reviewed-on: #24
2025-02-12 00:01:51 +01:00
4fd83f48cd adds Version to index
All checks were successful
CI / deploy (push) Successful in 4m22s
CI / deploy (pull_request) Successful in 4m27s
2025-02-11 23:52:08 +01:00
24acc0b942 push to packages
All checks were successful
CI / deploy (push) Successful in 4m25s
CI / deploy (pull_request) Successful in 4m26s
2025-02-11 23:34:46 +01:00
cab7ec34d2 push to packages
Some checks failed
CI / deploy (push) Failing after 6s
2025-02-11 23:01:24 +01:00
gitea
c8cd05f035 [no ci] prepare new Version 2025-02-11 21:56:44 +00:00
gitea
3e35dfb9ba [no ci] release 1.0.1 2025-02-11 21:56:38 +00:00
f465e1c101 fix SNAPSHOTS
All checks were successful
CI / deploy (push) Successful in 5m5s
2025-02-11 22:51:33 +01:00
gitea
2f4cc2de8b [no ci] release 1.0.1 2025-02-11 21:40:20 +00:00
b977996689 uses mvn:versions instead of jgitver
All checks were successful
CI / deploy (push) Successful in 4m46s
2025-02-11 22:35:30 +01:00
1544776ecc fix tag
Some checks failed
CI / deploy (push) Failing after 4m59s
2025-02-11 22:00:44 +01:00
468d9660c4 Merge pull request 'jgitver' (#23) from jgitver into main
Some checks failed
CI / deploy (push) Failing after 4m48s
Reviewed-on: #23
2025-02-11 21:42:15 +01:00
a43f5b30f7 remove SNAPSHOT
All checks were successful
CI / deploy (push) Successful in 4m53s
CI / deploy (pull_request) Successful in 4m50s
2025-02-11 21:31:28 +01:00
4ebd9a7262 fetch tags
All checks were successful
CI / deploy (push) Successful in 6m58s
2025-02-11 20:42:22 +01:00
ad4179a517 Merge pull request 'adds jgitver' (#22) from jgitver into main
All checks were successful
CI / deploy (push) Successful in 4m35s
Reviewed-on: #22
2025-02-11 20:36:32 +01:00
Arindy
f7705e15e5 adds jgitver
All checks were successful
CI / deploy (push) Successful in 4m30s
CI / deploy (pull_request) Successful in 4m27s
2025-02-11 20:24:09 +01:00
0299f89901 Merge pull request 'copies links to clipboard' (#20) from copy-to-clipboard into main
All checks were successful
CI / deploy (push) Successful in 4m28s
Reviewed-on: #20
2025-02-11 11:24:44 +01:00
Arindy
7f7a27a178 copies links to clipboard
All checks were successful
CI / deploy (push) Successful in 4m22s
CI / deploy (pull_request) Successful in 4m18s
2025-02-11 11:15:37 +01:00
d9caee420a Merge pull request 'overwrites dice-overlay sse, so that only the last instance rolls the dice' (#19) from remove-sse-on-close into main
All checks were successful
CI / deploy (push) Successful in 4m21s
Reviewed-on: #19
2025-02-10 21:39:03 +01:00
Arindy
f9893e2a17 overwrites dice-overlay sse, so that only the last instance rolls the dice
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m20s
2025-02-10 21:30:11 +01:00
fdd06978d8 Merge pull request 'show-all-overlays' (#18) from show-all-overlays into main
All checks were successful
CI / deploy (push) Successful in 4m24s
Reviewed-on: #18
2025-02-10 20:21:20 +01:00
Arindy
13f206ae97 splits gm-view from normal-view
All checks were successful
CI / deploy (push) Successful in 4m16s
CI / deploy (pull_request) Successful in 4m20s
2025-02-10 20:08:29 +01:00
Arindy
827b74ebf1 moves overlay notice 2025-02-10 18:41:13 +01:00
80d331cdd3 Merge pull request 'adds buttons for simple commands' (#16) from adds-easy-way-to-roll into main
All checks were successful
CI / deploy (push) Successful in 4m18s
Reviewed-on: #16
2025-02-10 18:39:00 +01:00
Arindy
0e3d88cd4a fixes overlay tooltip
All checks were successful
CI / deploy (push) Successful in 4m14s
CI / deploy (pull_request) Successful in 4m17s
2025-02-10 18:26:44 +01:00
Arindy
cb077b50de adds how-to
All checks were successful
CI / deploy (push) Successful in 4m21s
CI / deploy (pull_request) Successful in 4m19s
2025-02-10 18:19:54 +01:00
Arindy
9def3240b2 styles buttons
All checks were successful
CI / deploy (push) Successful in 4m29s
CI / deploy (pull_request) Successful in 4m22s
2025-02-10 18:01:57 +01:00
Arindy
13eacc886d adds buttons for simple commands
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m17s
2025-02-10 17:46:10 +01:00
af41ff5d78 Merge pull request 'adds a dice preview' (#15) from fix-color-picker into main
All checks were successful
CI / deploy (push) Successful in 4m24s
Reviewed-on: #15
2025-02-10 17:20:26 +01:00
Arindy
153b25e53f rounds elements
All checks were successful
CI / deploy (push) Successful in 4m23s
CI / deploy (pull_request) Successful in 4m13s
2025-02-10 17:04:57 +01:00
Arindy
2e6267e7b4 adds button icons
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 6m22s
2025-02-10 16:58:26 +01:00
Arindy
821e939b3d adds a dice preview
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m15s
2025-02-10 16:40:01 +01:00
7ee16d5f3e Merge pull request 'enter in color picker now sets the color' (#14) from fix-color-picker into main
All checks were successful
CI / deploy (push) Successful in 4m22s
Reviewed-on: #14
2025-02-10 16:05:24 +01:00
Arindy
4643ec107e enter in color picker now sets the color
All checks were successful
CI / deploy (push) Successful in 4m20s
CI / deploy (pull_request) Successful in 4m16s
2025-02-10 15:53:17 +01:00
36b7c290da Merge pull request 'implements timed clearing of dice' (#11) from make-dice-clear into main
All checks were successful
CI / deploy (push) Successful in 4m23s
Reviewed-on: #11
2025-02-10 15:25:06 +01:00
Arindy
b69bdb72ee implements timed clearing of dice
All checks were successful
CI / deploy (push) Successful in 4m20s
CI / deploy (pull_request) Successful in 4m17s
2025-02-10 15:10:03 +01:00
1518783090 Merge pull request 'adds Readme & adjusts Layout of index' (#10) from readme-and-overlay-adjustements into main
All checks were successful
CI / deploy (push) Successful in 4m21s
Reviewed-on: #10
2025-02-10 14:06:46 +01:00
Arindy
39d5107ea4 adds Readme & adjusts Layout of index
All checks were successful
CI / deploy (push) Successful in 4m19s
CI / deploy (pull_request) Successful in 4m22s
2025-02-10 13:57:18 +01:00
bdbc120e61 Merge pull request 'update-styling' (#4) from update-styling into main
All checks were successful
CI / deploy (push) Successful in 4m24s
Reviewed-on: #4
2025-02-10 03:35:38 +01:00
Arindy
9b25a69636 introduces results for individual players
All checks were successful
CI / deploy (pull_request) Successful in 4m23s
CI / deploy (push) Successful in 4m16s
2025-02-10 03:24:20 +01:00
Arindy
7f40d85022 adds scale queryparam to overlay
All checks were successful
CI / deploy (push) Successful in 4m1s
CI / deploy (pull_request) Successful in 3m59s
2025-02-10 02:42:44 +01:00
Arindy
70f0cc99fc fixes concurrent rolling of same user with different name
All checks were successful
CI / deploy (push) Successful in 4m1s
CI / deploy (pull_request) Successful in 4m1s
2025-02-10 02:26:54 +01:00
Arindy
f616726aae saves customizing to name
All checks were successful
CI / deploy (push) Successful in 4m18s
CI / deploy (pull_request) Successful in 4m20s
2025-02-10 02:04:58 +01:00
Arindy
d548ae2ea0 moves latest result to top
All checks were successful
CI / deploy (push) Successful in 4m14s
2025-02-10 01:51:18 +01:00
cc370fc3a0 Merge pull request 'saves dice config to localstorage' (#3) from update-styling into main
All checks were successful
CI / deploy (push) Successful in 4m22s
Reviewed-on: #3
2025-02-10 01:20:52 +01:00
Arindy
3a3677c17e refines dark-mode
All checks were successful
CI / deploy (push) Successful in 4m17s
CI / deploy (pull_request) Successful in 4m15s
2025-02-10 01:07:44 +01:00
Arindy
df5d072636 removes frame-border from results on index
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 4m18s
2025-02-10 01:02:10 +01:00
Arindy
9feee85cd1 switches to dark-mode
Some checks failed
CI / deploy (push) Has been cancelled
CI / deploy (pull_request) Successful in 4m42s
2025-02-10 00:56:25 +01:00
Arindy
ce1f43fe98 adds color to name in results
All checks were successful
CI / deploy (push) Successful in 4m17s
CI / deploy (pull_request) Successful in 4m16s
2025-02-10 00:45:38 +01:00
15 changed files with 883 additions and 206 deletions

BIN
.github/media/preview.gif vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -11,6 +11,8 @@ jobs:
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup JDK - name: Setup JDK
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@@ -18,8 +20,14 @@ jobs:
distribution: 'zulu' distribution: 'zulu'
java-version: '21' java-version: '21'
- name: new Version
if: github.ref_name == 'main'
run: |
./mvnw -B --no-transfer-progress versions:set -DremoveSnapshot
- name: Build Runner - name: Build Runner
run: ./mvnw --no-transfer-progress clean verify -Pnative -Dquarkus.native.remote-container-build=true run: |
./mvnw -B --no-transfer-progress clean verify -Pnative -Dquarkus.native.remote-container-build=true -Djgitver.skip=true
- name: Add coverage to PR - name: Add coverage to PR
id: jacoco id: jacoco
@@ -31,10 +39,32 @@ jobs:
min-coverage-changed-files: 60 min-coverage-changed-files: 60
title: ${{ env.REPO }} Coverage title: ${{ env.REPO }} Coverage
- id: version
name: Version
run: echo "VERSION=$(./mvnw help:evaluate -Dexpression=project.version -q -DforceStdout)" >> ${GITHUB_OUTPUT}
- name: Build Container - name: Build Container
if: github.ref_name == 'main' if: github.ref_name == 'main'
run: docker build -f src/main/docker/Dockerfile.native-micro -t ${{ env.REPO }} . run: |
echo ${{secrets.PACKAGES_TOKEN}} | docker login --username ${{ secrets.PACKAGES_USER }} --password-stdin git.arindy.de
docker build -f src/main/docker/Dockerfile.native-micro -t git.arindy.de/arindy/dice-tower:latest -t git.arindy.de/arindy/dice-tower:${{ steps.version.outputs.VERSION }} .
docker push git.arindy.de/arindy/dice-tower:${{ steps.version.outputs.VERSION }}
docker push git.arindy.de/arindy/dice-tower:latest
- name: Deploy - name: Deploy
if: github.ref_name == 'main' if: github.ref_name == 'main'
run: "docker compose up -d" run: "docker compose up -d"
- name: create tag
if: github.ref_name == 'main'
run: |
git config user.email "ci@git.arindy.de"
git config user.name "gitea"
git add ./pom.xml
git commit -m "[no ci] release ${{ steps.version.outputs.VERSION }}"
./mvnw -B --no-transfer-progress clean validate -Pnew-snapshot
git add ./pom.xml
git commit -m "[no ci] prepare new Version"
git tag ${{ steps.version.outputs.VERSION }} -m "release ${{ steps.version.outputs.VERSION }}"
git push origin main
git push origin ${{ steps.version.outputs.VERSION }}

View File

@@ -1,2 +1,84 @@
# dice-tower <h1 align="center">
Dice-Tower
</h1>
<h4 align="center">... they see them rolling ...</h4>
<p align="center">
<a href="#key-features">Key Features</a> •
<a href="#start-container">Start Container</a> •
<a href="#how-to-build-from-scratch">How To Build from scratch</a> •
<a href="#credits">Credits</a> •
<a href="#license">License</a>
</p>
![preview](.github/media/preview.gif)
---
## Key Features
* Connect to a room with others
* Configure your dice (theme and color)
* Roll any dice
* See the dice roll (Can be used as a Browser Source in OBS)
* Watch roll results (also available as Browser Source in OBS)
---
## Start Container
You can start dice-tower with docker compose
Create a `compose.yml`-File with following content:
```yaml
services:
api:
container_name: dice-tower
image: git.arindy.de/arindy/dice-tower:latest
restart: always
ports:
- "8080:8080"
```
Run the container with:
```bash
docker compose up -d
```
---
## How To Build from scratch
To clone and run this application, you'll need `git`, `java21` and `docker`.
```bash
# Clone this repository
$ git clone https://git.arindy.de/arindy/dice-tower.git
# Go into the repository
$ cd dice-tower
# Build the binary
$ ./mvnw clean verify -Pnative
# Build the container
$ docker build -f src/main/docker/Dockerfile.native-micro -t dice-tower .
# run the container in the background
$ docker run --network host -d dice-tower
# Visit the dice-tower in your browser on http://localhost:8080
```
---
## Credits
This software uses the following open source packages:
- [3D-Dice/dice-box](https://github.com/3d-dice/dice-box)
- [3D-Dice/dice-themes](https://github.com/3d-dice/dice-themes)
- [W3.CSS Color Themes](https://www.w3schools.com/w3css/w3css_color_themes.asp)
## License
GPL-3

View File

@@ -1,7 +1,7 @@
services: services:
api: api:
container_name: dice-tower container_name: dice-tower
image: dice-tower image: git.arindy.de/arindy/dice-tower:latest
restart: always restart: always
networks: networks:
- reverse_proxy - reverse_proxy

46
pom.xml
View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.arindy</groupId> <groupId>de.arindy</groupId>
<artifactId>dice-tower</artifactId> <artifactId>dice-tower</artifactId>
<version>1.0.0-SNAPSHOT</version> <version>1.0.3-SNAPSHOT</version>
<properties> <properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version> <compiler-plugin.version>3.13.0</compiler-plugin.version>
@@ -21,6 +21,13 @@
<rxjava-version>2.2.8</rxjava-version> <rxjava-version>2.2.8</rxjava-version>
</properties> </properties>
<scm>
<connection>scm:git:https://git.arindy.de/arindy/dice-tower.git</connection>
<developerConnection>scm:git:https://git.arindy.de/arindy/dice-tower.git</developerConnection>
<url>https://git.arindy.de/arindy/dice-tower</url>
<tag>HEAD</tag>
</scm>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -267,5 +274,42 @@
<quarkus.native.enabled>true</quarkus.native.enabled> <quarkus.native.enabled>true</quarkus.native.enabled>
</properties> </properties>
</profile> </profile>
<profile>
<id>new-snapshot</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>next-version</id>
<goals>
<goal>parse-version</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<version>2.18.0</version>
<executions>
<execution>
<id>release-version</id>
<phase>validate</phase>
<goals>
<goal>set</goal>
</goals>
<configuration>
<newVersion>${parsedVersion.majorVersion}.${parsedVersion.minorVersion}.${parsedVersion.nextIncrementalVersion}-SNAPSHOT</newVersion>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles> </profiles>
</project> </project>

View File

@@ -26,9 +26,6 @@ final class DiceResource(@Context val sse: Sse) {
@POST @POST
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
fun parseCommand(@PathParam("id") id: String, data: RollPayload) { fun parseCommand(@PathParam("id") id: String, data: RollPayload) {
if (!sseBroadcasters.containsKey(id)) {
sseBroadcasters[id] = sse.newBroadcaster()
}
data.roll = data.command.split(" ", "&", "and").filter { it.isNotEmpty() }.map { it.trim() }.toTypedArray<String>() data.roll = data.command.split(" ", "&", "and").filter { it.isNotEmpty() }.map { it.trim() }.toTypedArray<String>()
data.room = id.split(":")[0] data.room = id.split(":")[0]
data.user = id.split(":")[1] data.user = id.split(":")[1]
@@ -37,10 +34,21 @@ final class DiceResource(@Context val sse: Sse) {
.mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build() .mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build()
) )
if (data.roll.all { it.trim().isNotEmpty() }) { if (data.roll.all { it.trim().isNotEmpty() }) {
results(data.room!!, Result(data.name, data.user!!)) results(data.room!!, Result(data.name, data.user!!, data.themeColor))
} }
} }
@POST
@Path("/register")
@Consumes(MediaType.APPLICATION_JSON)
fun register(@PathParam("id") id: String, data: Any) {
println("id = [${id}], data = [${data}]")
sseBroadcasters["register:$id"]?.broadcast(
eventBuilder.id((UUID.randomUUID()).toString())
.mediaType(MediaType.APPLICATION_JSON_TYPE).data(data).build())
}
@POST @POST
@Path("/results") @Path("/results")
@Consumes(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON)
@@ -55,9 +63,7 @@ final class DiceResource(@Context val sse: Sse) {
@Path("/stream") @Path("/stream")
@Produces(MediaType.SERVER_SENT_EVENTS) @Produces(MediaType.SERVER_SENT_EVENTS)
fun stream(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) { fun stream(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
if (!sseBroadcasters.containsKey(id)) {
sseBroadcasters[id] = sse.newBroadcaster() sseBroadcasters[id] = sse.newBroadcaster()
}
sseBroadcasters[id]?.register(sseEventSink) sseBroadcasters[id]?.register(sseEventSink)
} }
@@ -71,7 +77,17 @@ final class DiceResource(@Context val sse: Sse) {
sseBroadcasters[id]?.register(sseEventSink) sseBroadcasters[id]?.register(sseEventSink)
} }
@GET
@Path("/users")
@Produces(MediaType.SERVER_SENT_EVENTS)
fun users(@PathParam("id") id: String, @Context sseEventSink: SseEventSink) {
if (!sseBroadcasters.containsKey("register:$id")) {
sseBroadcasters["register:$id"] = sse.newBroadcaster()
}
sseBroadcasters["register:$id"]?.register(sseEventSink)
}
@RegisterForReflection @RegisterForReflection
data class Result(val name: String, val user: String) {} data class Result(val name: String, val user: String, val themeColor: String)
} }

View File

@@ -0,0 +1,35 @@
package de.arindy.dicetower
import io.quarkus.qute.TemplateInstance
import io.quarkus.runtime.annotations.RegisterForReflection
import jakarta.enterprise.context.ApplicationScoped
import jakarta.ws.rs.Consumes
import jakarta.ws.rs.GET
import jakarta.ws.rs.POST
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.QueryParam
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.sse.OutboundSseEvent
import jakarta.ws.rs.sse.Sse
import jakarta.ws.rs.sse.SseBroadcaster
import jakarta.ws.rs.sse.SseEventSink
import org.eclipse.microprofile.config.inject.ConfigProperty
import java.util.UUID
@Path("/")
@ApplicationScoped
final class IndexResource() {
@ConfigProperty(name = "quarkus.application.version")
private lateinit var version: String
@GET
@Produces(MediaType.TEXT_HTML)
fun get(): TemplateInstance {
return Templates.index(version)
}
}

View File

@@ -5,6 +5,7 @@ import jakarta.ws.rs.GET
import jakarta.ws.rs.Path import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces import jakarta.ws.rs.Produces
import jakarta.ws.rs.QueryParam
import jakarta.ws.rs.core.MediaType import jakarta.ws.rs.core.MediaType
@Path("overlay/{diceid}") @Path("overlay/{diceid}")
@@ -12,14 +13,14 @@ class OverlayResource {
@GET @GET
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
fun get(@PathParam("diceid") diceid: String): TemplateInstance { fun get(@PathParam("diceid") diceid: String, @QueryParam("scale") scale: Int? = 7, @QueryParam("clearAfter") clearAfter: Long? = -1): TemplateInstance {
return Templates.overlay(diceid) return Templates.overlay(diceid, scale ?: 7, clearAfter ?: -1)
} }
@GET @GET
@Path("/results") @Path("/results")
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
fun results(@PathParam("diceid") room: String): TemplateInstance { fun results(@PathParam("diceid") room: String, @QueryParam("name") name: String?, @QueryParam("user") user: String?): TemplateInstance {
return Templates.results(room) return Templates.results(room, name ?: "all", user ?: "all")
} }
} }

View File

@@ -1,18 +0,0 @@
package de.arindy.dicetower
import io.quarkus.qute.TemplateInstance
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.PathParam
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.MediaType
@Path("results/{room}")
class ResultsResource {
@GET
@Produces(MediaType.TEXT_HTML)
fun get(@PathParam("room") room: String): TemplateInstance {
return Templates.results(room)
}
}

View File

@@ -6,7 +6,9 @@ import io.quarkus.qute.TemplateInstance
@CheckedTemplate @CheckedTemplate
object Templates { object Templates {
@JvmStatic @JvmStatic
external fun overlay(diceid: String): TemplateInstance external fun overlay(diceid: String, scale: Int?, clearAfter: Long?): TemplateInstance
@JvmStatic @JvmStatic
external fun results(room: String): TemplateInstance external fun results(room: String, name: String?, user: String?): TemplateInstance
@JvmStatic
external fun index(version: String): TemplateInstance
} }

View File

@@ -63,6 +63,7 @@ class ColorPicker extends HTMLElement {
.rgba-display-wrapper { .rgba-display-wrapper {
height: 100%; height: 100%;
padding: 1.25em; padding: 1.25em;
text-align: center;
} }
#rgba-display { #rgba-display {
font-family: "Courier New", monospace; font-family: "Courier New", monospace;
@@ -72,11 +73,11 @@ class ColorPicker extends HTMLElement {
display: inline; display: inline;
border: none; border: none;
border-radius: 0.2em; border-radius: 0.2em;
margin-bottom: 2.5em;
letter-spacing: 0.1em; letter-spacing: 0.1em;
width: 8em; width: 5em;
max-width: 100%; max-width: 100%;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
text-align: center;
} }
input[type=range] { input[type=range] {
width: 100%; width: 100%;
@@ -132,13 +133,17 @@ class ColorPicker extends HTMLElement {
this.lightnessInput = this.shadowRoot.getElementById( 'lightness' ); this.lightnessInput = this.shadowRoot.getElementById( 'lightness' );
this.colorPreview = this.shadowRoot.getElementById( 'color-preview' ); this.colorPreview = this.shadowRoot.getElementById( 'color-preview' );
this.rgbaDisplay = this.shadowRoot.getElementById( 'rgba-display' ); this.rgbaDisplay = this.shadowRoot.getElementById( 'rgba-display' );
this.rgbaDisplay.onkeyup = (event) => {
if(event.keyCode === 13) {
this.setColor( this.rgbaDisplay.value );
}
}
// Create a temporary canvas to read color values. // Create a temporary canvas to read color values.
let tempCanvas = document.createElement( 'canvas' ); let tempCanvas = document.createElement( 'canvas' );
tempCanvas.height = 1; tempCanvas.height = 1;
tempCanvas.width = 1; tempCanvas.width = 1;
this.tempCanvasCTX = tempCanvas.getContext( '2d', { willReadFrequently: true } ); this.tempCanvasCTX = tempCanvas.getContext( '2d', { willReadFrequently: true } );
} }

View File

@@ -0,0 +1,618 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dice-Tower</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="/META-INF/resources/vendor/color-picker.js"></script>
<style>
.w3-theme-l6 {
color: #000 !important;
background-color: #999999 !important;
border-radius: 10px;
}
.w3-theme-l4 {
color: #fff !important;
background-color: #666666 !important;
border-radius: 10px;
}
.w3-theme-l1 {
color: #fff !important;
background-color: #333333 !important;
border-radius: 10px;
}
.collapsible {
background-color: #333333;
color: black;
margin-top: 10px;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
.content {
padding: 0 18px;
display: none;
overflow: hidden;
background-color: #999999;
color: #000;
border-radius: 10px;
}
button {
padding: 10px;
border: #333333 3px solid;
border-radius: 10px;
background: #333333;
color: #fff
}
button:hover {
background: #444444;
}
button:active {
background: #222222;
}
input {
margin: 10px;
}
/* The switch - the box around the slider */
.switch {
position: relative;
display: inline-block;
width: 60px;
height: 25px;
}
/* Hide default HTML checkbox */
.switch input {
opacity: 0;
width: 0;
height: 0;
}
/* The slider */
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
-webkit-transition: .4s;
transition: .4s;
}
.slider:before {
position: absolute;
content: "";
height: 18px;
width: 26px;
left: 4px;
bottom: 4px;
background-color: white;
-webkit-transition: .4s;
transition: .4s;
}
input:checked + .slider {
background-color: #333333;
}
input:focus + .slider {
box-shadow: 0 0 1px #333333;
}
input:checked + .slider:before {
-webkit-transform: translateX(26px);
-ms-transform: translateX(26px);
transform: translateX(26px);
}
.overlayButton {
background: transparent;
border: none;
width: 5px;
height: 5px;
font-size: large;
color: #000
}
.overlayButton:hover {
background: transparent;
}
.tooltip {
position: fixed;
border-radius: 0.5rem;
padding: 15px;
color: #fff !important;
background-color: #333333dd !important
}
#dice-box {
position: relative;
box-sizing: border-box;
width: 100%;
height: 100%;
background: transparent;
background-size: cover;
}
#dice-box canvas {
width: 100%;
height: 100%;
margin: 20px
}
.checkbox {
position: relative;
padding-left: 100px;
margin-bottom: 12px;
margin-left: 10px;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkmark {
margin-left: 75px;
position: absolute;
top: 0;
left: 0;
height: 15px;
width: 15px;
background-color: #eee;
}
.checkbox:hover input ~ .checkmark {
background-color: #ccc;
}
/* When the checkbox is checked, add a blue background */
.checkbox input:checked ~ .checkmark {
background-color: #333333;
}
/* Create the checkmark/indicator (hidden when not checked) */
.checkmark:after {
content: "";
position: absolute;
display: none;
}
/* Show the checkmark when checked */
.checkbox input:checked ~ .checkmark:after {
display: block;
}
/* Style the checkmark/indicator */
.checkbox .checkmark:after {
left: 5px;
top: 2px;
width: 5px;
height: 10px;
border: solid white;
border-width: 0 3px 3px 0;
-webkit-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
#snackbar-container {
visibility: hidden;
position: fixed;
width: 100%;
z-index: 1;
bottom: 200px;
display: flex;
justify-content: center;
align-items: center;
}
#snackbar {
visibility: hidden;
text-align: center;
border-radius: 0.5rem;
padding: 15px;
color: #fff !important;
border: #fff 5px solid;
background-color: #333333dd !important
}
#snackbar-container.show {
visibility: visible;
}
#snackbar.show {
visibility: visible;
-webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s;
animation: fadein 0.5s, fadeout 0.5s 2.5s;
}
</style>
</head>
<body class="w3-theme-l1">
<div class="w3-container w3-content"
style="height: 95vh; display: flex; flex-direction: column; justify-content: space-between; padding: 25px">
<h1 style="text-align: center">Dice-Tower</h1>
<div class="w3-panel w3-theme-l4 w3-card w3-display-container"
style="padding: 25px; text-align: center; margin-bottom: auto;">
<label for="name" id="nameLabel">Name </label><input type="text" id="name" style="width: 50%; margin-top: 20px"
required onkeyup="start(event)"/><br/>
<label for="room" id="roomLabel">Room </label><input type="text" id="room" style="width: 50%" required
onkeyup="start(event)"/><br/>
<div>
<button id="start" onclick="start()" style="align-self: center; margin-top: 20px">Start 🞂</button>
<label class="checkbox" id="gm-container">Join as GM
<input type="checkbox" id="gm" style="margin-left: 50px">
<span class="checkmark"></span>
</label>
</div>
</div>
<div id="dice-tower" hidden class="w3-panel w3-theme-l4 w3-card w3-display-container"
style="padding: 25px; margin-bottom: auto">
<button type="button" class="collapsible" style="color: white; font-weight: bold">Overlay URLs <a>🞃</a></button>
<div class="content">
<div id="overlay-urls">
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;">
<label for="overlayId" id="overlayLabel">Dice-Overlay </label>
<input type="text" readonly id="overlayId" style="flex-grow: 1" onclick="copyToClipboard(this.id)"/>
<button id="overlay-hint-button" popovertarget="overlay-hint" data-trigger="hover"
class="overlayButton">🛈
</button>
</div>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;"
hidden id="all-results-urls">
<label for="resultsId">All-Results-Overlay </label>
<input type="text" readonly id="resultsId" style="flex-grow: 1" onclick="copyToClipboard(this.id)"/>
<button popovertarget="all-results-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline;">
<label for="myResultsId">My-Results-Overlay </label>
<input type="text" readonly id="myResultsId" style="flex-grow: 1" onclick="copyToClipboard(this.id)"/>
<button popovertarget="my-results-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
</div>
<button type="button" class="collapsible" style="color: white; font-weight: bold">Customize Dice <a>🞃</a>
</button>
<div class="content">
<label for="theme">Theme </label>
<select name="theme" id="theme" style="margin: 25px">
<option value="default">Default</option>
<option value="blueGreenMetal">Blue-Green Metal</option>
<option value="diceOfRolling">Dice of Rolling</option>
<option value="gemstone">Gemstone</option>
<option value="gemstoneMarble">Marble Gemstone</option>
<option value="rock">Rock</option>
<option value="rust">Rust</option>
<option value="smooth">Smooth</option>
<option value="wooden">Wooden</option>
</select>
<color-picker id="themeColor"></color-picker>
<div id="dice-box"></div>
<button style="margin: 10px" id="preview">Preview 🔎</button>
<button style="margin: 10px" onclick="saveDice()">Save 💾</button>
<button popovertarget="save-dice-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: center; margin: 20px 25px;">
<label style="font-size: x-large; font-weight: bold;">Roll </label>
<button style="border: transparent; border-radius: 100%; font-size: x-large; font-weight: bold; height: 50px; width: 50px"
onclick="removeDice()">-
</button>
<label style="font-size: x-large; font-weight: bold;" id="dice-amount">1</label>
<button style="border: transparent; border-radius: 100%; font-size: x-large; font-weight: bold; height: 50px; width: 50px"
onclick="addDice()">+
</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d4')">D4</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d6')">D6</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d8')">D8</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d10')">D10</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d12')">D12</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d20')">D20</button>
<button style="font-size: x-large; font-weight: bold;" onclick="rollEasy('d100')">D100</button>
</div>
<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; margin: 20px 25px;">
<label for="command">Command </label>
<input type="text" id="command" style="flex-grow: 1" onkeyup="roll(event)"/>
<button hidden id="roll" onclick="roll()">Roll 🎲</button>
<button popovertarget="command-hint" data-trigger="hover" class="overlayButton">🛈</button>
</div>
<div style="margin: 20px 25px" id="all-results">
<label for="resultSwitch">Show all results </label>
<label class="switch">
<input type="checkbox" id="resultSwitch">
<span class="slider"></span>
</label>
</div>
</div>
<div id="results" hidden class="w3-panel w3-theme-l6 w3-card w3-display-container"
style="padding: 25px; flex-grow: 1; margin-bottom: auto">
<iframe id="resultFrame" title="results" style="width: 100%; height: 85%; overflow: hidden; border: 0"
onload="this.height=this.contentWindow.document.body.scrollHeight;"></iframe>
</div>
<div popover id="overlay-hint" class="tooltip">
<p style="color: red; font-weight: bold">Only the last loaded instance of that overlay rolls the dice!</p>
<p>Query Params you can Change:</p>
<ul>
<li><strong>scale</strong> changes the size of the dice (any value over 1)</li>
<li><strong>clearAfter</strong> time until dice are cleared (in seconds; remove param or set -1 to keep the
dice)
</li>
</ul>
</div>
<div popover id="all-results-hint" class="tooltip">
<p>Shows all Results in this room</p>
</div>
<div popover id="my-results-hint" class="tooltip">
<p>Shows only my Results in this room</p>
</div>
<div popover id="save-dice-hint" class="tooltip">
This saves your current theme and theme color for current Name
</div>
<div popover id="command-hint" class="tooltip">
Example Commands: "1d6", "2d8 1d100", "1d4 and 1d6", "2d20 & 1d2, "5d6+10"
</div>
<div style="margin-top: 20px; flex-grow: 1" id="how-to">
<div class="w3-panel w3-theme-l4 w3-card w3-display-container" style="padding: 25px; margin-bottom: auto">
<h2 style="text-align: center">How-To</h2>
<ul>
<li>
Join a room by entering your character name and the name of the room.<br/>
<strong>If you are a GM, make sure to join the room first or let all other players rejoin to get all
Overlay-URLs.</strong>
</li>
<li>Open your Dice-Overlay either in a new Tab or as a browser source in OBS</li>
<ul>
<li style="color: red; font-weight: bold">Only the last loaded instance of that overlay rolls the
dice!
</li>
<li>You can configure your Overlay with query parameters (more information at the info element next
to the link)
</li>
</ul>
<li>Configure your dice</li>
<li>Save your dice configuration</li>
<li>Start rolling</li>
</ul>
</div>
</div>
</div>
<div id="snackbar-container">
<div id="snackbar">.. they see them rolling</div>
</div>
<script>
function addDice() {
let amount = +document.getElementById('dice-amount').innerText
document.getElementById('dice-amount').innerText = amount + 1
}
function removeDice() {
let amount = +document.getElementById('dice-amount').innerText
if (amount > 1) {
document.getElementById('dice-amount').innerText = amount - 1
}
}
function url() {
return window.location.protocol + '//' + window.location.hostname + (window.location.port?.length > 0 ? ':' + window.location.port : '');
}
function start(event) {
console.log(document.getElementById('gm').checked)
if ((!event || event.keyCode === 13) && document.getElementById('name').value.length > 0 && document.getElementById('room').value.length > 0) {
document.getElementById('overlayId').value = url() + '/overlay/' + document.getElementById('room').value + ':' + localStorage.getItem('userId') + '?scale=7&clearAfter=30';
document.getElementById('resultsId').value = url() + '/overlay/' + document.getElementById('room').value + '/results';
document.getElementById('myResultsId').value = document.getElementById('resultsId').value + '?name=' + encodeURIComponent(document.getElementById('name').value) + '&user=' + localStorage.getItem('userId');
document.getElementById('resultFrame').src = document.getElementById('myResultsId').value;
document.getElementById('roll').hidden = false;
document.getElementById('start').hidden = true;
document.getElementById('dice-tower').hidden = false;
document.getElementById('name').hidden = true;
document.getElementById('room').hidden = true;
document.getElementById('gm-container').hidden = true;
document.getElementById('how-to').hidden = true;
document.getElementById('results').hidden = false;
document.getElementById('all-results').hidden = !document.getElementById('gm').checked;
document.getElementById('all-results-urls').style.display = document.getElementById('gm').checked ? 'fles' : 'none';
document.getElementById('nameLabel').innerHTML = '<strong style="font-size:x-large;">' + document.getElementById('name').value + '</strong>';
document.getElementById('roomLabel').innerHTML = '<strong style="font-size:medium;">' + document.getElementById('room').value + '</strong>';
document.getElementById('overlayLabel').innerHTML = 'Dice-Overlay for <strong>' + document.getElementById('name').value + '</strong>';
document.title = document.getElementById('name').value + ' - Dice-Tower';
if (localStorage.getItem(document.getElementById('name').value + "-theme")) {
document.getElementById('theme').value = localStorage.getItem(document.getElementById('name').value + "-theme")
}
if (localStorage.getItem(document.getElementById('name').value + "-themeColor")) {
document.getElementById('themeColor').setColor(localStorage.getItem(document.getElementById('name').value + "-themeColor"));
}
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + '/register')
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: document.getElementById('name').value,
overlay: document.getElementById('overlayId').value,
id: document.getElementById('room').value + ':' + localStorage.getItem('userId')
}))
if (document.getElementById('gm').checked) {
document.getElementById('resultSwitch').checked = true;
document.getElementById('resultFrame').src = document.getElementById('resultsId').value;
const evtSource = new EventSource(url() + '/dice/' + document.getElementById('room').value + '/users');
evtSource.addEventListener('message', function (event) {
let data = JSON.parse(event.data);
if (data.id !== document.getElementById('room').value + ':' + localStorage.getItem('userId')) {
let overlays = document.getElementById('overlay-urls');
let newOverlay = document.getElementById(data.id) ?? document.createElement('div');
newOverlay.replaceChildren(...[]);
newOverlay.id = data.id;
newOverlay.style.display = "flex";
newOverlay.style.flexDirection = "row";
newOverlay.style.justifyContent = "space-between";
newOverlay.style.alignItems = "baseline";
let newLabel = document.createElement('label');
newLabel.for = data.id + 'url';
newLabel.innerHTML = "Dice-Overlay for <strong>" + data.name + "</strong>";
let newInput = document.createElement('input');
newInput.type = "text";
newInput.readOnly = true;
newInput.id = data.id + 'url';
newInput.style.flexGrow = '1';
newInput.value = data.overlay;
newInput.onclick = () => copyToClipboard(newInput.id)
let hint = document.getElementById('overlay-hint-button').cloneNode(true);
newOverlay.appendChild(newLabel);
newOverlay.appendChild(newInput);
newOverlay.appendChild(hint);
overlays.appendChild(newOverlay);
}
showPopover();
});
}
}
}
function rollEasy(dice) {
document.getElementById('command').value = document.getElementById('dice-amount').innerText + dice;
roll();
}
function roll(event) {
if ((!event || event.keyCode === 13) && document.getElementById('command').value?.length > 0) {
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + ':' + localStorage.getItem(`userId`))
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: document.getElementById('name').value,
command: document.getElementById('command').value,
themeColor: document.getElementById('themeColor').value,
theme: document.getElementById('theme').value
}))
}
}
function saveDice() {
localStorage.setItem(document.getElementById('name').value + "-theme", document.getElementById('theme').value)
localStorage.setItem(document.getElementById('name').value + "-themeColor", document.getElementById('themeColor').value)
}
let coll = document.getElementsByClassName("collapsible");
for (let i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
const content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
this.children[0].innerHTML = "🞃"
} else {
content.style.display = "block";
this.children[0].innerHTML = "🞁"
}
});
}
function showPopover() {
const popover = document.querySelectorAll("[popovertarget][data-trigger='hover']");
popover.forEach((e) => {
const target = document.querySelector("#" + e.getAttribute("popovertarget"));
e.addEventListener("mouseover", () => {
target.showPopover();
});
e.addEventListener("mouseout", () => {
target.hidePopover();
});
});
}
showPopover();
document.getElementById('resultSwitch').addEventListener('change', function () {
if (!this.checked) {
document.getElementById('resultFrame').src = document.getElementById('myResultsId').value;
} else {
document.getElementById('resultFrame').src = document.getElementById('resultsId').value;
}
})
document.addEventListener("DOMContentLoaded", async () => {
if (!localStorage.getItem("userId")) {
localStorage.setItem("userId", self.crypto.randomUUID());
}
})
function copyToClipboard(id) {
let copyText = document.getElementById(id);
copyText.select();
copyText.setSelectionRange(0, 99999);
navigator.clipboard.writeText(copyText.value);
showSnackbar("Link copied to clipboard: <br/>" + copyText.value);
}
function showSnackbar(message) {
let snackbar = document.getElementById("snackbar");
let snackbarContainer = document.getElementById("snackbar-container");
snackbar.innerHTML = message;
snackbar.className = "show";
snackbarContainer.className = "show";
setTimeout(function () {
snackbar.className = snackbar.className.replace("show", "");
snackbarContainer.className = snackbarContainer.className.replace("show", "");
}, 5000);
}
</script>
<script type="module">
import DiceBox from "/vendor/dice-box/dice-box.es.js";
document.addEventListener("DOMContentLoaded", async () => {
document.getElementById('preview').onclick = async () => {
document.getElementById('dice-box').replaceChildren(...[])
const diceBox = new DiceBox("#dice-box", {
assetPath: "/vendor/assets/",
theme: document.getElementById('theme').value,
themeColor: document.getElementById('themeColor').value,
scale: 14
});
await diceBox.init()
diceBox.roll(['1d2', '1d4', '1d6', '1d8', '1d10', '1d12', '1d20', '1d100']);
}
})
</script>
</body>
<footer class="w3-theme-l1 w3-center w3-padding-16">
Version {version} ::
<a href="https://git.arindy.de/arindy/dice-tower" target="_blank" class="w3-hover-text-black">Dice-Tower on my
GitTea</a>
</footer>
</html>

View File

@@ -41,7 +41,6 @@
} }
import DiceBox from "/vendor/dice-box/dice-box.es.js"; import DiceBox from "/vendor/dice-box/dice-box.es.js";
const evtSource = new EventSource(url() + "/dice/{diceid??}/stream"); const evtSource = new EventSource(url() + "/dice/{diceid??}/stream");
const diceBox = new DiceBox("#dice-box", { const diceBox = new DiceBox("#dice-box", {
assetPath: "/vendor/assets/", assetPath: "/vendor/assets/",
theme: 'default', theme: 'default',
@@ -56,13 +55,15 @@
'smooth', 'smooth',
'wooden' 'wooden'
], ],
scale: 7 scale: {scale}
}); });
document.addEventListener("DOMContentLoaded", async() => { document.addEventListener("DOMContentLoaded", async() => {
await diceBox.init() await diceBox.init()
let timeout = 0;
evtSource.addEventListener("message", function (event) { evtSource.addEventListener("message", function (event) {
clearInterval(timeout);
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
diceBox.onRollComplete = (rollResult) => { diceBox.onRollComplete = (rollResult) => {
let httpRequest = new XMLHttpRequest(); let httpRequest = new XMLHttpRequest();
@@ -71,12 +72,15 @@
httpRequest.send(JSON.stringify({ httpRequest.send(JSON.stringify({
name: data.name, name: data.name,
user: data.user, user: data.user,
themeColor: data.themeColor,
results: rollResult, results: rollResult,
} )) } ))
if ({clearAfter} > 0) {
timeout = setTimeout(() => diceBox.clear(), {clearAfter} * 1000)
}
} }
diceBox.roll(data.roll, { theme: data.theme?.length > 0 ? data.theme : 'default', themeColor: data.themeColor.length > 0 ? data.themeColor : '#4545FF' }); diceBox.roll(data.roll, { theme: data.theme?.length > 0 ? data.theme : 'default', themeColor: data.themeColor.length > 0 ? data.themeColor : '#4545FF' });
}) })
}) })
</script> </script>

View File

@@ -1,144 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dice-Tower</title>
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://www.w3schools.com/lib/w3-theme-black.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<script src="/vendor/color-picker.js"></script>
<style>
.collapsible {
background-color: grey;
color: black;
cursor: pointer;
margin-top: 10px;
padding: 18px;
width: 100%;
border: none;
text-align: left;
outline: none;
font-size: 15px;
}
.content {
padding: 0 18px;
display: none;
overflow: hidden;
background-color: lightgrey;
}
button {
padding: 10px;
}
input {
margin: 10px;
}
</style>
</head>
<body class="w3-theme-l4">
<div class="w3-container w3-content" style="height: 100vh">
<h1>Dice-Tower</h1>
<p>Welcome to Dice-Tower</p>
<div class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px">
<label for="name" id="nameLabel">Name </label><input type="text" id="name" style="width: 75%" required onkeyup="start(event)"/><br/>
<label for="room" id="roomLabel">Room </label><input type="text" id="room" style="width: 75%" required onkeyup="start(event)"/><br/>
</div>
<div id="dice-tower" hidden class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px">
<button type="button" class="collapsible" style="color: white; font-weight: bold">Overlay URLs</button>
<div class="content">
<label for="overlayId">Dice-Overlay </label><input type="text" readonly id="overlayId" style="width: 75%"/><br/>
<label for="resultsId">Results-Overlay </label><input type="text" readonly id="resultsId" style="width: 75%"/><br/>
</div>
<button type="button" class="collapsible" style="color: white; font-weight: bold">Customize Dice</button>
<div class="content">
<label for="theme">Theme </label>
<select name="theme" id="theme" style="margin: 25px">
<option value="default">Default</option>
<option value="blueGreenMetal">Blue-Green Metal</option>
<option value="diceOfRolling">Dice of Rolling</option>
<option value="gemstone">Gemstone</option>
<option value="gemstoneMarble">Marble Gemstone</option>
<option value="rock">Rock</option>
<option value="rust">Rust</option>
<option value="smooth">Smooth</option>
<option value="wooden">Wooden</option>
</select>
<br/>
<color-picker value="#cd72fe" id="themeColor"></color-picker><br/>
</div><br/>
<p>Example Commands: "1d6", "2d8 1d100", "1d4 and 1d6", "2d20 & 1d2, "5d6+10"</p>
<label for="command">Command </label><input type="text" id="command" onkeyup="roll(event)"/>
<button hidden id="roll" onclick="roll()">Roll</button>
</div>
<button id="start" onclick="start()">Start</button>
<div id="results" hidden class="w3-panel w3-white w3-card w3-display-container" style="padding: 25px; overflow: hidden; height: 50vh;">
<h2>Results</h2>
<iframe id="resultFrame" title="results" style="width: 100%; height: 85%; overflow: hidden" onload="this.height=this.contentWindow.document.body.scrollHeight;"></iframe>
</div>
</div>
<script>
function url() {
return window.location.protocol + '//' + window.location.hostname + (window.location.port?.length > 0 ? ':' + window.location.port : '');
}
function start(event) {
if((!event || event.keyCode === 13) && document.getElementById('name').value.length > 0 && document.getElementById('room').value.length > 0) {
document.getElementById('overlayId').value = url() + '/overlay/' + document.getElementById('room').value + ':' + localStorage.getItem('userId');
document.getElementById('resultsId').value = url() + '/overlay/' + document.getElementById('room').value + '/results';
document.getElementById('resultFrame').src = url() + '/overlay/' + document.getElementById('room').value + '/results';
document.getElementById('roll').hidden = false;
document.getElementById('start').hidden = true;
document.getElementById('dice-tower').hidden = false;
document.getElementById('name').hidden = true;
document.getElementById('room').hidden = true;
document.getElementById('results').hidden = false;
document.getElementById('nameLabel').innerHTML = 'Name: <strong>' + document.getElementById('name').value + '</strong>';
document.getElementById('roomLabel').innerHTML = 'Room: <strong>' + document.getElementById('room').value + '</strong>';
}
}
function roll(event) {
if((!event || event.keyCode === 13) && document.getElementById('command').value?.length > 0) {
let httpRequest = new XMLHttpRequest();
httpRequest.open('POST', url() + '/dice/' + document.getElementById('room').value + ':' + localStorage.getItem(`userId`))
httpRequest.setRequestHeader('Content-Type', 'application/json')
httpRequest.send(JSON.stringify({
name: document.getElementById('name').value,
command: document.getElementById('command').value,
themeColor: document.getElementById('themeColor').value,
theme: document.getElementById('theme').value
} ))
}
}
let coll = document.getElementsByClassName("collapsible");
for (let i = 0; i < coll.length; i++) {
coll[i].addEventListener("click", function () {
localStorage.setItem("theme", document.getElementById('theme').value)
localStorage.setItem("themeColor", document.getElementById('themeColor').value)
const content = this.nextElementSibling;
if (content.style.display === "block") {
content.style.display = "none";
} else {
content.style.display = "block";
}
});
}
document.addEventListener("DOMContentLoaded", async() => {
if (!localStorage.getItem("userId")) {
localStorage.setItem("userId", self.crypto.randomUUID());
}
if (localStorage.getItem("theme")) {
document.getElementById('theme').value = localStorage.getItem("theme")
}
if (localStorage.getItem("themeColor")) {
document.getElementById('themeColor').setColor(localStorage.getItem("themeColor"));
}
})
</script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<title>Results</title> <title>Results</title>
</head> </head>
<body> <body>
<div id="results" style="padding: 25px; font-size: x-large"> <div id="results" style="font-size: x-large">
</div> </div>
<script type="module"> <script type="module">
function url() { function url() {
@@ -14,8 +14,9 @@
const evtSource = new EventSource(url() + '/dice/{room}/results'); const evtSource = new EventSource(url() + '/dice/{room}/results');
evtSource.addEventListener('message', function (event) { evtSource.addEventListener('message', function (event) {
let data = JSON.parse(event.data); let data = JSON.parse(event.data);
let name = document.getElementById(data.user) ?? document.createElement('div'); if ("{name}" === "all" && "{user}" === "all" || "{name}" === data.name && "{user}" === data.user || "{name}" === "all" && "{user}" === data.user || "{name}" === data.name && "{user}" === "all") {
name.id = data.user; let name = document.getElementById(data.user + '-' + data.name) ?? document.createElement('div');
name.id = data.user + '-' + data.name;
name.replaceChildren(...[]); name.replaceChildren(...[]);
let node = document.createElement('p'); let node = document.createElement('p');
let resultText = '' let resultText = ''
@@ -30,9 +31,10 @@
resultText += '<br/>&ensp; D' + result.sides + ': [' + values.map(value => value === 1 ? '<strong style="color: red">' + value + '</strong>' : value === result.sides ? '<strong style="color: green">' + value + '</strong>' : value).join(' + ') + (result.modifier > 0 ? ' <a style="text-decoration: underline">+' + result.modifier + '</a>' : result.modifier < 0 ? ' <a style="text-decoration: underline">' + result.modifier + '</a>' : '') + '] = <strong style="font-size: x-large">' + result.value + '</strong> ' resultText += '<br/>&ensp; D' + result.sides + ': [' + values.map(value => value === 1 ? '<strong style="color: red">' + value + '</strong>' : value === result.sides ? '<strong style="color: green">' + value + '</strong>' : value).join(' + ') + (result.modifier > 0 ? ' <a style="text-decoration: underline">+' + result.modifier + '</a>' : result.modifier < 0 ? ' <a style="text-decoration: underline">' + result.modifier + '</a>' : '') + '] = <strong style="font-size: x-large">' + result.value + '</strong> '
}) })
} }
node.innerHTML = '<strong>' + data.name + ':</strong> &#127922; ' + resultText node.innerHTML = '<strong style="text-shadow: 2px 2px 10px ' + data.themeColor + ';">' + data.name + ':</strong> 🎲 ' + resultText
name.appendChild(node) name.appendChild(node)
document.getElementById('results').appendChild(name); document.getElementById('results').insertBefore(name, document.getElementById('results').firstChild);
}
}) })
</script> </script>
</body> </body>