import { FunctionalComponent, JSX } from "preact" import { useEffect, useState, useRef } from "preact/hooks" import { ButtonImg, Loading, CenterLeft, Progress } from "../../components/Controls" import { useHttpQueue, useTargetCommands } from "../../hooks" import { useWebSocketService } from "../../hooks/useWebSocketService" import { espHttpURL } from "../../components/Helpers" import { T } from "../../components/Translations" import { useUiContext, useModalsContext, useToastsContext, useUiContextFn, useSettingsContext, useSettingsContextFn, } from "../../contexts" import { Esp3dVersion, webUIversion } from "../../components/App/version" import { Github, RefreshCcw, UploadCloud, LifeBuoy, Info, BookOpen, Download } from "preact-feather" import { webUiUrl, fwUrl, Name, restartdelay } from "../../targets" import { showConfirmationModal, showModal, showProgressModal, showReleaseNotesModal, showFirmwareUpdateModal, } from "../../components/Modal" import { GitHubService } from "../../Services/GitHubService" import type { GitHubRelease } from "../../types/github.types" import { VersionBadge } from "../../components/VersionBadge" interface AboutData { id: string value: string } interface ProgressBar { update?: (value: number) => void } let about: AboutData[] = [] const defaultHelpUrl = "http://wiki.fluidnc.com/" const CustomEntry: FunctionalComponent = (): JSX.Element => { const { interfaceSettings } = useSettingsContext() let HelpEntry: JSX.Element | null = null let InfoEntry: JSX.Element | null = null if ( interfaceSettings.current.custom && (interfaceSettings.current.custom.help || interfaceSettings.current.custom.information) ) { if (interfaceSettings.current?.custom?.help) { const helpUrl = interfaceSettings.current.custom.help const onClickHelp = (e: MouseEvent) => { useUiContextFn.haptic() if (helpUrl) (window as any).open(helpUrl, "_blank") ;(e.target as HTMLElement).blur() } HelpEntry = } label={T("S72")} onClick={onClickHelp} /> } if (interfaceSettings.current?.custom?.information) { const infoUrl = interfaceSettings.current.custom.information const onClickInfo = (e: MouseEvent) => { useUiContextFn.haptic() if (infoUrl) (window as any).open(infoUrl, "_blank") ;(e.target as HTMLElement).blur() } InfoEntry = } label={T("S123")} onClick={onClickInfo} /> } return (
  • {HelpEntry} {InfoEntry}
  • ) } const onClickHelp = (e: MouseEvent) => { useUiContextFn.haptic() ;(window as any).open(defaultHelpUrl, "_blank") ;(e.target as HTMLElement).blur() } HelpEntry = ( } label="FluidNC Wiki" onClick={onClickHelp} /> ) return (
  • {T("S225")}: {HelpEntry}
  • ) } const About: FunctionalComponent = (): JSX.Element => { console.log("about") const { uisettings } = useUiContext() const { toasts } = useToastsContext() const { modals } = useModalsContext() const webSocketService = useWebSocketService() const { createNewRequest, abortRequest } = useHttpQueue() const { targetCommands } = useTargetCommands() const { interfaceSettings, connectionSettings } = useSettingsContext() const [isLoading, setIsLoading] = useState(true) const progressBar: ProgressBar = {} const [props, setProps] = useState([...about]) const [isFwUpdate, setIsFwUpdate] = useState(false) const [latestRelease, setLatestRelease] = useState(null) const [availableReleases, setAvailableReleases] = useState([]) const [isCheckingUpdates, setIsCheckingUpdates] = useState(false) const [latestFirmwareRelease, setLatestFirmwareRelease] = useState(null) const [availableFirmwareReleases, setAvailableFirmwareReleases] = useState([]) const inputFilesRef = useRef(null) const isFlashFS = connectionSettings.current.FlashFileSystem == "none" ? false : true const isSDFS = connectionSettings.current.SDConnection == "none" ? false : true const getProps = (): void => { setIsLoading(true) const callbacks = { onSuccess: (result: any) => { const jsonResult = JSON.parse(result) if (jsonResult.cmd != 420 || jsonResult.status == "error" || !jsonResult.data) { toasts.addToast({ content: T("S194"), type: "error" }) setIsLoading(false) return } setProps([...jsonResult.data]) about = [...jsonResult.data] setIsLoading(false) }, onFail: (error: any) => { setIsLoading(false) toasts.addToast({ content: error, type: "error" }) console.log(error) }, } targetCommands("[ESP420]json=yes", undefined, undefined, callbacks) } //from https://stackoverflow.com/questions/5916900/how-can-you-detect-the-version-of-a-browser function getBrowserInformation(): string { var ua = navigator.userAgent, tem: any, M: any = ua.match(/(opera|chrome|safari|firefox|msie|trident(?=\/))\/?\s*(\d+)/i) || [] if (/trident/i.test(M[1])) { tem = /\brv[ :]+(\d+)/g.exec(ua) || [] return `IE ${tem[1] || ""}` } if (M[1] === "Chrome") { tem = ua.match(/\b(OPR|Edge)\/(\d+)/) if (tem != null) return tem.slice(1).join(" ").replace("OPR", "Opera") } M = M[2] ? [M[1], M[2]] : [navigator.appName, navigator.appVersion, "-?"] if ((tem = ua.match(/version\/(\d+)/i)) != null) M.splice(1, 1, tem[1]) return M.join(" ") } const onFWUpdate = (e: MouseEvent) => { useUiContextFn.haptic() ;(e.target as HTMLElement).blur() const uploadFromDisk = () => { setIsFwUpdate(true) if (inputFilesRef.current) { inputFilesRef.current.value = "" inputFilesRef.current.accept = ".bin" inputFilesRef.current.multiple = false inputFilesRef.current.click() } } const downloadFromGithub = () => { const currentFirmwareVersion = props.find((element) => element.id == "FW version")?.value || "Unknown" showFirmwareUpdateModal({ modals, releases: availableFirmwareReleases, currentVersion: currentFirmwareVersion, onUploadFile: () => { setIsFwUpdate(true) if (inputFilesRef.current) { inputFilesRef.current.value = "" inputFilesRef.current.accept = ".bin" inputFilesRef.current.multiple = false inputFilesRef.current.click() } }, onViewReleaseNotes: () => { const releasesToShow = availableFirmwareReleases.length > 0 ? availableFirmwareReleases.slice(0, 10) : [] if (releasesToShow.length === 0) { toasts.addToast({ content: "No firmware release information available", type: "error", }) return } showReleaseNotesModal({ modals, releases: releasesToShow, githubUrl: "https://github.com/bdring/FluidNC/releases", }) }, }) } showModal({ modals, title: "Update Firmware", content: (

    Choose an update method:

    Download firmware release, extract, then upload .bin file
    ), id: "firmware-update-choice", hideclose: false, }) } const onFWGit = (e: MouseEvent) => { useUiContextFn.haptic() const i = useSettingsContextFn.getValue("Screen") let url = "" if (interfaceSettings.current.custom && interfaceSettings.current.custom.fwurl) { url = interfaceSettings.current.custom.fwurl } else if (i && i != "none" && (fwUrl as readonly any[]).length > 1) { url = (fwUrl as readonly any[])[1] } else { url = fwUrl[0] || "" } ;(window as any).open(url, "_blank") ;(e.target as HTMLElement).blur() } const checkForUpdates = async () => { setIsCheckingUpdates(true) try { const githubService = new GitHubService({ owner: "michmela44", repo: "ESP3D-WEBUI", assetName: "index.html.gz", }) const latest = await githubService.getLatestRelease() setLatestRelease(latest) const releases = await githubService.getReleases(10) setAvailableReleases(releases) } catch (error) { console.error("Failed to check for updates:", error) } finally { setIsCheckingUpdates(false) } } const checkForFirmwareUpdates = async () => { try { const githubService = new GitHubService({ owner: "bdring", repo: "FluidNC", assetName: "", }) const latest = await githubService.getLatestRelease() setLatestFirmwareRelease(latest) const releases = await githubService.getReleases(10) setAvailableFirmwareReleases(releases) } catch (error) { console.error("Failed to check for firmware updates:", error) } } const showDownloadModal = () => { if (availableReleases.length === 0) { toasts.addToast({ content: "No releases available. Check your internet connection.", type: "error", }) return } let selectedReleaseIndex = 0 const downloadSelectedRelease = () => { const selectedRelease = availableReleases[selectedReleaseIndex] const githubService = new GitHubService({ owner: "michmela44", repo: "ESP3D-WEBUI", assetName: "index.html.gz", }) const asset = githubService.findAssetByName(selectedRelease, "index.html.gz") if (!asset) { toasts.addToast({ content: `Asset 'index.html.gz' not found in release ${selectedRelease.tag_name}`, type: "error", }) return } const downloadUrl = asset.browser_download_url const link = document.createElement("a") link.href = downloadUrl link.download = "index.html.gz" document.body.appendChild(link) link.click() document.body.removeChild(link) const downloadModalIndex = modals.getModalIndex("github-download") if (downloadModalIndex !== -1) { modals.removeModal(downloadModalIndex) } toasts.addToast({ content: `Downloading ${selectedRelease.tag_name}. Use 'Upload to Device' button after download completes.`, type: "success", }) } const cancelDownload = () => { const downloadModalIndex = modals.getModalIndex("github-download") if (downloadModalIndex !== -1) { modals.removeModal(downloadModalIndex) } } const compareVersions = (current: string, target: string): string => { const cleanCurrent = current.split(".").slice(0, 3).join(".") const cleanTarget = target.replace(/^v/, "").split(".").slice(0, 3).join(".") const currentParts = cleanCurrent.split(".").map(Number) const targetParts = cleanTarget.split(".").map(Number) for (let i = 0; i < 3; i++) { if ((currentParts[i] || 0) < (targetParts[i] || 0)) return "upgrade" if ((currentParts[i] || 0) > (targetParts[i] || 0)) return "downgrade" } return "same" } showModal({ modals, title: "Download Web UI from GitHub", content: (
    Current Version {webUIversion}
    {latestRelease && ( <>
    {compareVersions(webUIversion, latestRelease.tag_name) === "upgrade" && ( Upgrade Available )} {compareVersions(webUIversion, latestRelease.tag_name) === "same" && ( Up to date )}
    Latest Release {latestRelease.tag_name}
    )}
    {availableReleases[0]?.tag_name} - Released{" "} {new Date(availableReleases[0]?.published_at || "").toLocaleDateString()}
    ), button1: { text: "Download", cb: downloadSelectedRelease, noclose: true, }, button2: { text: "Cancel", cb: cancelDownload, noclose: true, }, id: "github-download", hideclose: false, }) } const showReleaseNotes = () => { const releasesToShow = availableReleases.length > 0 ? availableReleases.slice(0, 10) : [] if (releasesToShow.length === 0) { toasts.addToast({ content: "No release information available", type: "error", }) return } showReleaseNotesModal({ modals, releases: releasesToShow, githubUrl: "https://github.com/michmela44/ESP3D-WEBUI/releases", }) } const onWebUiUpdate = (e: MouseEvent) => { useUiContextFn.haptic() ;(e.target as HTMLElement).blur() showModal({ modals, title: "Update Web UI", content: (

    Choose an update method:

    Download a release, then upload it to your device
    Select an index.html.gz file you already have
    ), id: "webui-upload", hideclose: false, }) } const onWebUiGit = (e: MouseEvent) => { useUiContextFn.haptic() ;(window as any) .open( webUiUrl, "_blank" )(e.target as HTMLElement) .blur() } const uploadFiles = (renameToIndexHtml = false): void => { const list = inputFilesRef.current?.files if (!list) return const formData = new FormData() formData.append("path", useSettingsContextFn.getValue("HostUploadPath")) formData.append("createPath", "true") if (list.length > 0) { for (let i = 0; i < list.length; i++) { const file = list[i] const fileName = renameToIndexHtml && file.name.endsWith(".html.gz") ? "index.html.gz" : file.name const arg = `${useSettingsContextFn.getValue("HostUploadPath") + fileName}S` formData.append(arg, String(file.size)) formData.append("myfiles", file, useSettingsContextFn.getValue("HostUploadPath") + fileName) } } showProgressModal({ modals, title: T("S32"), button1: { cb: abortRequest, text: T("S28") }, content: , }) const base = isFwUpdate ? "updatefw" : useSettingsContextFn.getValue("HostTarget") console.log(base) createNewRequest( espHttpURL(base), { method: "POST", id: "upload", body: formData }, { onSuccess: (_result: any) => { if (progressBar.update && typeof progressBar.update === "function") progressBar.update(100) modals.removeModal(modals.getModalIndex("upload")) webSocketService.disconnect(isFwUpdate ? "restart" : "connecting") if (isFwUpdate) { setTimeout(() => { window.location.reload() }, restartdelay * 1000) } else window.location.reload() }, onFail: (error: any) => { modals.removeModal(modals.getModalIndex("upload")) toasts.addToast({ content: error, type: "error" }) }, onProgress: (e: number) => { if (progressBar.update && typeof progressBar.update === "function") progressBar.update(e) }, } ) } const valueTranslated = (value: string): string => { if (value.startsWith("ON (") || value.startsWith("OFF (") || value.startsWith("shared (")) { const reg_search = /(?