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 { MemoryDatabase } from "../utils/databases/Memory.mjs";
|
||||
import { registerMetaSettings } from "../settings/meta.mjs";
|
||||
import { UserFlagDatabase } from "../utils/databases/UserFlag.mjs";
|
||||
|
||||
Hooks.on(`init`, () => {
|
||||
Logger.debug(`Initializing`);
|
||||
|
||||
registerMetaSettings();
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
CONFIG.StatsDatabase = UserFlagDatabase;
|
||||
} else {
|
||||
CONFIG.StatsDatabase = MemoryDatabase;
|
||||
}
|
||||
|
||||
Handlebars.registerHelper(helpers);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
export function registerMetaSettings() {
|
||||
game.settings.register(__ID__, `tables`, {
|
||||
scope: `world`,
|
||||
type: Array,
|
||||
config: false,
|
||||
requiresReload: false,
|
||||
});
|
||||
|
||||
game.settings.register(__ID__, `data`, {
|
||||
scope: `user`,
|
||||
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": [
|
||||
"./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