Skip to content

Commit

Permalink
feat: registerConfig support
Browse files Browse the repository at this point in the history
  • Loading branch information
BigJk committed Sep 18, 2024
1 parent a5dfe07 commit bc4d3be
Show file tree
Hide file tree
Showing 19 changed files with 220 additions and 24 deletions.
13 changes: 13 additions & 0 deletions cmd/app/electron.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package main

import (
"fmt"
"io"
"io/ioutil"
stdlog "log"
Expand Down Expand Up @@ -40,6 +41,18 @@ func init() {
}

func startElectron(db database.Database, debug bool) {
// Writing the preload.js file that is needed for the electron web-view
if err := os.WriteFile(filepath.Join(sndDataDir, "/preload.js"), []byte(`
// Helper for Sales & Dungeons to let template/generator preview communicate with the main process
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
sendData: (type, data) => {
ipcRenderer.sendToHost("data", type, JSON.stringify(data));
},
});`), 0666); err != nil {
panic(fmt.Errorf("could not write preload.js file: %w", err))
}

// Start the S&D Backend in separate go-routine.
go func() {
startServer(db, debug)
Expand Down
21 changes: 14 additions & 7 deletions cmd/app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,27 @@ func isMacAppBundle() bool {
return strings.Contains(execDir, "Sales & Dungeons.app")
}

func openDatabase() database.Database {
userdata := "./userdata/"
func getSndDataDir() string {
dir := "./"

if isMacAppBundle() {
home, err := os.UserHomeDir()
if err != nil {
panic(err)
}
userdata = filepath.Join(home, "/Documents/Sales & Dungeons/userdata")

err = os.MkdirAll(userdata, 0777)
fmt.Println("INFO: changed userdata folder because of app bundle", userdata, err)
dir = filepath.Join(home, "/Documents/Sales & Dungeons")
err = os.MkdirAll(dir, 0777)
fmt.Println("INFO: changed data folder because of app bundle", dir, err)
}

return dir
}

var sndDataDir = getSndDataDir()

func openDatabase() database.Database {
userdata := filepath.Join(sndDataDir, "userdata")

db, err := badger.New(userdata)
if err != nil {
panic(err)
Expand All @@ -85,7 +92,7 @@ func openDatabase() database.Database {
func startServer(db database.Database, debug bool) {
rand.Seed(time.Now().UnixNano())

s, err := server.New(db, append(serverOptions, server.WithDebug(debug), server.WithPrinter(&cups.CUPS{}), server.WithPrinter(&remote.Remote{}), server.WithPrinter(&serial.Serial{}), server.WithPrinter(&dump.Dump{}))...)
s, err := server.New(db, append(serverOptions, server.WithDataDir(sndDataDir), server.WithDebug(debug), server.WithPrinter(&cups.CUPS{}), server.WithPrinter(&remote.Remote{}), server.WithPrinter(&serial.Serial{}), server.WithPrinter(&dump.Dump{}))...)
if err != nil {
panic(err)
}
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/js/core/generator-ipc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import m from 'mithril';

import { filterOutDynamicConfigValues, mergeConfigValues } from 'js/types/config';
import Generator, { sanitizeConfig } from 'js/types/generator';

/**
* Creates a function that handles IPC messages. These messages can either be from a webview or a iframe.
* They are used to communicate from the generator to the editor.
* @param generator The generator object.
* @param state The state object.
*/
export function createOnMessage(generator?: Generator | null, state?: { config: any }) {
return (type: string, msg: any) => {
console.log('IPCMessage', type, msg);

if (!generator || !state) {
return;
}

if (type === 'registerConfig') {
if (Array.isArray(msg)) {
generator.config = mergeConfigValues(
filterOutDynamicConfigValues(generator.config),
msg.map((c) => ({
...c,
isDynamic: true,
})),
);
state.config = sanitizeConfig(generator, state.config);
}
}

m.redraw();
};
}
3 changes: 3 additions & 0 deletions frontend/src/js/core/guid.ts
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
/**
* Generate a unique identifier
*/
export default () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
20 changes: 20 additions & 0 deletions frontend/src/js/core/store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import m from 'mithril';
import { flatten } from 'lodash-es';

import Fuse from 'fuse.js';
Expand Down Expand Up @@ -62,6 +63,7 @@ type Store = {
ai: {
token: string;
};
dataDir: string;
};

const initialState: Store = {
Expand All @@ -80,6 +82,7 @@ const initialState: Store = {
ai: {
token: '',
},
dataDir: '',
};

const store = create(initialState, (atom) => ({
Expand All @@ -88,6 +91,7 @@ const store = create(initialState, (atom) => ({
*/
loadAll(noSettings: boolean = false) {
return Promise.all([
this.loadDataDir(),
...(noSettings ? [] : [this.loadSettings()]),
this.loadTemplates(),
this.loadGenerators(),
Expand All @@ -99,6 +103,21 @@ const store = create(initialState, (atom) => ({
]);
},

/**
* LoadDataDir loads the data directory from the backend.
*/
loadDataDir() {
m.request({
method: 'GET',
url: '/api/dataDir',
}).then((res) =>
atom.update((state) => ({
...state,
dataDir: res as string,
})),
);
},

/**
* LoadSettings loads the settings from the backend.
*/
Expand Down Expand Up @@ -263,3 +282,4 @@ export const printer = store.focus('printer');
export const printerTypes = store.focus('printerTypes');
export const packages = store.focus('publicLists');
export const ai = store.focus('ai');
export const dataDir = store.focus('dataDir');
15 changes: 15 additions & 0 deletions frontend/src/js/core/templating.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,20 @@ const aiScript = `
</script>
`;

const IPCScript = `
<script>
const isElectron = !!window.api?.sendData;
function registerConfig(config) {
if(isElectron) {
window.api.sendData('registerConfig', config);
} else {
window.parent.postMessage({ type: 'registerConfig', data: config }, '*');
}
}
</script>
`;

/**
* State object for template rendering.
*/
Expand Down Expand Up @@ -209,6 +223,7 @@ export const render = (
let additional = '';
additional += rngScript(clonedState.config.seed ?? Math.ceil(Math.random() * 500000000));
additional += aiScript;
additional += IPCScript;

if (minimal) {
additional = '';
Expand Down
35 changes: 35 additions & 0 deletions frontend/src/js/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ export type ConfigValue = {
description: string;
type: string;
default: any;

/**
* Whether the config value is dynamic and should not be saved to the generator.
*/
isDynamic?: boolean;
};

/**
Expand All @@ -18,3 +23,33 @@ export const fillConfigValues = (config: any, configValues: ConfigValue[]) => {
}
return result;
};

/**
* Merge two config value arrays. If a value is present in both arrays, the value from the main array is used.
* @param main Main config value array.
* @param merge Config value array to merge into the main array.
*/
export const mergeConfigValues = (main: ConfigValue[], merge: ConfigValue[]) => {
main = filterValidConfigValues(main);
merge = filterValidConfigValues(merge);

const result: ConfigValue[] = [];
const mainKeys = main.map((c) => c.key);
for (const config of merge) {
if (!mainKeys.includes(config.key)) {
result.push(config);
}
}
return [...main, ...result];
};

/**
* Filter out invalid config values. A check for all values is performed.
*/
export const filterValidConfigValues = (configValues: ConfigValue[]) =>
configValues.filter((config) => !!config.key && !!config.name && !!config.description && !!config.type && config.default != undefined);

/**
* Filter out dynamic config values.
*/
export const filterOutDynamicConfigValues = (configValues: ConfigValue[]) => configValues.filter((config) => !config.isDynamic);
11 changes: 11 additions & 0 deletions frontend/src/js/types/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,18 @@ type Generator = BasicInfo & {
count?: number;
};

/**
* Generates a random seed.
*/
function seed() {
return Math.ceil(Math.random() * 999999999).toString();
}

/**
* Sanitizes the config object by setting default values for missing fields and removing old fields that are not present in the config anymore.
* @param g Generator object.
* @param configs Config object.
*/
const sanitizeConfig = (g: Generator, configs: any) => {
// Create base config
if (configs === undefined) {
Expand All @@ -40,6 +48,9 @@ const sanitizeConfig = (g: Generator, configs: any) => {
return pickBy(configs, (val, key) => key === 'seed' || g.config.some((conf) => conf.key === key));
};

/**
* Creates an empty generator object.
*/
function createEmptyGenerator(): Generator {
return {
name: 'Your Generator Name',
Expand Down
5 changes: 3 additions & 2 deletions frontend/src/js/types/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ type Settings = {
printerEndpoint: string;
printerWidth: number;
commands: Commands;
stylesheets: string[];
spellcheckerLanguages: string[];
packageRepos: string[];
syncKey: string;
Expand All @@ -29,6 +28,9 @@ type Settings = {
aiUrl: string;
};

/**
* Create an empty settings object.
*/
export function createEmptySettings(): Settings {
return {
printerType: 'unknown',
Expand All @@ -44,7 +46,6 @@ export function createEmptySettings(): Settings {
splitHeight: 0,
splitDelay: 0,
},
stylesheets: [],
spellcheckerLanguages: [],
packageRepos: [],
syncKey: '',
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/js/types/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ type Template = BasicInfo & {
count?: number;
};

/**
* Sanitizes the config object by setting default values for missing fields and removing old fields that are not present in the config anymore.
* @param t Template object.
* @param configs Config object.
*/
const sanitizeConfig = (t: Template, configs: any) => {
// Create base config
if (configs === undefined) {
Expand All @@ -30,6 +35,9 @@ const sanitizeConfig = (t: Template, configs: any) => {
return pickBy(configs, (val, key) => key === 'seed' || t.config.some((conf) => conf.key === key));
};

/**
* Creates an empty template object.
*/
function createEmptyTemplate(): Template {
return {
name: 'Your Template Name',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/js/ui/components/editor/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import m from 'mithril';

import { fillConfigValues } from 'js/types/config';
import Generator, { sanitizeConfig, seed } from 'js/types/generator';
import { createOnMessage } from 'js/core/generator-ipc';
import { createNunjucksCompletionProvider } from 'js/core/monaco/completion-nunjucks';
import { settings } from 'js/core/store';

Expand Down Expand Up @@ -203,6 +204,7 @@ export default (): m.Component<GeneratorEditorProps> => {
onRendered: (html) => {
attrs.onRendered?.(html);
},
onMessage: createOnMessage(attrs.generator, state),
}),
]),
];
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/js/ui/components/print-preview-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type PrintPreviewTemplateProps = {
width?: number;
onRendered?: (html: string) => void;
onError?: (error: PrintPreviewError[]) => void;
onMessage?: (type: string, data: any) => void;
};

export default (): m.Component<PrintPreviewTemplateProps> => {
Expand Down Expand Up @@ -124,6 +125,7 @@ export default (): m.Component<PrintPreviewTemplateProps> => {
className: attrs.className,
content: lastRendered,
width: attrs.width ?? 320,
onMessage: attrs.onMessage,
loading,
},
aiPresent && !attrs.hideAiNotice && settings.value.aiEnabled
Expand Down
Loading

0 comments on commit bc4d3be

Please sign in to comment.