diff --git a/api-messages.go b/api-messages.go
index 46c9f19..cb4f67f 100644
--- a/api-messages.go
+++ b/api-messages.go
@@ -585,7 +585,7 @@ func (app *appContext) MatrixLogin(gc *gin.Context) {
respond(401, "Unauthorized", gc)
return
}
- tempConfig, _ := ini.Load(app.configPath)
+ tempConfig, _ := ini.ShadowLoad(app.configPath)
matrix := tempConfig.Section("matrix")
matrix.Key("enabled").SetValue("true")
matrix.Key("homeserver").SetValue(req.Homeserver)
diff --git a/api.go b/api.go
index a7b4162..d8f10d5 100644
--- a/api.go
+++ b/api.go
@@ -290,6 +290,8 @@ func (app *appContext) GetConfig(gc *gin.Context) {
val := app.config.Section(sectName).Key(settingName)
s := resp.Sections[sectName].Settings[settingName]
switch setting.Type {
+ case "list":
+ s.Value = val.StringsWithShadows("|")
case "text", "email", "select", "password", "note":
s.Value = val.MustString("")
case "number":
@@ -327,7 +329,7 @@ func (app *appContext) GetConfig(gc *gin.Context) {
// @Summary Modify app config.
// @Produce json
-// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings."
+// @Param appConfig body configDTO true "Config split into sections as in config.ini, all values as strings (lists split with | delimiter)."
// @Success 200 {object} boolResponse
// @Failure 500 {object} stringResponse
// @Router /config [post]
@@ -337,7 +339,7 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
var req configDTO
gc.BindJSON(&req)
// Load a new config, as we set various default values in app.config that shouldn't be stored.
- tempConfig, _ := ini.Load(app.configPath)
+ tempConfig, _ := ini.ShadowLoad(app.configPath)
for section, settings := range req {
if section != "restart-program" {
_, err := tempConfig.GetSection(section)
@@ -350,6 +352,15 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
}
if (section == "discord" || section == "matrix") && setting == "language" {
tempConfig.Section("telegram").Key("language").SetValue(value.(string))
+ } else if app.configBase.Sections[section].Settings[setting].Type == "list" {
+ splitValues := strings.Split(value.(string), "|")
+ for i, v := range splitValues {
+ if i == 0 {
+ tempConfig.Section(section).Key(setting).SetValue(v)
+ } else {
+ tempConfig.Section(section).Key(setting).AddShadow(v)
+ }
+ }
} else if value.(string) != app.config.Section(section).Key(setting).MustString("") {
tempConfig.Section(section).Key(setting).SetValue(value.(string))
}
diff --git a/config.go b/config.go
index 2f4a33d..8ea8e12 100644
--- a/config.go
+++ b/config.go
@@ -36,7 +36,7 @@ func (app *appContext) MustSetValue(section, key, val string) {
func (app *appContext) loadConfig() error {
var err error
- app.config, err = ini.Load(app.configPath)
+ app.config, err = ini.ShadowLoad(app.configPath)
if err != nil {
return err
}
diff --git a/main.go b/main.go
index 76002f5..8083563 100644
--- a/main.go
+++ b/main.go
@@ -232,7 +232,7 @@ func start(asDaemon, firstCall bool) {
app.err.Fatalf(lm.FailedCopyConfig, app.configPath, err)
}
app.info.Printf(lm.CopyConfig, app.configPath)
- tempConfig, _ := ini.Load(app.configPath)
+ tempConfig, _ := ini.ShadowLoad(app.configPath)
tempConfig.Section("").Key("first_run").SetValue("true")
tempConfig.SaveTo(app.configPath)
}
diff --git a/migrations.go b/migrations.go
index f77b433..a9f03aa 100644
--- a/migrations.go
+++ b/migrations.go
@@ -60,7 +60,7 @@ func migrateBootstrap(app *appContext) {
}
func migrateEmailConfig(app *appContext) {
- tempConfig, _ := ini.Load(app.configPath)
+ tempConfig, _ := ini.ShadowLoad(app.configPath)
fmt.Println(warning("Part of your email configuration will be migrated to the new \"messages\" section.\nA backup will be made."))
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
if err != nil {
@@ -111,7 +111,7 @@ func migrateEmailStorage(app *appContext) error {
return fmt.Errorf("email address was type %T, not string: \"%+v\"\n", addr, addr)
}
}
- config, err := ini.Load(app.configPath)
+ config, err := ini.ShadowLoad(app.configPath)
if err != nil {
return err
}
@@ -467,7 +467,7 @@ func intialiseCustomContent(app *appContext) {
// Migrate poorly-named and duplicate "url_base" settings to the single "external jfa-go URL" setting.
func migrateExternalURL(app *appContext) {
- tempConfig, _ := ini.Load(app.configPath)
+ tempConfig, _ := ini.ShadowLoad(app.configPath)
err := tempConfig.SaveTo(app.configPath + "_" + commit + ".bak")
if err != nil {
app.err.Fatalf("Failed to backup config: %v", err)
diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts
index 40403da..839cc46 100644
--- a/ts/modules/settings.ts
+++ b/ts/modules/settings.ts
@@ -29,7 +29,7 @@ interface Setting {
requires_restart: boolean;
advanced?: boolean;
type: string;
- value: string | boolean | number;
+ value: string | boolean | number | string[];
depends_true?: string;
depends_false?: string;
wiki_link?: string;
@@ -37,6 +37,11 @@ interface Setting {
asElement: () => HTMLElement;
update: (s: Setting) => void;
+
+ hide: () => void;
+ show: () => void;
+
+ valueAsString: () => string;
}
const splitDependant = (section: string, dep: string): string[] => {
@@ -49,17 +54,22 @@ const splitDependant = (section: string, dep: string): string[] => {
class DOMInput {
protected _input: HTMLInputElement;
- private _container: HTMLDivElement;
- private _tooltip: HTMLDivElement;
- private _required: HTMLSpanElement;
- private _restart: HTMLSpanElement;
- private _advanced: boolean;
+ protected _container: HTMLDivElement;
+ protected _tooltip: HTMLDivElement;
+ protected _required: HTMLSpanElement;
+ protected _restart: HTMLSpanElement;
+ protected _advanced: boolean;
+ protected _section: string;
+ protected _name: string;
+
+ hide = () => this._input.parentElement.classList.add("unfocused");
+ show = () => this._input.parentElement.classList.remove("unfocused");
private _advancedListener = (event: settingsBoolEvent) => {
if (!Boolean(event.detail)) {
- this._input.parentElement.classList.add("unfocused");
+ this.hide();
} else {
- this._input.parentElement.classList.remove("unfocused");
+ this.show();
}
}
@@ -109,10 +119,23 @@ class DOMInput {
}
}
- constructor(inputType: string, setting: Setting, section: string, name: string) {
+ valueAsString = (): string => { return ""+this.value; };
+
+ onValueChange = () => {
+ const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": this.valueAsString() })
+ document.dispatchEvent(event);
+ if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
+ };
+
+ constructor(inputType: string, setting: Setting, section: string, name: string, customInput?: string) {
+ this._section = section;
+ this._name = name;
this._container = document.createElement("div");
this._container.classList.add("setting");
this._container.setAttribute("data-name", name)
+ const defaultInput = `
+
+ `;
this._container.innerHTML = `
`;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
- this._input = this._container.querySelector("input[type=" + inputType + "]") as HTMLInputElement;
+ this._input = this._container.querySelector(".setting-input") as HTMLInputElement;
if (setting.depends_false || setting.depends_true) {
let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
let state = true;
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
- this._input.parentElement.classList.add("unfocused");
+ this.hide();
} else {
- this._input.parentElement.classList.remove("unfocused");
+ this.show();
}
});
}
- const onValueChange = () => {
- const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
- document.dispatchEvent(event);
- if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
- };
- this._input.onchange = onValueChange;
+ this._input.onchange = this.onValueChange;
this.update(setting);
}
@@ -173,6 +191,85 @@ class DOMText extends DOMInput implements SText {
set value(v: string) { this._input.value = v; }
}
+interface SList extends Setting {
+ value: string[];
+}
+
+class DOMList extends DOMInput implements SList {
+ protected _inputs: HTMLDivElement;
+ type: string = "list";
+
+ valueAsString = (): string => { return this.value.join("|"); };
+
+ get value(): string[] {
+ let values = [];
+ const inputs = this._input.querySelectorAll("input") as NodeListOf;
+ for (let i in inputs) {
+ if (inputs[i].value) values.push(inputs[i].value);
+ }
+ return values;
+ }
+ set value(v: string[]) {
+ this._input.textContent = ``;
+ for (let val of v) {
+ let input = this.inputRow(val);
+ this._input.appendChild(input);
+ }
+ const addDummy = () => {
+ const dummyRow = this.inputRow();
+ const input = dummyRow.querySelector("input") as HTMLInputElement;
+ input.placeholder = window.lang.strings("Add");
+ const onDummyChange = () => {
+ if (!(input.value)) return;
+ addDummy();
+ input.removeEventListener("change", onDummyChange);
+ input.placeholder = ``;
+ }
+ input.addEventListener("change", onDummyChange);
+ this._input.appendChild(dummyRow);
+ };
+ addDummy();
+ }
+
+ private inputRow(v: string = ""): HTMLDivElement {
+ let container = document.createElement("div") as HTMLDivElement;
+ container.classList.add("flex", "flex-row", "justify-between");
+ container.innerHTML = `
+
+
+ `;
+ const input = container.querySelector("input") as HTMLInputElement;
+ input.value = v;
+ input.onchange = this.onValueChange;
+ const removeRow = container.querySelector("button") as HTMLButtonElement;
+ removeRow.onclick = () => {
+ if (!(container.nextElementSibling)) return;
+ container.remove();
+ this.onValueChange();
+ }
+ return container;
+ }
+
+ update = (s: Setting) => {
+ this.name = s.name;
+ this.description = s.description;
+ this.required = s.required;
+ this.requires_restart = s.requires_restart;
+ this.value = s.value as string[];
+ this.advanced = s.advanced;
+ }
+
+ asElement = (): HTMLDivElement => { return this._container; }
+
+ constructor(setting: Setting, section: string, name: string) {
+ super("list", setting, section, name,
+ ``
+ );
+ }
+}
+
interface SPassword extends Setting {
value: string;
}
@@ -214,12 +311,15 @@ class DOMBool implements SBool {
private _restart: HTMLSpanElement;
type: string = "bool";
private _advanced: boolean;
+
+ hide = () => this._input.parentElement.classList.add("unfocused");
+ show = () => this._input.parentElement.classList.remove("unfocused");
private _advancedListener = (event: settingsBoolEvent) => {
if (!Boolean(event.detail)) {
- this._input.parentElement.classList.add("unfocused");
+ this.hide();
} else {
- this._input.parentElement.classList.remove("unfocused");
+ this.show();
}
}
@@ -268,8 +368,12 @@ class DOMBool implements SBool {
this._restart.textContent = "";
}
}
+
+ valueAsString = (): string => { return ""+this.value; };
+
get value(): boolean { return this._input.checked; }
set value(state: boolean) { this._input.checked = state; }
+
constructor(setting: SBool, section: string, name: string) {
this._container = document.createElement("div");
this._container.classList.add("setting");
@@ -289,7 +393,7 @@ class DOMBool implements SBool {
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
const onValueChange = () => {
- const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
+ const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() })
document.dispatchEvent(event);
};
this._input.onchange = () => {
@@ -304,9 +408,9 @@ class DOMBool implements SBool {
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
- this._input.parentElement.classList.add("unfocused");
+ this.hide();
} else {
- this._input.parentElement.classList.remove("unfocused");
+ this.show();
}
});
}
@@ -338,11 +442,14 @@ class DOMSelect implements SSelect {
type: string = "bool";
private _advanced: boolean;
+ hide = () => this._container.classList.add("unfocused");
+ show = () => this._container.classList.remove("unfocused");
+
private _advancedListener = (event: settingsBoolEvent) => {
if (!Boolean(event.detail)) {
- this._container.classList.add("unfocused");
+ this.hide();
} else {
- this._container.classList.remove("unfocused");
+ this.show();
}
}
@@ -391,6 +498,9 @@ class DOMSelect implements SSelect {
this._restart.textContent = "";
}
}
+
+ valueAsString = (): string => { return ""+this.value; };
+
get value(): string { return this._select.value; }
set value(v: string) { this._select.value = v; }
@@ -431,14 +541,14 @@ class DOMSelect implements SSelect {
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
- this._container.classList.add("unfocused");
+ this.hide();
} else {
- this._container.classList.remove("unfocused");
+ this.show();
}
});
}
const onValueChange = () => {
- const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.value })
+ const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() })
document.dispatchEvent(event);
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
};
@@ -478,6 +588,9 @@ class DOMNote implements SNote {
type: string = "note";
private _style: string;
+ hide = () => this._container.classList.add("unfocused");
+ show = () => this._container.classList.remove("unfocused");
+
get name(): string { return this._name.textContent; }
set name(n: string) { this._name.textContent = n; }
@@ -486,6 +599,8 @@ class DOMNote implements SNote {
this._description.innerHTML = d;
}
+ valueAsString = (): string => { return ""; };
+
get value(): string { return ""; }
set value(v: string) { return; }
@@ -521,9 +636,9 @@ class DOMNote implements SNote {
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
- this._container.classList.add("unfocused");
+ this.hide();
} else {
- this._container.classList.remove("unfocused");
+ this.show();
}
});
}
@@ -603,12 +718,15 @@ class sectionPanel {
case "note":
setting = new DOMNote(setting as SNote, this._sectionName);
break;
+ case "list":
+ setting = new DOMList(setting as SList, this._sectionName, name);
+ break;
}
if (setting.type != "note") {
this.values[name] = ""+setting.value;
document.addEventListener(`settings-${this._sectionName}-${name}`, (event: CustomEvent) => {
// const oldValue = this.values[name];
- this.values[name] = ""+event.detail;
+ this.values[name] = event.detail;
document.dispatchEvent(new CustomEvent("settings-section-changed"));
});
}