diff --git a/src/components/configuration_item.svelte b/src/components/configuration_item.svelte new file mode 100644 index 0000000..e49d123 --- /dev/null +++ b/src/components/configuration_item.svelte @@ -0,0 +1,421 @@ + + +
+ + + + {device?.name ?? device?.deviceID} + + + + {#if folder && !isThisDevice} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {:else if device && !isThisDevice} + + + + + + + + + + + + + + + + + + + + + {:else if isThisDevice && device} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/if} +
+
+ + Folder ID +
+
+ {folder.id} +
+
+ + Folder Path +
+
+ {folder.path} +
+
+ + Global State +
+
+ + Files: 100, Folders: 50, Storage: ~40 MiB +
+
+ + Local State +
+
+ + Files: 100, Folders: 50, Storage: ~40 MiB +
+
+ + Rescans +
+
+ +
+ + 1h + + Enabled +
+
+
+ + File Versioning +
+
+ + Staggered +
+
+ + Shared With +
+
+ + + {folder.devices + .map((device) => device.deviceID.slice(0, 7)) + .join(", ")} + +
+
+ + Last Scan +
+
+ + {new Date().toISOString()} +
+
+ + Latest Change +
+
+ {"Updated .ext"} +
+
+ + Last seen +
+
+ + {new Date().toString()} +
+
+ + Sync Status +
+
+ + up to date +
+
+ + Addresses +
+
+ {device.address.join(", ")} +
+
+ + Identification +
+
+ { + new Notice("Not implemented yet!"); + }} + > + {device.deviceID.slice(0, 7)} + +
+
+ + Folders +
+
+ {device.name} +
+
+ + Download Rate +
+
+ 0 B/s (0 B) +
+
+ + Upload Rate +
+
+ 0 B/s (0 B) +
+
+ + Local State (Total) +
+
+ Files: 100, Folders: 50, Storage: ~40 MiB +
+
+ + Listeners +
+
+ 3/3 +
+
+ + Discovery +
+
+ 4/5 +
+
+ + Uptime +
+
+ 10h 23m +
+
+ + Identification +
+
+ new Notice("not implement yet !")} + > + {device.deviceID.slice(0, 7)} + +
+
+ + Version +
+
+ v1.23.7, Windows, blabla +
+ + {#if folder && !isThisDevice} + + + + + {:else if device && !isThisDevice} + + + {/if} + +
+ + diff --git a/src/components/configuration_table.svelte b/src/components/configuration_table.svelte new file mode 100644 index 0000000..1c1a452 --- /dev/null +++ b/src/components/configuration_table.svelte @@ -0,0 +1,135 @@ + + + + +
+
+

Folders ({folders.length})

+ {#each folders as folder} + + {/each} +
+ + + +
+
+
+
+
+

This Device

+ +
+
+

+ Remote Devices ({devices.filter((value) => value !== thisDevice) + .length}) +

+ {#each devices.filter((value) => value !== thisDevice) as device} + + {/each} +
+ + + +
+
+
+ + diff --git a/src/components/folder_item.svelte b/src/components/folder_item.svelte new file mode 100644 index 0000000..9b8a9fc --- /dev/null +++ b/src/components/folder_item.svelte @@ -0,0 +1,74 @@ + + + + + + + {folder.label} + Unshared + + + + + + diff --git a/src/components/obsidian_lucide_icon.svelte b/src/components/obsidian_lucide_icon.svelte index b6c2df9..2143bfd 100644 --- a/src/components/obsidian_lucide_icon.svelte +++ b/src/components/obsidian_lucide_icon.svelte @@ -11,4 +11,4 @@ }); -
+
diff --git a/src/components/remote_item.svelte b/src/components/remote_item.svelte new file mode 100644 index 0000000..4d112be --- /dev/null +++ b/src/components/remote_item.svelte @@ -0,0 +1,61 @@ + + + + + +
+ + {device.name ?? device.deviceID} +
+
+ Disconnected + +
+
+ + +
+ + diff --git a/src/components/settings_view.svelte b/src/components/settings_view.svelte index 152ad7b..4b5f6ea 100644 --- a/src/components/settings_view.svelte +++ b/src/components/settings_view.svelte @@ -7,10 +7,12 @@ import ObsidianLucideIcon from "./obsidian_lucide_icon.svelte"; import ObsidianSettingsItem from "./obsidian_settings_item.svelte"; import ObsidianToggle from "./obsidian_toggle.svelte"; + import { ConfigurationModal } from "src/views/configuration_modal"; export let parent: SyncthingSettingTab; let hasSyncthing = true; let apiInputType = "password"; let guiPasswordInputType = "password"; + let syncthingBaseUrl = ""; onMount(async () => { hasSyncthing = await parent.syncthingController.hasSyncthing(); @@ -26,6 +28,7 @@ parent.plugin.saveSettings(); }); } + $: syncthingBaseUrl = `${parent.plugin.settings.configuration.url?.protocol}://${parent.plugin.settings.configuration.url?.ip_address}:${parent.plugin.settings.configuration.url?.port}`; @@ -169,22 +172,101 @@ {/if} - + - { - parent.plugin.settings.configuration.syncthingBaseUrl = - event.currentTarget.value; - await parent.plugin.saveSettings(); - }} +
+ style="display: flex; flex-direction: column; align-items: flex-start; justify-content: flex-end;" + > +
+ + + { + console.log( + "ip address change: ", + event, + event.currentTarget.value + ); + const valueChange = event.currentTarget.value; + if (!parent.plugin.settings.configuration.url) { + parent.plugin.settings.configuration.url = { + protocol: "http", + ip_address: "localhost", + port: 8384, + }; + } + parent.plugin.settings.configuration.url.ip_address = + valueChange === "" || !valueChange + ? "localhost" + : valueChange; + await parent.plugin.saveSettings(); + }} + style="border-radius: 0;" + /> + { + console.log( + "port change: ", + event, + event.currentTarget.value + ); + const valueChange = event.currentTarget.value; + if (!parent.plugin.settings.configuration.url) { + parent.plugin.settings.configuration.url = { + protocol: "http", + ip_address: "localhost", + port: 8384, + }; + } + parent.plugin.settings.configuration.url.port = + isNaN(parseInt(valueChange)) || !valueChange + ? 8384 + : parseInt(valueChange); + await parent.plugin.saveSettings(); + }} + style="border-radius: 0 var(--input-radius) var(--input-radius) 0;" + /> +
+ +
{ @@ -204,7 +286,7 @@ /> { @@ -250,17 +332,9 @@ parent.plugin.settings.gui_username && parent.plugin.settings.gui_password ) { - url = `https://${parent.plugin.settings.gui_username}:${ - parent.plugin.settings.gui_password - }@${ - parent.plugin.settings.configuration.syncthingBaseUrl ?? - "localhost:8384" - }`; + url = `https://${parent.plugin.settings.gui_username}:${parent.plugin.settings.gui_password}@${syncthingBaseUrl}`; } else { - url = `https://${ - parent.plugin.settings.configuration.syncthingBaseUrl ?? - "localhost:8384" - }`; + url = `https://${syncthingBaseUrl}`; } // eslint-disable-next-line no-undef open(url); @@ -275,7 +349,17 @@ +> + + {#if Platform.isDesktopApp} diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 0000000..8838688 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,4 @@ +export type ConfigurationItemData = { + icon: string; + title: string; +}[]; diff --git a/src/components/warning_message.svelte b/src/components/warning_message.svelte new file mode 100644 index 0000000..8eff5e2 --- /dev/null +++ b/src/components/warning_message.svelte @@ -0,0 +1,75 @@ + + +{#if !dismiss} +
+
+
+ + + + + +
+
{title}
+
(dismiss = !dismiss)} + on:click={() => (dismiss = !dismiss)} + class="close-button" + /> +
+
+

{message}

+
+
+{/if} + + diff --git a/src/controllers/utils.ts b/src/controllers/utils.ts index ac47d36..cc1005b 100644 --- a/src/controllers/utils.ts +++ b/src/controllers/utils.ts @@ -158,6 +158,95 @@ export function isStringArray(value: unknown): value is string[] { return isArrayOf(value, (item): item is string => typeof item === "string"); } -export function isNumberArray(value: unknown) { +export function isNumberArray(value: unknown): value is number[] { return isArrayOf(value, (item): item is number => typeof item === "number"); } + +// /** +// * Validates that a JSON object has the expected fields and types. +// * @param json The JSON object to validate. +// * @param field A field to validate. +// * @param type The type of the field. +// * @returns `true` if the JSON object is valid, `false` otherwise. +// */ +// export function validateField( +// json: unknown, +// field: string, +// type: "string" | "number" | "boolean" | "object" | "array" +// ): json is object & Record { +// if (typeof json !== "object" || json === null) { +// return false; +// } +// for (const [key, type] of Object.entries(fields)) { +// if (!json.hasOwnProperty(key)) { +// return false; +// } +// if (typeof json[key as keyof typeof json] !== typeof type) { +// return false; +// } +// } +// return true; +// } + +// let json: unknown = { test: 42 }; +// validateJSON(json, { test: 42 }); +// json; + +/** + * Validates that a JSON object has the expected field and type. + * + * @param json an unknown json response to validate + * @param field a field to validate in the json response + * @param type the type of the field to validate + * @returns `true` if the JSON object is valid, `false` otherwise. And gives the json object the correct type. + * + * @example + * ```ts + * const json: unknown = { test: 42 }; + * if (!validateField(json, "test", "")) throw new Error("Type error"); + * json; // should be of type `object & Record<"test", string>` + * ``` + * + */ +export function validateJsonField( + json: unknown, + field: T, + type: K +): json is object & Record { + return ( + typeof json === "object" && + json !== null && + field in json && + typeof json[field as keyof typeof json] === type + ); +} + +export function logErrorIfNotValidJson( + json: unknown, + field: T, + type: K +): asserts json is object & Record { + if (!validateJsonField(json, field, type)) { + console.error( + `Error validating JSON: ${field} is not present or is not of type ${typeof type}. Got ${JSON.stringify( + json + )}.` + ); + } +} +// export function throwIfNotValid( +// json: unknown, +// field: T, +// type: "string" | "number" | "boolean" | "object" +// ): asserts json is object & Record { +// let checkedType; +// if (type === "string") checkedType = ""; +// else if (type === "number") checkedType = 42; +// else if (type === "boolean") checkedType = true; +// else if (type === "object") checkedType = {}; +// if (!validateField(json, field, checkedType)) { +// throw new Error( +// `Error validating JSON: ${field} is not present or is not of type ${typeof type}. Got ${json}.` +// ); +// } +// } diff --git a/src/data/syncthing_remote_datasource.ts b/src/data/syncthing_remote_datasource.ts index 24ff26f..a1ab75e 100644 --- a/src/data/syncthing_remote_datasource.ts +++ b/src/data/syncthing_remote_datasource.ts @@ -1,4 +1,4 @@ -import { requestUrl } from "obsidian"; +import { Platform, requestUrl } from "obsidian"; import SyncthingPlugin from "src/main"; import { SyncthingDevice } from "src/models/entities"; import { RestFailure } from "src/models/failures"; @@ -27,15 +27,17 @@ export class SyncthingFromREST { /** * Get all the folders of Syncthing installation using the REST API. + * @see https://docs.syncthing.net/rest/config#rest-config-folders-rest-config-devices */ async getAllFolders(): Promise { - const response = await this.requestEndpoint( - "/rest/system/config/folders" - ); + const response = await this.requestEndpoint("/rest/config/folders"); const foldersModel: SyncthingFolderModel[] = []; - for (const folder of await response.json()) { + console.log("REST: ", response.json); + for (const folder of response.json) { console.log("REST: ", folder); - foldersModel.push(SyncthingFolderModel.fromJSON(folder)); + foldersModel.push( + SyncthingFolderModel.fromJSON(JSON.stringify(folder)) + ); } return foldersModel; } @@ -57,14 +59,16 @@ export class SyncthingFromREST { /** * Get all the devices of Syncthing installation using the REST API. + * @see https://docs.syncthing.net/rest/config#rest-config-folders-rest-config-devices */ async getDevices(): Promise { - const response = await this.requestEndpoint( - "/rest/system/config/devices" - ); + const response = await this.requestEndpoint("/rest/config/devices"); const devicesModel: SyncthingDeviceModel[] = []; - for (const device of await response.json()) { - devicesModel.push(SyncthingDeviceModel.fromJSON(device)); + console.log("REST: ", response.json); + for (const device of response.json) { + devicesModel.push( + SyncthingDeviceModel.fromJSON(JSON.stringify(device)) + ); } return devicesModel; } @@ -75,14 +79,24 @@ export class SyncthingFromREST { * @see https://docs.syncthing.net/rest/config.html */ async getConfiguration(): Promise { - const response = await this.requestEndpoint("/rest/system/config"); - return SyncthingConfigurationModel.fromJSON(await response.json()); + const response = await this.requestEndpoint("/rest/config"); + return SyncthingConfigurationModel.fromJSON(response.json); } + /** + * Private method to request an endpoint of the REST API. + * The endpoint should start with a `/`. + * + * @param endpoint - The REST endpoint to call. @see https://docs.syncthing.net/dev/rest.html + * @returns The response of the REST API. + */ private async requestEndpoint(endpoint: string) { // FIXME: Fix the issue when connecting to the REST API. (error 403) console.log("requestEndpoint: Endpoint", endpoint); - const url = `${this.plugin.settings.configuration.syncthingBaseUrl}/${endpoint}`; + let ip_address = this.plugin.settings.configuration.url?.ip_address; + if (ip_address === "localhost" && Platform.isMobileApp) + ip_address = "127.0.0.1"; + const url = `${this.plugin.settings.configuration.url?.protocol}://${ip_address}:${this.plugin.settings.configuration.url?.port}${endpoint}`; const response = requestUrl({ url: url, method: "GET", @@ -91,6 +105,7 @@ export class SyncthingFromREST { Accept: "*/*", "Content-Type": "application/json", "Access-Control-Allow-Origin": "*", + redirect: "follow", }, }); console.log( diff --git a/src/main.ts b/src/main.ts index d340b9f..aba2e5b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,9 +4,7 @@ import { DevModeModal, PluginDevModeController, } from "./controllers/plugin_dev_mode"; -import { - SyncthingFromAndroid -} from "./data/syncthing_android_datasource"; +import { SyncthingFromAndroid } from "./data/syncthing_android_datasource"; import { SyncthingFromCLI } from "./data/syncthing_local_datasource"; import { SyncthingFromREST } from "./data/syncthing_remote_datasource"; import { SyncthingConfiguration } from "./models/entities"; @@ -23,7 +21,13 @@ interface SyncthingPluginSettings { } const DEFAULT_SETTINGS: Partial = { - configuration: { syncthingBaseUrl: "localhost:8384" }, + configuration: { + url: { + protocol: "http", + ip_address: "localhost", + port: 8384, + }, + }, devMode: false, }; diff --git a/src/models/entities.ts b/src/models/entities.ts index 46be081..c3f73d8 100644 --- a/src/models/entities.ts +++ b/src/models/entities.ts @@ -14,10 +14,19 @@ export class SyncthingConfiguration { public version: string, public folders: SyncthingFolder[], public devices: SyncthingDevice[], - public syncthingBaseUrl: string + public url: SyncthingURL ) {} } +/** + * Simple URL object. + */ +export type SyncthingURL = { + protocol: "https" | "http"; + ip_address: string | "localhost"; + port: number; +}; + /** * Available sync types in Syncthing. * @see https://docs.syncthing.net/users/config.html#config-option-folder.type @@ -64,7 +73,7 @@ export class SyncthingDevice { */ public deviceID: string, public introducedBy: string, - public encryptionPassword: string, + // public encryptionPassword: string, // TODO: move to the Folder element. public address: string[], public paused: boolean, public ignoredFolders: string[], @@ -79,7 +88,7 @@ export class ReducedSyncthingDevice implements Pick< SyncthingDevice, - "deviceID" | "introducedBy" | "encryptionPassword" + "deviceID" | "introducedBy" /* | "encryptionPassword" */ > { constructor( diff --git a/src/models/models.ts b/src/models/models.ts index 40220eb..3aaf863 100644 --- a/src/models/models.ts +++ b/src/models/models.ts @@ -1,10 +1,11 @@ -import { isStringArray } from "src/controllers/utils"; +import { isStringArray, logErrorIfNotValidJson } from "src/controllers/utils"; import { ReducedSyncthingDevice, SyncTypes, SyncthingConfiguration, SyncthingDevice, SyncthingFolder, + SyncthingURL, } from "src/models/entities"; export class SyncthingConfigurationModel extends SyncthingConfiguration { @@ -12,9 +13,13 @@ export class SyncthingConfigurationModel extends SyncthingConfiguration { version: string, folders: SyncthingFolder[], devices: SyncthingDevice[], - syncthingBaseUrl = "localhost:8384" + url: SyncthingURL = { + protocol: "http", + ip_address: "localhost", + port: 8384, + } ) { - super(version, folders, devices, syncthingBaseUrl); + super(version, folders, devices, url); } /** @@ -26,19 +31,19 @@ export class SyncthingConfigurationModel extends SyncthingConfiguration { const folders: SyncthingFolder[] = []; if (!(typeof parsedJSON === "object" && parsedJSON !== null)) throw new Error("JSON is not an object or is null"); + logErrorIfNotValidJson(parsedJSON, "version", "string"); if ( !( - "folders" in parsedJSON && Array.isArray(parsedJSON["folders"]) - ) || - !( - "devices" in parsedJSON && Array.isArray(parsedJSON["devices"]) + ( + "folders" in parsedJSON && + Array.isArray(parsedJSON["folders"]) + ) + // TODO: validate the custom type. ) || - !( - "version" in parsedJSON && - typeof parsedJSON["version"] === "string" - ) + !("devices" in parsedJSON && Array.isArray(parsedJSON["devices"])) + // TODO: validate the custom type. ) - throw new Error("Error parsing JSON"); + throw new Error("Error parsing JSON: missing fields or wrong type"); for (const parsedFolder of parsedJSON["folders"]) { console.log(parsedFolder); folders.push( @@ -69,64 +74,23 @@ export class SyncthingConfigurationModel extends SyncthingConfiguration { export class SyncthingFolderModel extends SyncthingFolder { static fromJSON(json: string): SyncthingFolderModel { const parsedJSON = JSON.parse(json); - const reducedDeviceInfos: ReducedSyncthingDevice[] = []; + const reducedDeviceInfos: ReducedSyncthingDeviceModel[] = []; // TODO: to refactor w/ a function. if (!(typeof parsedJSON === "object" && parsedJSON !== null)) throw new Error("JSON is not an object or is null"); - if ( - !( - "folders" in parsedJSON && Array.isArray(parsedJSON["folders"]) - ) || - !( - "devices" in parsedJSON && Array.isArray(parsedJSON["devices"]) - ) || - !("id" in parsedJSON && typeof parsedJSON["id"] === "string") || - !( - "label" in parsedJSON && typeof parsedJSON["label"] === "string" - ) || - !("path" in parsedJSON && typeof parsedJSON["path"] === "string") || - !( - "filesystemType" in parsedJSON && - typeof parsedJSON["filesystemType"] === "string" - ) || - !("type" in parsedJSON && typeof parsedJSON["type"] === "string") || - !( - "maxConflicts" in parsedJSON && - typeof parsedJSON["maxConflicts"] === "number" - ) - ) - throw new Error("Error parsing JSON"); + logErrorIfNotValidJson(parsedJSON, "id", "string"); + logErrorIfNotValidJson(parsedJSON, "label", "string"); + logErrorIfNotValidJson(parsedJSON, "path", "string"); + logErrorIfNotValidJson(parsedJSON, "filesystemType", "string"); + logErrorIfNotValidJson(parsedJSON, "type", "string"); + logErrorIfNotValidJson(parsedJSON, "maxConflicts", 0); + if (!("devices" in parsedJSON && Array.isArray(parsedJSON["devices"]))) + throw new Error( + "Error validating JSON: devices is not present or is not an array" + ); for (const device of parsedJSON["devices"]) { - // TODO: to refactor w/ a function. - if (!(typeof device === "object" && device !== null)) - throw new Error("Error parsing JSON"); - if ( - !( - "deviceID" in device && - typeof device["deviceID"] === "string" - ) - ) - throw new Error("Error parsing JSON"); - if ( - !( - "introducedBy" in device && - typeof device["introducedBy"] === "string" - ) - ) - throw new Error("Error parsing JSON"); - if ( - !( - "encryptionPassword" in device && - typeof device["encryptionPassword"] === "string" - ) - ) - throw new Error("Error parsing JSON"); reducedDeviceInfos.push( - new ReducedSyncthingDevice( - device["deviceID"], - device["introducedBy"], - device["encryptionPassword"] - ) + ReducedSyncthingDeviceModel.fromJSON(JSON.stringify(device)) ); } return new SyncthingFolderModel( @@ -152,50 +116,36 @@ export class SyncthingDeviceModel extends SyncthingDevice { // TODO: to refactor w/ a function. if (!(typeof parsedJSON === "object" && parsedJSON !== null)) throw new Error("JSON is not an object or is null"); + logErrorIfNotValidJson(parsedJSON, "deviceID", "string"); + logErrorIfNotValidJson(parsedJSON, "introducedBy", "string"); + logErrorIfNotValidJson(parsedJSON, "paused", true); if ( !( "addresses" in parsedJSON && isStringArray(parsedJSON["addresses"]) ) || !( - "ignoredFolders" in parsedJSON && - isStringArray(parsedJSON["ignoredFolders"]) - ) || - !( - "deviceID" in parsedJSON && - typeof parsedJSON["deviceID"] === "string" - ) || - !( - "introducedBy" in parsedJSON && - typeof parsedJSON["introducedBy"] === "string" - ) || - !( - "encryptionPassword" in parsedJSON && - typeof parsedJSON["encryptionPassword"] === "string" + ("ignoredFolders" in parsedJSON) /*&&*/ + // (isStringArray(parsedJSON["ignoredFolders"]) || + // Array.isArray( + // parsedJSON["ignoredFolders"] && + // (parsedJSON["ignoredFolders"] as Array) + // .length === 0 + // )) ) || !( "name" in parsedJSON && - typeof parsedJSON["name"] === "string" && - typeof parsedJSON["name"] === "undefined" - ) || - !("type" in parsedJSON && typeof parsedJSON["type"] === "string") || - !( - "maxConflicts" in parsedJSON && - typeof parsedJSON["maxConflicts"] === "number" - ) || - !( - "paused" in parsedJSON && - typeof parsedJSON["paused"] === "boolean" + (typeof parsedJSON["name"] === "string" || + typeof parsedJSON["name"] === "undefined") ) ) - throw new Error("Error parsing JSON"); + throw new Error("Error validating JSON"); return new SyncthingDeviceModel( parsedJSON["deviceID"], parsedJSON["introducedBy"], - parsedJSON["encryptionPassword"], parsedJSON["addresses"], parsedJSON["paused"], - parsedJSON["ignoredFolders"], + parsedJSON["ignoredFolders"] as Array, // TODO: refactor this. parsedJSON["name"] ?? "" ); } @@ -204,3 +154,23 @@ export class SyncthingDeviceModel extends SyncthingDevice { return JSON.stringify(this); } } + +export class ReducedSyncthingDeviceModel extends ReducedSyncthingDevice { + static fromJSON(json: string): ReducedSyncthingDeviceModel { + const parsedJSON = JSON.parse(json); + if (!(typeof parsedJSON === "object" && parsedJSON !== null)) + throw new Error("Error parsing JSON"); + logErrorIfNotValidJson(parsedJSON, "deviceID", "string"); + logErrorIfNotValidJson(parsedJSON, "introducedBy", "string"); + logErrorIfNotValidJson(parsedJSON, "encryptionPassword", "string"); + return new ReducedSyncthingDeviceModel( + parsedJSON["deviceID"], + parsedJSON["introducedBy"], + parsedJSON["encryptionPassword"] + ); + } + + toJSON(): string { + throw new Error("Method not implemented."); + } +} diff --git a/src/views/configuration_modal.ts b/src/views/configuration_modal.ts new file mode 100644 index 0000000..9d9202b --- /dev/null +++ b/src/views/configuration_modal.ts @@ -0,0 +1,31 @@ +import { App, Modal } from "obsidian"; +import ConfigurationTable from "src/components/configuration_table.svelte"; +import SyncthingPlugin from "src/main"; +import { SvelteComponent } from "svelte"; + +export class ConfigurationModal extends Modal { + private components: SvelteComponent[] = []; + constructor(app: App, public plugin: SyncthingPlugin) { + super(app); + } + + async onOpen() { + this.modalEl.style.width = "100%"; + this.modalEl.style.height = "100%"; + + this.components.push( + new ConfigurationTable({ + target: this.contentEl, + props: { + parent: this, + }, + }) + ); + } + + onClose() { + const { contentEl } = this; + this.components.forEach((component) => component.$destroy()); + contentEl.empty(); + } +}