Enhance Dice Roller Plugin: Update version, add inline formula detection, improve UI interactions, and refine error handling

This commit is contained in:
mkuch 2026-05-13 08:03:15 +02:00
parent 5fbf2304e3
commit a77fafdfba
9 changed files with 116 additions and 83 deletions

16
main.ts
View file

@ -18,7 +18,7 @@ export default class DiceRollerPlugin extends Plugin {
// Add ribbon icon to open dice roller
this.addRibbonIcon('dice', 'Open Dice Roller', () => {
this.activateDiceView();
void this.activateDiceView();
});
// Add command to open dice roller
@ -26,7 +26,7 @@ export default class DiceRollerPlugin extends Plugin {
id: 'open-dice-roller',
name: 'Open Dice Roller',
callback: () => {
this.activateDiceView();
void this.activateDiceView();
}
});
@ -54,10 +54,10 @@ export default class DiceRollerPlugin extends Plugin {
}
onunload() {
this.app.workspace.detachLeavesOfType(DICE_BUILDER_VIEW_TYPE);
}
async activateDiceView() {
async activateDiceView(): Promise<DiceBuilderView | null> {
const { workspace } = this.app;
let leaf: WorkspaceLeaf | null = null;
@ -81,8 +81,12 @@ export default class DiceRollerPlugin extends Plugin {
// Reveal the leaf in case it is in a collapsed sidebar
if (leaf) {
workspace.revealLeaf(leaf);
await workspace.revealLeaf(leaf);
if (leaf.view instanceof DiceBuilderView) {
return leaf.view;
}
}
return null;
}
refreshDiceView() {
@ -96,7 +100,7 @@ export default class DiceRollerPlugin extends Plugin {
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
this.settings = Object.assign({}, DEFAULT_SETTINGS, ((await this.loadData()) ?? {}) as Partial<DiceRollerSettings>);
}
async saveSettings() {

View file

@ -1,11 +1,10 @@
{
"id": "rpg-dice-roller",
"name": "RPG Dice Roller",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"version": "1.1.0",
"minAppVersion": "1.7.2",
"description": "Build RPG dice roll commands with d20 syntax for Discord and Roll20. Create formulas visually with advantage/disadvantage support.",
"author": "Miłosz Kucharski",
"authorUrl": "https://mzkuch.pl",
"fundingUrl": "https://github.com/sponsors/Butterski",
"fundingUrl": "https://buymeacoffee.com/butterski",
"isDesktopOnly": false
}

View file

@ -15,7 +15,6 @@
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",

View file

@ -68,7 +68,7 @@ export class DiceParser {
advantage,
disadvantage
};
} catch (e) {
} catch (_) {
return null;
}
}
@ -135,13 +135,21 @@ export class DiceParser {
* Example: "Roll 2d6 + 4 for damage" or "Attack: 1d20 + 5"
*/
static detectInlineFormulas(text: string): Array<{formula: string, start: number, end: number}> {
// Match common dice patterns: XdY, XdY+Z, XdY-Z, etc.
const regex = /(\d*d\d+(?:[a-z]+[<>]?\d+)?(?:\s*[+-]\s*(?:\d*d\d+(?:[a-z]+[<>]?\d+)?|\d+))*(?:\s+(?:adv|dis|advantage|disadvantage))?)/gi;
// Match common dice patterns: XdY, XdY+Z, XdY-Z, Z+XdY, etc.
// We match a sequence of terms (dice or numbers) separated by + or -
// Then we filter to ensure at least one dice term is present
const regex = /((?:\d*d\d+(?:[a-z]+[<>]?\d+)?|\d+)(?:\s*[+-]\s*(?:\d*d\d+(?:[a-z]+[<>]?\d+)?|\d+))*(?:\s+(?:adv|dis|advantage|disadvantage))?)/gi;
const results = [];
let match;
while ((match = regex.exec(text)) !== null) {
const formula = match[1].trim();
// Must contain a 'd' to be a dice formula (avoids matching just "5 + 5")
if (!formula.toLowerCase().includes('d')) {
continue;
}
// Validate it's a real dice formula
if (this.parse(formula)) {
results.push({

View file

@ -25,54 +25,43 @@ export function registerRollSyntaxProcessor(plugin: DiceRollerPlugin) {
codeEl.setAttribute('data-formula', formula);
// Make it clickable
codeEl.style.cursor = 'pointer';
codeEl.addClass('dice-roll-clickable');
// Add click handler
codeEl.addEventListener('click', async (e) => {
codeEl.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const parsed = DiceParser.parse(formula);
if (parsed) {
// Find the dice view and load the formula
const leaves = plugin.app.workspace.getLeavesOfType('dice-builder-view');
if (leaves.length > 0) {
const view = leaves[0].view;
if (view instanceof DiceBuilderView) {
view.loadFormula(parsed);
plugin.app.workspace.revealLeaf(leaves[0]);
new Notice(`Loaded dice formula: ${formula}`);
}
} else {
// Open the dice view first
await plugin.activateDiceView();
// Wait a bit for the view to open
setTimeout(() => {
const newLeaves = plugin.app.workspace.getLeavesOfType('dice-builder-view');
if (newLeaves.length > 0) {
const view = newLeaves[0].view;
if (view instanceof DiceBuilderView) {
view.loadFormula(parsed);
new Notice(`Loaded dice formula: ${formula}`);
}
void (async () => {
// Find the dice view and load the formula
const leaves = plugin.app.workspace.getLeavesOfType('dice-builder-view');
if (leaves.length > 0) {
const view = leaves[0].view;
if (view instanceof DiceBuilderView) {
view.loadFormula(parsed);
// Use window.setTimeout per popout window guidelines
window.setTimeout(() => {
void plugin.app.workspace.revealLeaf(leaves[0]);
}, 10);
new Notice(`Loaded dice formula: ${formula}`);
}
}, 100);
}
} else {
// Open the dice view first
const view = await plugin.activateDiceView();
if (view) {
view.loadFormula(parsed);
new Notice(`Loaded dice formula: ${formula}`);
}
}
})();
}
});
// Add hover effect
codeEl.addEventListener('mouseenter', () => {
codeEl.style.transform = 'translateY(-1px)';
codeEl.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.15)';
});
codeEl.addEventListener('mouseleave', () => {
codeEl.style.transform = '';
codeEl.style.boxShadow = '';
});
}
});
});

View file

@ -16,7 +16,9 @@ export class DiceRollerSettingTab extends PluginSettingTab {
containerEl.empty();
containerEl.createEl('h2', { text: 'RPG Dice Roller Settings' });
new Setting(containerEl)
.setName('RPG Dice Roller Settings')
.setHeading();
new Setting(containerEl)
.setName('Default platform')
@ -52,6 +54,17 @@ export class DiceRollerSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
new Setting(containerEl)
.setName('Enable inline formula detection')
.setDesc('Automatically detect dice formulas like "1d20 + 5" in your notes. If disabled, only ROLL[...] syntax will be detected.')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.enableInlineFormulas)
.onChange(async (value) => {
this.plugin.settings.enableInlineFormulas = value;
await this.plugin.saveSettings();
this.plugin.refreshDiceView();
}));
containerEl.createEl('h3', { text: 'About' });
const aboutDiv = containerEl.createDiv({ cls: 'dice-roller-about' });

View file

@ -4,12 +4,14 @@ export interface DiceRollerSettings {
discordPrefix: string;
roll20Prefix: string;
defaultPlatform: 'discord' | 'roll20';
enableInlineFormulas: boolean;
}
export const DEFAULT_SETTINGS: DiceRollerSettings = {
discordPrefix: '.r',
roll20Prefix: '/roll',
defaultPlatform: 'discord'
defaultPlatform: 'discord',
enableInlineFormulas: true
}
export interface DiceFormula {

View file

@ -10,6 +10,8 @@ export const DICE_BUILDER_VIEW_TYPE = 'dice-builder-view';
export class DiceBuilderView extends ItemView {
plugin: DiceRollerPlugin;
private currentFormula: DiceFormula;
private builderContainer: HTMLElement;
private suggestionsContainer: HTMLElement;
constructor(leaf: WorkspaceLeaf, plugin: DiceRollerPlugin) {
super(leaf);
@ -30,21 +32,27 @@ export class DiceBuilderView extends ItemView {
}
async onOpen(): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
container.addClass('dice-builder-container');
this.contentEl.empty();
this.contentEl.addClass('dice-builder-container');
this.render(container);
this.builderContainer = this.contentEl.createDiv({ cls: 'dice-builder-section' });
this.suggestionsContainer = this.contentEl.createDiv({ cls: 'dice-suggestions-section' });
this.render();
}
async onClose(): Promise<void> {
const container = this.containerEl.children[1];
container.empty();
this.contentEl.empty();
}
private render(containerEl?: Element): void {
const container = containerEl || this.containerEl.children[1];
container.empty();
private render(): void {
this.renderBuilder();
this.renderSuggestions();
}
private renderBuilder(): void {
this.builderContainer.empty();
const container = this.builderContainer;
// Title
const titleEl = container.createEl('h4', { text: 'Dice Formula Builder' });
@ -63,7 +71,7 @@ export class DiceBuilderView extends ItemView {
clearBtn.addClass('dice-clear-button');
clearBtn.onclick = () => {
this.currentFormula = { parts: [], advantage: false, disadvantage: false };
this.render();
this.renderBuilder();
};
// Dice buttons section
@ -167,7 +175,7 @@ export class DiceBuilderView extends ItemView {
if (this.currentFormula.advantage) {
this.currentFormula.disadvantage = false;
}
this.render();
this.renderBuilder();
};
const disBtn = advDisRow.createEl('button', {
@ -180,7 +188,7 @@ export class DiceBuilderView extends ItemView {
if (this.currentFormula.disadvantage) {
this.currentFormula.advantage = false;
}
this.render();
this.renderBuilder();
};
// Action buttons
@ -197,9 +205,6 @@ export class DiceBuilderView extends ItemView {
const rollSyntaxBtn = actionSection.createEl('button', { text: 'Create ROLL[...] Syntax' });
rollSyntaxBtn.addClass('dice-action-button');
rollSyntaxBtn.onclick = () => this.createRollSyntax();
// Suggestions section
this.renderSuggestions();
}
private addDice(quantity: number, sides: number, operator: '+' | '-'): void {
@ -210,7 +215,7 @@ export class DiceBuilderView extends ItemView {
operator
};
this.currentFormula.parts.push(part);
this.render();
this.renderBuilder();
}
private addModifier(value: number, operator: '+' | '-'): void {
@ -220,7 +225,7 @@ export class DiceBuilderView extends ItemView {
operator
};
this.currentFormula.parts.push(part);
this.render();
this.renderBuilder();
}
private getCommandPrefix(): string {
@ -274,15 +279,16 @@ export class DiceBuilderView extends ItemView {
}
private renderSuggestions(): void {
const container = this.containerEl.children[1];
const suggestionsSection = container.createDiv({ cls: 'dice-suggestions-section' });
suggestionsSection.createEl('h5', { text: 'Suggestions from Open Notes' });
const container = this.suggestionsContainer;
container.empty();
container.createEl('h5', { text: 'Suggestions from Open Notes' });
// Get all open markdown views
const markdownLeaves = this.app.workspace.getLeavesOfType('markdown');
if (markdownLeaves.length === 0) {
suggestionsSection.createEl('p', {
container.createEl('p', {
text: 'No open notes',
cls: 'dice-no-suggestions'
});
@ -308,8 +314,11 @@ export class DiceBuilderView extends ItemView {
// Detect ROLL[...] syntax
const rollSyntaxMatches = DiceParser.detectRollSyntax(content);
// Detect inline formulas
const inlineMatches = DiceParser.detectInlineFormulas(content);
// Detect inline formulas (if enabled)
let inlineMatches: Array<{formula: string, start: number, end: number}> = [];
if (this.plugin.settings.enableInlineFormulas) {
inlineMatches = DiceParser.detectInlineFormulas(content);
}
const allMatches = [
...rollSyntaxMatches.map(m => ({ formula: m.formula, source: '📌 ROLL' })),
@ -326,26 +335,26 @@ export class DiceBuilderView extends ItemView {
});
if (uniqueFormulas.size === 0) {
suggestionsSection.createEl('p', {
container.createEl('p', {
text: 'No dice formulas found in current note',
cls: 'dice-no-suggestions'
});
return;
}
const suggestionsList = suggestionsSection.createDiv({ cls: 'dice-suggestions-list' });
const suggestionsList = container.createDiv({ cls: 'dice-suggestions-list' });
uniqueFormulas.forEach((match) => {
const suggestionItem = suggestionsList.createDiv({ cls: 'dice-suggestion-item' });
const leftSection = suggestionItem.createDiv({ cls: 'dice-suggestion-left' });
const formulaSpan = leftSection.createEl('span', {
leftSection.createEl('span', {
text: match.formula,
cls: 'dice-suggestion-formula'
});
const sourceSpan = leftSection.createEl('span', {
leftSection.createEl('span', {
text: match.source,
cls: 'dice-suggestion-source'
});
@ -356,7 +365,7 @@ export class DiceBuilderView extends ItemView {
const parsed = DiceParser.parse(match.formula);
if (parsed) {
this.currentFormula = parsed;
this.render();
this.renderBuilder();
new Notice(`Loaded: ${match.formula}`);
}
};
@ -386,7 +395,7 @@ export class DiceBuilderView extends ItemView {
this.currentFormula.advantage = false;
}
this.render();
this.renderBuilder();
new Notice(`Added: ${match.formula}`);
}
};
@ -399,6 +408,6 @@ export class DiceBuilderView extends ItemView {
public loadFormula(formula: DiceFormula): void {
this.currentFormula = formula;
this.render();
this.renderBuilder();
}
}

View file

@ -393,3 +393,13 @@ code[class*="ROLL"],
font-size: 0.95rem !important;
font-family: var(--font-monospace);
}
.dice-roll-clickable {
cursor: pointer;
transition: transform 0.1s ease, box-shadow 0.1s ease;
}
.dice-roll-clickable:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}