Start working on the Stats Viewer application
This commit is contained in:
parent
cb3bc7c86c
commit
91863d85a8
18 changed files with 422 additions and 1 deletions
128
module/Apps/StatsViewer.mjs
Normal file
128
module/Apps/StatsViewer.mjs
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
import { filePath } from "../consts.mjs";
|
||||||
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
|
||||||
|
const { HandlebarsApplicationMixin, ApplicationV2 } = foundry.applications.api;
|
||||||
|
|
||||||
|
export class StatsViewer extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
classes: [
|
||||||
|
__ID__,
|
||||||
|
`StatsViewer`,
|
||||||
|
],
|
||||||
|
window: {
|
||||||
|
title: `Stat Viewer`,
|
||||||
|
frame: true,
|
||||||
|
positioned: true,
|
||||||
|
resizable: true,
|
||||||
|
minimizable: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: 475,
|
||||||
|
height: 315,
|
||||||
|
},
|
||||||
|
actions: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
tableSelect: {
|
||||||
|
template: filePath(`templates/Apps/StatsViewer/tableSelect.hbs`),
|
||||||
|
},
|
||||||
|
dataFilters: {
|
||||||
|
template: filePath(`templates/Apps/StatsViewer/dataFilters.hbs`),
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
template: filePath(`templates/Apps/StatsViewer/graph.hbs`),
|
||||||
|
},
|
||||||
|
tableOverview: {
|
||||||
|
template: filePath(`templates/Apps/StatsViewer/dataOverview.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
async _onRender(context, options) {
|
||||||
|
await super._onRender(context, options);
|
||||||
|
|
||||||
|
const { parts } = options;
|
||||||
|
|
||||||
|
if (parts.includes(`tableSelect`)) {
|
||||||
|
this.element
|
||||||
|
.querySelector(`[data-application-part="tableSelect"] [data-bind]`)
|
||||||
|
?.addEventListener(`change`, this.#bindListener.bind(this));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
async _preparePartContext(partId) {
|
||||||
|
const ctx = {};
|
||||||
|
|
||||||
|
switch (partId) {
|
||||||
|
case `tableSelect`: {
|
||||||
|
this.#prepareTableSelectContext(ctx);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
Logger.log(`Context`, ctx);
|
||||||
|
};
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
|
|
||||||
|
_selectedTable;
|
||||||
|
_selectedSubtable;
|
||||||
|
async #prepareTableSelectContext(ctx) {
|
||||||
|
const tables = new Set();
|
||||||
|
const subtables = {};
|
||||||
|
|
||||||
|
for (const tableConfig of CONFIG.StatsDatabase.getTables()) {
|
||||||
|
const [ table, subtable ] = tableConfig.name.split(`/`);
|
||||||
|
tables.add(table);
|
||||||
|
if (subtable?.length > 0) {
|
||||||
|
subtables[table] ??= [];
|
||||||
|
subtables[table].push(subtable);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const tableList = Array.from(tables);
|
||||||
|
this._selectedTable ??= tableList[0];
|
||||||
|
|
||||||
|
ctx.table = this._selectedTable;
|
||||||
|
ctx.tables = tableList;
|
||||||
|
|
||||||
|
const subtableList = subtables[this._selectedTable];
|
||||||
|
if (subtableList && !subtableList.includes(this._selectedSubtable)) {
|
||||||
|
this._selectedSubtable = subtableList[0];
|
||||||
|
}
|
||||||
|
ctx.subtable = this._selectedSubtable;
|
||||||
|
ctx.subtables = subtableList;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
async #bindListener(event) {
|
||||||
|
const target = event.target;
|
||||||
|
const data = target.dataset;
|
||||||
|
|
||||||
|
const binding = data.bind;
|
||||||
|
if (!binding || !Object.hasOwn(this, binding)) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.log(`updating ${binding} value to ${target.value}`);
|
||||||
|
this[binding] = target.value;
|
||||||
|
this.render();
|
||||||
|
// this.#updatePartContainingElement(target);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param { HTMLElement } element
|
||||||
|
*/
|
||||||
|
#updatePartContainingElement(element) {
|
||||||
|
const partRoot = element.closest(`[data-application-part]`);
|
||||||
|
if (!partRoot) { return };
|
||||||
|
const data = partRoot.dataset;
|
||||||
|
const partId = data.applicationPart;
|
||||||
|
this.render({ parts: [partId] });
|
||||||
|
};
|
||||||
|
};
|
||||||
58
module/Apps/TestApp.mjs
Normal file
58
module/Apps/TestApp.mjs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import Chart from "chart.js/auto";
|
||||||
|
import { filePath } from "../consts.mjs";
|
||||||
|
|
||||||
|
const { ApplicationV2, HandlebarsApplicationMixin } = foundry.applications.api;
|
||||||
|
|
||||||
|
const data = [
|
||||||
|
{ face: 1, count: Math.floor(Math.random() * 50) },
|
||||||
|
{ face: 2, count: Math.floor(Math.random() * 50) },
|
||||||
|
{ face: 3, count: Math.floor(Math.random() * 50) },
|
||||||
|
{ face: 4, count: Math.floor(Math.random() * 50) },
|
||||||
|
{ face: 5, count: Math.floor(Math.random() * 50) },
|
||||||
|
{ face: 6, count: Math.floor(Math.random() * 50) },
|
||||||
|
];
|
||||||
|
|
||||||
|
export class TestApp extends HandlebarsApplicationMixin(ApplicationV2) {
|
||||||
|
// #region Options
|
||||||
|
static DEFAULT_OPTIONS = {
|
||||||
|
window: {
|
||||||
|
title: `Dice Pool`,
|
||||||
|
frame: true,
|
||||||
|
positioned: true,
|
||||||
|
resizable: false,
|
||||||
|
minimizable: true,
|
||||||
|
},
|
||||||
|
position: {
|
||||||
|
width: `auto`,
|
||||||
|
height: `auto`,
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
static PARTS = {
|
||||||
|
numberOfDice: {
|
||||||
|
template: filePath(`templates/Apps/TestApp/main.hbs`),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// #endregion
|
||||||
|
|
||||||
|
_onRender() {
|
||||||
|
const canvas = this.element.querySelector(`canvas`);
|
||||||
|
new Chart(
|
||||||
|
canvas,
|
||||||
|
{
|
||||||
|
type: `bar`,
|
||||||
|
data: {
|
||||||
|
labels: data.map( r => r.face),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: `d6 Rolls`,
|
||||||
|
data: data.map(r => r.count),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
18
module/api.mjs
Normal file
18
module/api.mjs
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { StatsViewer } from "./Apps/StatsViewer.mjs";
|
||||||
|
import { TestApp } from "./Apps/TestApp.mjs";
|
||||||
|
|
||||||
|
const { deepFreeze } = foundry.utils;
|
||||||
|
|
||||||
|
Object.defineProperty(
|
||||||
|
globalThis,
|
||||||
|
`stats`,
|
||||||
|
{
|
||||||
|
value: deepFreeze({
|
||||||
|
Apps: {
|
||||||
|
TestApp,
|
||||||
|
StatsViewer,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
writable: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
6
module/handlebarsHelpers/_index.mjs
Normal file
6
module/handlebarsHelpers/_index.mjs
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { options } from "./options.mjs";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
// #region Complex
|
||||||
|
"st-options": options,
|
||||||
|
};
|
||||||
33
module/handlebarsHelpers/options.mjs
Normal file
33
module/handlebarsHelpers/options.mjs
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* @typedef {object} Option
|
||||||
|
* @property {string} [label]
|
||||||
|
* @property {string|number} value
|
||||||
|
* @property {boolean} [disabled]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | number} selected
|
||||||
|
* @param {Array<Option | string>} opts
|
||||||
|
* @param {any} meta
|
||||||
|
*/
|
||||||
|
export function options(selected, opts) {
|
||||||
|
selected = Handlebars.escapeExpression(selected);
|
||||||
|
const htmlOptions = [];
|
||||||
|
|
||||||
|
for (let opt of opts) {
|
||||||
|
if (typeof opt === `string`) {
|
||||||
|
opt = { label: opt, value: opt };
|
||||||
|
};
|
||||||
|
opt.value = Handlebars.escapeExpression(opt.value);
|
||||||
|
htmlOptions.push(
|
||||||
|
`<option
|
||||||
|
value="${opt.value}"
|
||||||
|
${selected === opt.value ? `selected` : ``}
|
||||||
|
${opt.disabled ? `disabled` : ``}
|
||||||
|
>
|
||||||
|
${ opt.label }
|
||||||
|
</option>`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
return new Handlebars.SafeString(htmlOptions.join(`\n`));
|
||||||
|
};
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
import { registerMetaSettings } from "../settings/meta.mjs";
|
import helpers from "../handlebarsHelpers/_index.mjs";
|
||||||
import { Logger } from "../utils/Logger.mjs";
|
import { Logger } from "../utils/Logger.mjs";
|
||||||
|
import { MemoryDatabase } from "../utils/databases/Memory.mjs";
|
||||||
|
import { registerMetaSettings } from "../settings/meta.mjs";
|
||||||
|
import { UserFlagDatabase } from "../utils/databases/UserFlag.mjs";
|
||||||
|
|
||||||
Hooks.on(`init`, () => {
|
Hooks.on(`init`, () => {
|
||||||
Logger.debug(`Initializing`);
|
Logger.debug(`Initializing`);
|
||||||
|
|
||||||
registerMetaSettings();
|
registerMetaSettings();
|
||||||
|
|
||||||
|
if (import.meta.env.PROD) {
|
||||||
|
CONFIG.StatsDatabase = UserFlagDatabase;
|
||||||
|
} else {
|
||||||
|
CONFIG.StatsDatabase = MemoryDatabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
Handlebars.registerHelper(helpers);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,11 @@
|
||||||
export function registerMetaSettings() {
|
export function registerMetaSettings() {
|
||||||
|
game.settings.register(__ID__, `tables`, {
|
||||||
|
scope: `world`,
|
||||||
|
type: Array,
|
||||||
|
config: false,
|
||||||
|
requiresReload: false,
|
||||||
|
});
|
||||||
|
|
||||||
game.settings.register(__ID__, `data`, {
|
game.settings.register(__ID__, `data`, {
|
||||||
scope: `user`,
|
scope: `user`,
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
||||||
34
module/utils/databases/Memory.mjs
Normal file
34
module/utils/databases/Memory.mjs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
import { Table } from "./model.mjs";
|
||||||
|
|
||||||
|
export class MemoryDatabase {
|
||||||
|
static getTables() {
|
||||||
|
/** @type {Array<{ name: string; }>} */
|
||||||
|
return [
|
||||||
|
{ name: `Dice/d4` },
|
||||||
|
{ name: `Dice/d6` },
|
||||||
|
{ name: `Dice/d8` },
|
||||||
|
{ name: `Dice/d10` },
|
||||||
|
{ name: `Dice/d12` },
|
||||||
|
{ name: `Dice/d20` },
|
||||||
|
{ name: `Dice/d100` },
|
||||||
|
{ name: `Count of Successes` },
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
static createRow(table, user, row) {};
|
||||||
|
|
||||||
|
static getRows(tableId, ...users) {
|
||||||
|
if (users.length === 0) { users = [game.user] };
|
||||||
|
|
||||||
|
const datasets = {};
|
||||||
|
|
||||||
|
return datasets;
|
||||||
|
};
|
||||||
|
|
||||||
|
static updateRow(table, user, rowId, changes) {};
|
||||||
|
|
||||||
|
static deleteRow(table, user, rowId) {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
36
module/utils/databases/UserFlag.mjs
Normal file
36
module/utils/databases/UserFlag.mjs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
/* eslint-disable no-unused-vars */
|
||||||
|
import { Table } from "./model.mjs";
|
||||||
|
|
||||||
|
const tablesFlag = `tables`;
|
||||||
|
|
||||||
|
export class UserFlagDatabase {
|
||||||
|
static getTables() {
|
||||||
|
/** @type {Array<{ name: string; }>} */
|
||||||
|
const tableConfig = game.settings.get(__ID__, `tables`);
|
||||||
|
return tableConfig ?? [];
|
||||||
|
};
|
||||||
|
|
||||||
|
static createRow(table, user, row) {};
|
||||||
|
|
||||||
|
static getRows(tableId, ...users) {
|
||||||
|
if (users.length === 0) { users = [game.user] };
|
||||||
|
|
||||||
|
const datasets = {};
|
||||||
|
for (const user of users) {
|
||||||
|
const tables = user.getFlag(__ID__, tablesFlag) ?? {};
|
||||||
|
if (tables[tableId] === undefined) {
|
||||||
|
datasets[user.id] = null;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const table = new Table(tables[tableId]);
|
||||||
|
}
|
||||||
|
return datasets;
|
||||||
|
};
|
||||||
|
|
||||||
|
static updateRow(table, user, rowId, changes) {};
|
||||||
|
|
||||||
|
static deleteRow(table, user, rowId) {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/* eslint-enable no-unused-vars */
|
||||||
43
module/utils/databases/model.mjs
Normal file
43
module/utils/databases/model.mjs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
const { fields } = foundry.data;
|
||||||
|
|
||||||
|
// MARK: Table
|
||||||
|
export class Table extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
return {
|
||||||
|
name: new fields.StringField({
|
||||||
|
nullable: false,
|
||||||
|
required: true,
|
||||||
|
blank: false,
|
||||||
|
trim: true,
|
||||||
|
validate(value) {
|
||||||
|
return !value.match(/[^a-z0-9_\-:]/i);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
data: new fields.TypedObjectField(Row),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// MARK: Row
|
||||||
|
export class Row extends foundry.abstract.DataModel {
|
||||||
|
static defineSchema() {
|
||||||
|
return {
|
||||||
|
id: new fields.StringField({
|
||||||
|
nullable: false,
|
||||||
|
required: true,
|
||||||
|
blank: false,
|
||||||
|
}),
|
||||||
|
timestamp: new fields.NumberField({
|
||||||
|
min: 0,
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
}),
|
||||||
|
value: new fields.AnyField(),
|
||||||
|
private: new fields.BooleanField({
|
||||||
|
initial: false,
|
||||||
|
required: true,
|
||||||
|
nullable: false,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -9,5 +9,11 @@
|
||||||
},
|
},
|
||||||
"esmodules": [
|
"esmodules": [
|
||||||
"./module.mjs"
|
"./module.mjs"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
{
|
||||||
|
"src": "styles/main.css",
|
||||||
|
"layer": "modules.stats-tracker"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
public/styles/Apps/StatsViewer.css
Normal file
12
public/styles/Apps/StatsViewer.css
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
.stat-tracker.StatsViewer {
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-application-part="tableSelect"] {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
public/styles/main.css
Normal file
3
public/styles/main.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
@layer resets, elements, components, apps, exceptions;
|
||||||
|
|
||||||
|
@import url("./Apps/StatsViewer.css") layer(apps);
|
||||||
3
public/templates/Apps/StatsViewer/dataFilters.hbs
Normal file
3
public/templates/Apps/StatsViewer/dataFilters.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
Data Filters
|
||||||
|
</div>
|
||||||
3
public/templates/Apps/StatsViewer/dataOverview.hbs
Normal file
3
public/templates/Apps/StatsViewer/dataOverview.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
Table Overview
|
||||||
|
</div>
|
||||||
3
public/templates/Apps/StatsViewer/graph.hbs
Normal file
3
public/templates/Apps/StatsViewer/graph.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
Graph Canvas
|
||||||
|
</div>
|
||||||
14
public/templates/Apps/StatsViewer/tableSelect.hbs
Normal file
14
public/templates/Apps/StatsViewer/tableSelect.hbs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
data-bind="_selectedTable"
|
||||||
|
>
|
||||||
|
{{ st-options table tables }}
|
||||||
|
</select>
|
||||||
|
{{#if subtables}}
|
||||||
|
<select
|
||||||
|
data-bind="_selectedSubtable"
|
||||||
|
>
|
||||||
|
{{ st-options subtable subtables }}
|
||||||
|
</select>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
3
public/templates/Apps/TestApp/main.hbs
Normal file
3
public/templates/Apps/TestApp/main.hbs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
<div>
|
||||||
|
<canvas></canvas>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue