/* index.tsx - ESP3D WebUI navigation tab 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, TargetedMouseEvent, JSX } from "preact" import { useState, useRef } from "preact/hooks" import { useSettingsContext, useSettingsContextFn, useUiContextFn, useToastsContext, } from "../../contexts" import { ButtonImg, Loading } from "../../components/Controls" import { useHttpQueue, useSettings } from "../../hooks" import { espHttpURL, checkDependencies, } from "../../components/Helpers" import { T } from "../../components/Translations" import { RefreshCcw, Save, ExternalLink, Flag, Download } from "preact-feather" import { Field, FieldGroup } from "../../components/Controls" import { exportPreferences, exportPreferencesSection, ExportPreferences, InterfaceSettingsData } from "./exportHelper" import { importPreferencesSection, formatPreferences, ImportPreferencesResult } from "./importHelper" // Option for select fields interface SelectOption { label: string; value: string; depend?: any[]; } // Field data interface interface FieldData { id: string; type: string; value: any; initial: any; label?: string; depend?: any[]; shortkey?: boolean; step?: number; min?: number | string; max?: number | string; minSecondary?: number; minsecondary?: number; options?: SelectOption[]; name?: string; regexpattern?: string; nb?: number; haserror?: boolean; hasmodified?: boolean; newItem?: boolean; hide?: boolean; } // Validation result interface interface ValidationResult { message: JSX.Element | string | null; valid: boolean; modified: boolean; } const isDependenciesMet = (depend: any): boolean => { const { interfaceSettings, connectionSettings } = useSettingsContext() return checkDependencies(depend, interfaceSettings.current.settings, connectionSettings.current) } const generateValidationGlobal = ( fieldData: FieldData, isFlashFS?: boolean, isSDFS?: boolean, connectionSettings?: any, interfaceSettings?: any, setShowSave?: (value: boolean) => void, checkSaveStatus?: () => boolean ): ValidationResult => { const validation: ValidationResult = { message: , valid: true, modified: true, } if (fieldData.shortkey && interfaceSettings) { if (fieldData.value.length > 0) { if (fieldData.value.endsWith("+")) { validation.message = T("S214") validation.valid = false console.log("Error") } //look if used const keysRefs = [ { ref: interfaceSettings.current.settings.jog, entry: "keymap", }, { ref: interfaceSettings.current.settings.macros, entry: "macros", }, ] keysRefs.forEach((list) => { let keysmap = list.ref.find((element: any) => { if (element.id == list.entry) return true }) if (keysmap) { let counter = 0 keysmap.value.forEach((element: any) => { element.value.forEach((sub: any) => { if ( sub.name == "key" && sub.value == fieldData.value && sub.id != fieldData.id ) { counter++ } }) }) if (counter != 0) { validation.message = T("S213") validation.valid = false console.log("Error") } } }) } } else { if (typeof fieldData.step !== "undefined") { //hack to avoid float precision issue const mult = (1 / fieldData.step).toFixed(0) > "0" ? parseFloat((1 / fieldData.step).toFixed(0)) : 1 const valueMult = Math.round(fieldData.value * mult) const stepMult = Math.round(fieldData.step * mult) if (valueMult % stepMult != 0) { validation.message = validation.valid = false console.log("Error") } } if (fieldData.type == "list") { const stringified = JSON.stringify(fieldData.value) //check new item or modified item if ( stringified.includes('"newItem":true') || fieldData.nb != fieldData.value.length ) fieldData.hasmodified = true else fieldData.hasmodified = stringified.includes('"hasmodified":true') //check order change fieldData.value.forEach((element: any, index: number) => { if (element.index != index) fieldData.hasmodified = true }) validation.valid = !stringified.includes('"haserror":true') } if (fieldData.type === "text") { if (fieldData.regexpattern) { const regex = new RegExp(fieldData.regexpattern) if (!regex.test(fieldData.value)) { validation.valid = false console.log("Error") } } // Text length validation if (fieldData.min !== undefined) { const minValue = typeof fieldData.min === 'number' ? fieldData.min : parseInt(fieldData.min || '0') if (fieldData.value.trim().length < minValue) { validation.valid = false console.log("Error") } else if (fieldData.minSecondary != undefined) { if ( fieldData.value.trim().length < fieldData.minSecondary && fieldData.value.trim().length > minValue ) { validation.valid = false console.log("Error") } } } if (fieldData.max) { const maxValue = typeof fieldData.max === 'number' ? fieldData.max : parseInt(fieldData.max) if (fieldData.value.trim().length > maxValue) { validation.valid = false console.log("Error") } } } else if (fieldData.type == "number") { // Number range validation if (fieldData.max !== undefined) { const maxValue = typeof fieldData.max === 'number' ? fieldData.max : parseInt(fieldData.max) if (fieldData.value > maxValue) { validation.valid = false console.log("Error") } } if (fieldData.min !== undefined) { const minValue = typeof fieldData.min === 'number' ? fieldData.min : parseInt(fieldData.min) const minSecondaryValue = fieldData.minsecondary ?? 0 if (fieldData.minSecondary != undefined || fieldData.minsecondary != undefined) { if ( fieldData.value != minValue && fieldData.value < minSecondaryValue ) { validation.valid = false console.log("Error") } } else if (fieldData.value < minValue) { validation.valid = false console.log("Error") } } } else if (fieldData.type == "select") { const opt = fieldData.options?.find( (element) => element.value === fieldData.value ) if (opt && opt.depend) { if (interfaceSettings && connectionSettings) { const canshow = checkDependencies( opt.depend, interfaceSettings.current.settings, connectionSettings.current ) if (!canshow) { validation.valid = false console.log("Error") } } } if ( fieldData.name == "type" && fieldData.value == "camera" && interfaceSettings ) { //Update camera source automaticaly //Note: is there a less complexe way to do ? const sourceId = fieldData.id.split("-")[0] const extraList = interfaceSettings.current.settings.extracontents //look for extra panels entry const subextraList = extraList[ extraList.findIndex((element: any) => { return element.id == "extracontents" }) ].value //look for extra panel specific id const datavalue = subextraList[ subextraList.findIndex((element: any) => { return element.id == sourceId }) ].value //get source item const sourceItemValue = datavalue[ datavalue.findIndex((element: any) => { return element.id == `${sourceId }-source` }) ] //force /snap as source sourceItemValue.value = "/snap" } const index = fieldData.options?.findIndex((element) => { return ( (parseInt(element.value) === parseInt(fieldData.value) && !isNaN(parseInt(element.value))) || element.value == fieldData.value ) }) if (index == -1) { validation.valid = false console.log("Error") } } } if (!validation.valid) { if (!fieldData.shortkey) validation.message = T("S42") } fieldData.haserror = !validation.valid if (fieldData.type !== "list") { if (fieldData.value === fieldData.initial) { fieldData.hasmodified = false } else { fieldData.hasmodified = true } if (fieldData.newItem) fieldData.hasmodified = true } if ( (isFlashFS !== undefined || isSDFS !== undefined) && setShowSave !== undefined && checkSaveStatus !== undefined ) { if (isFlashFS || isSDFS) { setShowSave(checkSaveStatus()) } else { setShowSave(false) } } if (!fieldData.hasmodified && !fieldData.haserror) { validation.message = null validation.valid = true validation.modified = false } if (!validation.valid) { console.log(fieldData) } return validation } const InterfaceTab = () => { const { toasts } = useToastsContext() const { createNewRequest, abortRequest } = useHttpQueue() const { getInterfaceSettings } = useSettings() const { interfaceSettings, connectionSettings } = useSettingsContext() const [isLoading, setIsLoading] = useState(false) const [showSave, setShowSave] = useState(true) const inputFile = useRef(null) console.log("Interface") const isFlashFS = useSettingsContextFn.getValue("FlashFileSystem") == "none" ? false : true const isSDFS = useSettingsContextFn.getValue("SDConnection") == "none" ? false : true const generateValidation = (fieldData: FieldData): ValidationResult => { return generateValidationGlobal( fieldData, isFlashFS, isSDFS, connectionSettings, interfaceSettings, setShowSave, checkSaveStatus ) } function checkSaveStatus(): boolean { const stringified = JSON.stringify(interfaceSettings.current.settings) const hasmodified = stringified.includes('"hasmodified":true') const haserrors = stringified.includes('"haserror":true') return !haserrors && hasmodified } const getInterface = () => { useUiContextFn.haptic() setIsLoading(true) getInterfaceSettings(setIsLoading) } const fileSelected = () => { if (inputFile.current && inputFile.current.files && inputFile.current.files.length > 0) { setIsLoading(true) const reader = new FileReader() reader.onload = function (e) { try { const result = e.target?.result if (typeof result !== 'string') { throw new Error("File result is not a string") } const importData = JSON.parse(result) const importResult: ImportPreferencesResult = importPreferencesSection( interfaceSettings.current.settings, importData.settings ) interfaceSettings.current.settings = importResult.preferences if (importData.custom) { interfaceSettings.current.custom = importData.custom } if (importData.extensions) { interfaceSettings.current.extensions = importData.extensions } formatPreferences(interfaceSettings.current.settings) //console.log("Imported") //console.log(interfaceSettings.current) if (importResult.hasErrors) { toasts.addToast({ content: "S56", type: "error" }) console.log("Error") } } catch (e) { console.log(e) console.log("Error") toasts.addToast({ content: "S56", type: "error" }) } finally { setIsLoading(false) } } reader.readAsText(inputFile.current.files[0]) } } const SaveSettings = () => { const settings_to_save: ExportPreferences = exportPreferences( interfaceSettings.current as InterfaceSettingsData, false ) const preferencestosave = JSON.stringify(settings_to_save, null, " ") const blob = new Blob([preferencestosave], { type: "application/json", }) const preferencesFileName = `${useSettingsContextFn.getValue("HostUploadPath") }preferences.json` const formData = new FormData() const file = new File([blob], preferencesFileName) formData.append("path", useSettingsContextFn.getValue("HostUploadPath")) formData.append("creatPath", "true") formData.append("myfiles", file, preferencesFileName) formData.append(`${preferencesFileName }S`, String(preferencestosave.length)) setIsLoading(true) createNewRequest( espHttpURL(useSettingsContextFn.getValue("HostTarget")), { method: "POST", id: "preferences", body: formData }, { onSuccess: (result: string) => { setTimeout(() => { window.location.reload() }, 1000) }, onFail: (error: string) => { setIsLoading(false) }, } ) } //console.log(JSON.stringify(interfaceSettings.current, null, 2)) return (

{T("S17")}

{isLoading && } {!isLoading && ( {interfaceSettings.current.settings && (
{Object.keys( interfaceSettings.current.settings ).map((sectionId) => { const section = interfaceSettings.current.settings[ sectionId ] return (
{Object.keys(section).map( (subsectionId) => { const fieldData: FieldData = section[ subsectionId ] if ( fieldData.type == "group" ) { //show group if ( fieldData.depend ) { if ( !isDependenciesMet( fieldData.depend ) ) { return } } return ( {Object.keys( fieldData.value ).map( ( subData ) => { const subFieldData: FieldData = fieldData .value[ subData ] const [ validation, setvalidation, ] = useState() const { label, initial, type, ...rest } = subFieldData return ( { if ( !update ) { subFieldData.value = val } setvalidation( generateValidation( subFieldData ) ) }} validation={ validation } /> ) } )} ) } else if ( !fieldData.hide ) { const [ validation, setvalidation, ] = useState() const { label, initial, type, ...rest } = fieldData return ( { if ( !update ) { fieldData.value = val } setvalidation( generateValidation( fieldData ) ) }} validation={ validation } /> ) } } )}
) })}
)}
)}

} onClick={getInterface} /> } onClick={(e: TargetedMouseEvent) => { useUiContextFn.haptic() e.currentTarget.blur() if (inputFile.current) { inputFile.current.value = "" inputFile.current.click() } }} /> } onClick={(e: TargetedMouseEvent) => { useUiContextFn.haptic() e.currentTarget.blur() //console.log(interfaceSettings.current) exportPreferences(interfaceSettings.current as InterfaceSettingsData) }} /> {showSave && ( } onClick={(e: TargetedMouseEvent) => { useUiContextFn.haptic() e.currentTarget.blur() SaveSettings() }} /> )}
) } export { InterfaceTab, generateValidationGlobal, exportPreferences, exportPreferencesSection, importPreferencesSection, formatPreferences, }