/*
index.tsx - ESP3D WebUI areas file
Copyright (c) 2020 Luc Lebosse. All rights reserved.
This code is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with This code; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
import { Fragment, FunctionalComponent, render, TargetedEvent } from "preact"
import { Menu } from "./menu"
import { iconsFeather } from "../components/Images"
import { machineSettings, iconsTarget } from "../targets"
import { ConnectionContainer } from "./connection"
import { MainContainer } from "./main"
import { useUiContext, useUiContextFn } from "../contexts/UiContext"
import { useModalsContext } from "../contexts/ModalsContext"
import { useToastsContext } from "../contexts/ToastsContext"
import {
generateValidationGlobal as generateValidation,
exportPreferences,
exportPreferencesSection,
importPreferencesSection,
} from "../tabs/interface"
import {
useSettingsContext,
useSettingsContextFn,
} from "../contexts/SettingsContext"
import { useSettings, useHttpQueue, useTargetCommands } from "../hooks"
import { useEffect } from "preact/hooks"
import { Field, FieldGroup } from "../components/Controls"
import { showKeepConnected, showModal } from "../components/Modal"
import { espHttpURL, dispatchToExtensions } from "../components/Helpers"
import { T, baseLangRessource } from "../components/Translations"
import { HelpCircle, Layout } from "preact-feather"
/*
* Local const
*
*/
const ViewContainer: FunctionalComponent = () => {
const { connection, dialogs } = useUiContext()
if (dialogs.showKeepConnected == true) {
dialogs.setShowKeepConnected(false)
showKeepConnected()
}
if (
connection.connectionState.connected &&
!connection.connectionState.updating
)
return (
)
else {
return
}
}
const ContentContainer: FunctionalComponent = () => {
const { getConnectionSettings, getInterfaceSettings } = useSettings()
const { connectionSettings, interfaceSettings, featuresSettings } =
useSettingsContext()
const { createNewRequest } = useHttpQueue()
const { targetCommands } = useTargetCommands()
const { toasts } = useToastsContext()
const { modals } = useModalsContext()
const processExtensionMessage = (eventMsg: any) => {
if (eventMsg.data.type && eventMsg.data.target == "webui") {
switch (eventMsg.data.type) {
case "response":
//TBD: if need real both way communication
//between iFrame and Main UI
break
case "cmd": {
const cmdCallbacks = {
onSuccess: (result: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"cmd",
{
status: "success",
response: result,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
onFail: (error: string) => {
console.log(error)
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"cmd",
{
status: "error",
error: error,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
}
targetCommands(eventMsg.data.content, undefined, undefined, cmdCallbacks)
break
}
case "query": {
let cmd: string | undefined = undefined
const queryCallbacks = {
onSuccess: (result: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"query",
{
status: "success",
response: result,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
onFail: (error: string) => {
console.log(error)
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"query",
{
status: "error",
error: error,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
}
if (eventMsg.data.url=="command" && !eventMsg.data.arg) {
console.log("Command")
console.log(eventMsg.data)
targetCommands(eventMsg.data.args.cmd, undefined, undefined, queryCallbacks)
} else {
// This does not fit into the targetCommands paradigm because
// either eventMsg.data.url is not "command" or eventMsg.data.args
// is not empty
createNewRequest(
espHttpURL(eventMsg.data.url, eventMsg.data.args),
{ method: "GET" },
queryCallbacks
)
}
createNewRequest(
espHttpURL(eventMsg.data.url, eventMsg.data.args),
{ method: "GET", echo: cmd },
{
onSuccess: (result: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"query",
{
status: "success",
response: result,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
onFail: (error: string) => {
console.log(error)
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"query",
{
status: "error",
error: error,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
}
)
break
}
case "upload": {
const formData = new FormData()
const file = new File(
[eventMsg.data.content],
eventMsg.data.filename
)
const initiator = {
type: "upload",
id: eventMsg.data.id,
url: eventMsg.data.url,
target: eventMsg.data.target,
path: eventMsg.data.path,
filename: eventMsg.data.filename,
size: eventMsg.data.size,
args: eventMsg.data.args,
noDispatch: eventMsg.data.noDispatch,
}
//TODO add support for additional POST arguments if needed
formData.append("path", eventMsg.data.path)
formData.append(
`${eventMsg.data.filename }S`,
eventMsg.data.size
)
formData.append("myfiles", file, eventMsg.data.filename)
createNewRequest(
espHttpURL(eventMsg.data.url, eventMsg.data.args),
{
method: "POST",
id: eventMsg.data.id,
body: formData,
},
{
onSuccess: (result: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"upload",
{
status: "success",
response: result,
initiator: initiator,
},
eventMsg.data.id
)
},
onFail: (error: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"upload",
{
status: "error",
error,
finitiator: initiator,
},
eventMsg.data.id
)
},
onProgress: (e: any) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"upload",
{
status: "progress",
progress: e,
initiator: initiator,
},
eventMsg.data.id
)
},
}
)
break
}
case "download":
createNewRequest(
espHttpURL(eventMsg.data.url, eventMsg.data.args),
{ method: "GET", id: "download" },
{
onSuccess: (result: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"download",
{
status: "success",
response: result,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
onFail: (error: string) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"download",
{
status: "error",
error: error,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
onProgress: (e: any) => {
if (!eventMsg.data.noDispatch)
dispatchToExtensions(
"download",
{
status: "progress",
progress: e,
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
}
)
break
case "toast":
toasts.addToast({
content: eventMsg.data.content.text,
type: eventMsg.data.content.type,
})
break
case "modal": {
let inputData: any = null
const validationBtn: any = {}
const content = eventMsg.data.content
const hasError = () => {
if (content.style == "fields") {
const stmp = JSON.stringify(content.fields)
if (
stmp.includes('haserror":true') ||
stmp.includes("haserror':true")
) {
return true
}
}
return false
}
const exportResult = () => {
const settingsValues: any = {}
if (content.style == "fields") {
settingsValues.values = inputData
settingsValues.export = exportPreferencesSection(
settingsValues,
false
)
} else {
settingsValues.export = inputData
}
return settingsValues.export
}
const cb1 = () => {
if (
(content.style == "fields" ||
content.style == "input") &&
content.validation == "bt1"
) {
if (hasError()) {
return
}
modals.removeModal(modals.getModalIndex(content.id))
}
setTimeout(() => {
dispatchToExtensions(
"modal",
{
response: content.response1,
inputData:
content.validation == "bt1"
? exportResult()
: "",
initiator: eventMsg.data,
},
eventMsg.data.id
)
}, 500)
}
const cb2 = () => {
if (
(content.style == "fields" ||
content.style == "input") &&
content.validation == "bt2"
) {
if (hasError()) {
return
}
modals.removeModal(modals.getModalIndex(content.id))
}
setTimeout(() => {
dispatchToExtensions(
"modal",
{
response: content.response2,
inputData:
content.validation == "bt2"
? exportPreferences(
exportResult(),
false
)
: "",
initiator: eventMsg.data,
},
eventMsg.data.id
)
}, 500)
}
if (content.style == "fields") {
if (content.validation == "bt1") {
validationBtn.id = content.bt1Id
? content.bt1Id
: "bt1"
}
if (content.validation == "bt2") {
validationBtn.id = content.bt2Id
? content.bt2Id
: "bt2"
}
}
if (content.style == "input") {
inputData = content.value
}
const modalContent: any = {}
if (content.style == "fields") {
//merge format and fields
const result = importPreferencesSection(
content.fields,
content.values
)
inputData = result.preferences
//This function is a replacement of the hook feature which is not available in this context
const checkValidation = (fieldData: any) => {
const id_group = `group-${ fieldData.id}`
if (typeof fieldData.initial == "undefined") {
fieldData.initial = fieldData.value
}
const validation = generateValidation(fieldData)
const element = document.getElementById(id_group)
if (!element) return
const divToRemove =
element.getElementsByClassName(
"form-input-hint"
)
if (divToRemove.length > 0) {
element.removeChild(divToRemove[0])
}
if (validation.modified && validation.valid) {
element.classList.add("has-modification")
const newDiv = document.createElement("div")
render(validation.message, newDiv)
newDiv.classList.add(
"form-input-hint",
"text-center"
)
element.appendChild(newDiv)
} else {
const groupElement = document.getElementById(id_group)
if (groupElement) {
groupElement.classList.remove("has-modification")
}
}
if (!validation.valid) {
const groupElement = document.getElementById(id_group)
if (groupElement) {
groupElement.classList.add("has-error")
}
const newDiv = document.createElement("div")
newDiv.innerHTML = T(validation.message)
newDiv.classList.add(
"form-input-hint",
"text-center"
)
element.appendChild(newDiv)
} else {
const groupElement = document.getElementById(id_group)
if (groupElement) {
groupElement.classList.remove("has-error")
}
}
const validationBtnElement = document.getElementById(validationBtn.id)
if (validationBtnElement) {
if (hasError()) {
validationBtnElement.style.visibility = "hidden"
} else {
validationBtnElement.style.visibility = "visible"
}
}
}
const renderFields = () => {
const section = inputData
return Object.keys(section).map((subsectionId) => {
const fieldData = section[subsectionId]
//console.log(fieldData)
if (fieldData.type == "group") {
return (
{Object.keys(fieldData.value).map(
(subData) => {
const subFieldData =
fieldData.value[subData]
const {
label,
initial,
type,
...rest
} = subFieldData
return (
{
if (!update) {
subFieldData.value =
val
}
checkValidation(
subFieldData
)
}}
{...rest}
/>
)
}
)}
)
} else {
const { label, initial, type, ...rest } =
fieldData
return (
{
if (!update) {
fieldData.value = val
}
checkValidation(fieldData)
}}
/>
)
}
})
}
modalContent.content = renderFields()
} else {
modalContent.content = T(content.text)
}
let modal_content = modalContent.content
if (content.style != "fields") {
modal_content = ()
}
showModal({
modals,
title: T(content.title),
button2: content.bt2Txt
? {
cb: cb2,
text: T(content.bt2Txt),
id: "bt2",
noclose:
content.validation == "bt2"
? true
: false,
}
: undefined,
button1: content.bt1Txt
? {
cb: cb1,
text: T(content.bt1Txt),
id: "bt1",
noclose:
content.validation == "bt1"
? true
: false,
}
: undefined,
icon:
content.icon ? iconsFeather[content.icon as keyof typeof iconsFeather] : content.style == "question" ? (
) : (
),
id: content.id,
content: (
{modal_content}
{content.style == "input" && (
) => {
inputData = e.currentTarget.value.trim()
}}
value={content.value}
/>
)}
),
hideclose: content.hideclose,
overlay: content.overlay,
})
break
}
case "sound":
if (eventMsg.data.content == "beep") useUiContextFn.beep()
if (eventMsg.data.content == "error")
useUiContextFn.beepError()
if (eventMsg.data.content == "seq")
useUiContextFn.beepSeq(eventMsg.data.seq)
break
case "translate":
if (eventMsg.data.all) {
dispatchToExtensions(
"translate",
{
response: baseLangRessource,
initiator: eventMsg.data,
},
eventMsg.data.id
)
} else {
dispatchToExtensions(
"translate",
{
response: T(eventMsg.data.content),
initiator: eventMsg.data,
},
eventMsg.data.id
)
}
break
case "icon": {
const iconToSend = iconsFeather[eventMsg.data.id as keyof typeof iconsFeather]
let iconSvgString = ""
if (iconToSend) {
//Temporary DOM
const tempElement = document.createElement("div")
//DO icon rendering
render(iconToSend, tempElement)
//Get the SVG string
iconSvgString = tempElement.firstChild ? (tempElement.firstChild as Element).outerHTML : ""
//Delete the temporary DOM
tempElement.remove()
} else {
iconSvgString = ""
console.error("Icon not found:", eventMsg.data.id)
}
dispatchToExtensions(
"icon",
{
response: iconSvgString.replaceAll("\"", "'"),
initiator: eventMsg.data,
},
eventMsg.data.id
)
break
}
case "extensionsData": {
//Get extension name
const section = eventMsg.data.id
//Get extensions settings
const data = eventMsg.data.content
//Some sanity check
if (!interfaceSettings.current.extensions) {
interfaceSettings.current.extensions = {}
}
if (!interfaceSettings.current.extensions[section]) {
interfaceSettings.current.extensions[section] = {}
}
//Update the settings
//Note: it will overwrite the whole section
interfaceSettings.current.extensions[section] = data
//now do a copy of interfaceSettings
const interfaceSettingsData = JSON.parse(
JSON.stringify(interfaceSettings.current)
)
//now update the settings section with export version
interfaceSettingsData.settings = exportPreferencesSection(interfaceSettingsData.settings, false, true)
//now stringify and save
const preferencestosave = JSON.stringify(
interfaceSettingsData,
null,
" "
)
//Create a blob
const blob = new Blob([preferencestosave], {
type: "application/json",
})
//Create a file
const preferencesFileName =
`${useSettingsContextFn.getValue("HostUploadPath")
}preferences.json`
const formDataExtensions = new FormData()
const file_to_save = new File([blob], preferencesFileName)
formDataExtensions.append(
"path",
useSettingsContextFn.getValue("HostUploadPath")
)
formDataExtensions.append("creatPath", "true")
formDataExtensions.append(
`${preferencesFileName }S`,
preferencestosave.length.toString()
)
formDataExtensions.append(
"myfiles",
file_to_save,
preferencesFileName
)
//Send the file
createNewRequest(
espHttpURL(useSettingsContextFn.getValue("HostTarget")),
{
method: "POST",
id: "preferences",
body: formDataExtensions,
},
{
onSuccess: (result: string) => {
dispatchToExtensions(
"extensionsData",
{
response: { status: "success" },
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
onFail: (error: string) => {
dispatchToExtensions(
"extensionsData",
{
response: { status: "error" },
initiator: eventMsg.data,
},
eventMsg.data.id
)
},
}
)
break
}
case "capabilities": {
let response: any = {}
switch (eventMsg.data.id) {
case "connection":
response = JSON.parse(
JSON.stringify(connectionSettings.current)
)
break
case "settings":
response = JSON.parse(
JSON.stringify(machineSettings)
)
break
case "interface":
response = JSON.parse(
JSON.stringify(interfaceSettings.current)
)
break
case "features":
response = JSON.parse(
JSON.stringify(featuresSettings.current)
)
break
case "extensions":
if (interfaceSettings.current.extensions) {
if (
interfaceSettings.current.extensions[
eventMsg.data.name
]
) {
response = JSON.parse(
JSON.stringify(
interfaceSettings.current
.extensions[eventMsg.data.name]
)
)
} else {
response = "{}"
}
} else {
response = "{}"
}
break
default:
response = {}
}
dispatchToExtensions(
"capabilities",
{
response: response,
initiator: eventMsg.data,
},
eventMsg.data.id
)
break
}
case "dispatch":
dispatchToExtensions(
"dispatch",
{
response: eventMsg.data.content,
initiator: eventMsg.data,
},
eventMsg.data.targetid
)
break
default:
//core and stream are only supposed to come from ESP3D or main FW
return
}
}
}
useEffect(() => {
getConnectionSettings(getInterfaceSettings)
window.addEventListener("message", processExtensionMessage, false)
}, [])
return
}
export { ContentContainer }