From c68cf0e1b9ca9def980622eecafd9462ae22b74c Mon Sep 17 00:00:00 2001 From: Luc <8822552+luc-github@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:21:36 +0800 Subject: [PATCH 01/28] split container and content for better readibility - WIP - POC Add div that is the place the cached content are stored because content are never destroyed but only hidden Add elementsCache tool to handle communication between container and content Adapt fullscreen button with new logic for extra content --- src/areas/elementsCache.js | 140 ++++++ src/components/App/index.js | 2 + src/components/Controls/FullScreenButton.js | 24 +- .../ExtraContent/extraContentItem.js | 185 +++++++ src/components/ExtraContent/index.js | 456 +++++------------- src/components/Panels/ExtraPanel.js | 2 +- src/pages/extrapages/index.js | 2 +- src/style/components/_app.scss | 16 + 8 files changed, 482 insertions(+), 345 deletions(-) create mode 100644 src/areas/elementsCache.js create mode 100644 src/components/ExtraContent/extraContentItem.js diff --git a/src/areas/elementsCache.js b/src/areas/elementsCache.js new file mode 100644 index 00000000..9e920745 --- /dev/null +++ b/src/areas/elementsCache.js @@ -0,0 +1,140 @@ +/* + elementsCache.js - ESP3D WebUI MainPage file + + Copyright (c) 2020 Luc Lebosse. All rights reserved. + Original code inspiration : 2021 Alexandre Aussourd + + 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 { h, render } from "preact" +import { ExtraContentItem } from "../components/ExtraContent" + +const ElementsCache = () => { + return (
) +} + +const elementsCache = { + + has: (id) => { + const cacheHost = document.getElementById("elementsCache") + if (!cacheHost) return false + return cacheHost.querySelector('#' + id) !== null + }, + + create: (id, props) => { + if (!elementsCache.has(id)) { + const cacheHost = document.getElementById("elementsCache") + console.log("Creating element, because it doesn't exist: " + id) + console.log("Current host size is " + cacheHost.children.length) + try { + const container = document.getElementById("elementsCache") + const vnode = container.appendChild(document.createElement('div')) + vnode.id = "new_element" + id + const new_vnode = render(, container, vnode) + console.log("Element created: " + id + ", exists in cache: " + elementsCache.has(id)) + console.log("Now Current host size is " + cacheHost.children.length) + console.log(cacheHost.innerHTML) + return true + } catch (error) { + console.error(`Error creating element ${id}:`, error) + return false + } + } else { + //console.log("Element already exists: " + id) + return true + } + }, + + get: (id) => { + const cacheHost = document.getElementById("elementsCache") + if (!cacheHost) return null + return cacheHost.querySelector('#' + id) + }, + + remove: (id) => { + const cacheItem = elementsCache.get(id) + if (cacheItem) { + try { + const cacheHost = document.getElementById("elementsCache") + if (!cacheHost) return false + removeCache = removeChild(cacheHost, cacheItem) + if (removeCache) { + removeCache.remove() + return true + } else { + return false + } + } catch (error) { + console.error(`Error removing element ${id}:`, error) + return false + } + } + return false + }, + + updateState: (id, newState) => { + const element = document.getElementById(id); + console.log("Updating state for element " + id) + console.log(newState) + if (element) { + if ('isVisible' in newState) { + element.style.display = newState.isVisible ? 'block' : 'none'; + } + if ('isFullScreen' in newState) { + if (newState.isFullScreen) { + element.style.position = 'fixed'; + element.style.top = '0'; + element.style.left = '0'; + element.style.width = '100%'; + element.style.height = '100%'; + element.style.zIndex = '9999'; + } else { + // Rétablir les styles normaux + element.style.position = 'absolute'; + element.style.top = ''; + element.style.left = ''; + element.style.width = ''; + element.style.height = ''; + element.style.zIndex = ''; + } + } + return true; + } + return false; + }, + + updatePosition: (id, position) => { + const cacheItem = elementsCache.get(id) + //console.log("Updating position for element " + id + ", exists: " + elementsCache.has(id)) + if (cacheItem) { + try { + //console.log("Updating positions to", position) + cacheItem.style.top = `${position.top}px`; + cacheItem.style.left = `${position.left}px`; + cacheItem.style.width = `${position.width}px`; + cacheItem.style.height = `${position.height}px`; + return true + } catch (error) { + console.error(`Error updating position for element ${id}:`, error) + return false + } + } else { + //console.log("Element " + id + " doesn't exist") + } + return false + } +} + +export { ElementsCache, elementsCache } \ No newline at end of file diff --git a/src/components/App/index.js b/src/components/App/index.js index 7f92d3c1..d8e49da3 100644 --- a/src/components/App/index.js +++ b/src/components/App/index.js @@ -31,10 +31,12 @@ import { TargetContextProvider } from "../../targets" import { ToastsContainer } from "../Toast" import { ModalContainer } from "../Modal" import { ContentContainer } from "../../areas" +import { ElementsCache } from "../../areas/elementsCache" const App = () => { return (
+ diff --git a/src/components/Controls/FullScreenButton.js b/src/components/Controls/FullScreenButton.js index 11b9bba4..dcc0c354 100644 --- a/src/components/Controls/FullScreenButton.js +++ b/src/components/Controls/FullScreenButton.js @@ -25,12 +25,17 @@ const isFullScreen = (element) => { return document.fullscreenElement === element } -const FullScreenButton = ({ panelRef, hideOnFullScreen, asButton }) => { +const FullScreenButton = ({ panelRef,panelId, hideOnFullScreen, asButton, onclick }) => { const [isFullScreenMode, setIsFullScreenMode] = useState(false) - + const getPanelRef = () => { + if(panelRef) return panelRef.current + if (panelId)return document.getElementById(panelId) + return null + } useEffect(() => { const handleFullscreenChange = () => { - setIsFullScreenMode(isFullScreen(panelRef.current)) + if(getPanelRef) + setIsFullScreenMode(isFullScreen(getPanelRef)) } document.addEventListener("fullscreenchange", handleFullscreenChange) @@ -41,11 +46,18 @@ const FullScreenButton = ({ panelRef, hideOnFullScreen, asButton }) => { handleFullscreenChange ) } - }, [panelRef]) + }, []) const toggleFullScreen = () => { - if (!isFullScreenMode) { - panelRef.current.requestFullscreen() + if (!isFullScreenMode) {// + if (onclick) { + onclick() + } + if (getPanelRef()){ + getPanelRef().requestFullscreen() + } + + console.log("Fullscreen activated pour " + panelId) } else { if (document.fullscreenElement) { document.exitFullscreen() diff --git a/src/components/ExtraContent/extraContentItem.js b/src/components/ExtraContent/extraContentItem.js new file mode 100644 index 00000000..2aac080a --- /dev/null +++ b/src/components/ExtraContent/extraContentItem.js @@ -0,0 +1,185 @@ +/* + extraContentItem.js - ESP3D WebUI navigation page 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, h } from "preact" +import { useState, useEffect, useCallback } from "preact/hooks" +import { espHttpURL } from "../Helpers" +import { useHttpFn } from "../../hooks" +import { + ButtonImg, + FullScreenButton, + CloseButton, + ContainerHelper, +} from "../Controls" +import { T } from "../Translations" +import { Play, Pause, Aperture, RefreshCcw } from "preact-feather" +import { elementsCache } from "../../areas/elementsCache" + +const ExtraContentItem = ({ id, source, type, label, target, refreshtime, isVisible = true, isFullScreen = false, refreshPaused = false }) => { + const [contentUrl, setContentUrl] = useState("") + const { createNewRequest } = useHttpFn + console.log( id) + //console.log("ExtraContentItem rendering for " + id, "is in cache: " + elementsCache.has(id)) + /*const loadContent = useCallback((forceReload = false) => { + if ( elementsCache.has(id) && !forceReload) { + console.log("Content already loaded for " + id) + return + } + if (source.startsWith("http") || forceReload) { + setContentUrl(source) + } else { + const idquery = type === "content" ? type + id : "download" + id + createNewRequest( + espHttpURL(source), + { method: "GET", id: idquery, max: 2 }, + { + onSuccess: handleContentSuccess, + onFail: handleContentError, + } + ) + } + }, [id, source, type, createNewRequest]) + + const handleContentSuccess = useCallback((result) => { + let blob + switch (type) { + case "camera": + case "image": + blob = new Blob([result], { type: "image/jpeg" }) + break + case "extension": + case "content": + blob = new Blob([result], { type: "text/html" }) + break + default: + blob = new Blob([result], { type: "text/plain" }) + } + const url = URL.createObjectURL(blob) + setContentUrl(url) + }, [type]) + + const handleContentError = useCallback((error) => { + console.error(`Error loading content for ${id}:`, error) + const errorContent = `

Error loading content

` + const errorBlob = new Blob([errorContent], { type: "text/html" }) + setContentUrl(URL.createObjectURL(errorBlob)) + }, [id]) +*/ + useEffect(() => { + console.log("ExtraContentItem rendering for " + id, "is in cache: " + elementsCache.has(id)) + }, []) +/* + const handleRefresh = () => loadContent(true) + const toggleFullScreen = () => elementsCache.updateState(id, { isFullScreen: !isFullScreen }) + const toggleRefreshPause = () => elementsCache.updateState(id, { refreshPaused: !refreshPaused }) + + const renderControls = () => ( +
+ {parseInt(refreshtime) === 0 && target === "page" && ( + } + onclick={handleRefresh} + /> + )} + {parseInt(refreshtime) > 0 && type !== "extension" && ( + <> + : } + onclick={toggleRefreshPause} + /> + {type !== "content" && ( + } + onclick={() => { + // Handle screenshot functionality here + }} + /> + )} + + )} + + {target === "panel" && ( + + elementsCache.updateState(id, { isVisible: false })} + /> + + + )} +
+ ) +*/ + const contentStyle = { + position: 'absolute', + top: 0, + left: 0, + width: '100%', + height: '100%', + zIndex: isFullScreen ? '9999' : 'auto', + display: isVisible ? 'block' : 'none', + } + + let content + if (type === "camera" || type === "image") { + content = {label} + } else { + content = ( + - ) - } + + const handleRefresh = () => { + useUiContextFn.haptic() + console.log("Refreshing element " + extra_content_id) + elementsCache.updateState(extra_content_id, { forceRefresh: true }) } - useEffect(() => { - //load using internal http manager - if (!pageSource.startsWith("http")) loadContent(true) - //init timer if any + const handleFullScreen = () => { + setIsFullScreen(!isFullScreen) + console.log("Toggling fullscreen for element " + extra_content_id) + elementsCache.updateState(extra_content_id, { isFullScreen: !isFullScreen }) + } - if (refreshtime != 0 && type != "extension") { - clearInterval(timerIDs[id]) - timerIDs[id] = setInterval(loadContent, refreshtime) - } + const renderControls = () => ( +
+ } + onclick={handleRefresh} + /> + + + elementsCache.updateState(extra_content_id, { isVisible: false })} + /> + +
+ ) - return () => { - //cleanup - if (refreshtime != 0 && type != "extension") { - clearInterval(timerIDs[id]) - } - } - }) - if (target == "page") + if (target === "page") { + console.log("Rendering page element " + extra_content_id) return ( -
- - - +
+ + {renderControls()}
) - if (target == "panel") { - const displayIcon = iconsList[icon] ? iconsList[icon] : "" - //console.log("Panel :", id, "Ref :", panelRef.current) + } + + if (target === "panel") { + // console.log("Rendering panel element " + extra_content_id) + const displayIcon = iconsList[icon] || "" return ( -
- - + + ) } } -export default ExtraContent +export { ExtraContent, ExtraContentItem } \ No newline at end of file diff --git a/src/components/Panels/ExtraPanel.js b/src/components/Panels/ExtraPanel.js index 6449679b..d07a7125 100644 --- a/src/components/Panels/ExtraPanel.js +++ b/src/components/Panels/ExtraPanel.js @@ -17,7 +17,7 @@ Files.js - ESP3D WebUI component file */ import { h } from "preact" -import ExtraContent from "../ExtraContent" +import {ExtraContent} from "../ExtraContent" /* * Local const diff --git a/src/pages/extrapages/index.js b/src/pages/extrapages/index.js index 06973735..f6b5c3ff 100644 --- a/src/pages/extrapages/index.js +++ b/src/pages/extrapages/index.js @@ -18,7 +18,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ import { h } from "preact" -import ExtraContent from "../../components/ExtraContent" +import {ExtraContent} from "../../components/ExtraContent" const ExtraPage = ({ id, source, refreshtime, label, type }) => { return ( diff --git a/src/style/components/_app.scss b/src/style/components/_app.scss index a94a0115..bc4de2b2 100644 --- a/src/style/components/_app.scss +++ b/src/style/components/_app.scss @@ -109,3 +109,19 @@ body { .modal-container { max-height: 95vh; } + +.page_extra { + height: 100%; + display: flex; + flex-direction: column; +} +.extra-content-container { + height: 100%; + width: 100%; + position:absolute; + background-color: red!important; + color: white!important; + border: 1px solid red!important; + z-index: 99999; + opacity: 1; +} \ No newline at end of file From 75b16e5d02b6b238b3f1f5cf41ad59962b3cfd18 Mon Sep 17 00:00:00 2001 From: Luc <8822552+luc-github@users.noreply.github.com> Date: Mon, 29 Jul 2024 12:31:45 +0800 Subject: [PATCH 02/28] Adjust buttons with target container - WIP - POC rearrange the class / style for consistency Comment verbose debug log that are no more necessaries Add fallback if error loading content --- src/areas/elementsCache.js | 50 +++-- .../ExtraContent/extraContentItem.js | 173 +++++++++--------- src/components/ExtraContent/index.js | 50 +++-- src/style/components/_app.scss | 18 +- src/style/components/_control.scss | 42 ++++- src/style/components/_menu.scss | 6 + 6 files changed, 201 insertions(+), 138 deletions(-) diff --git a/src/areas/elementsCache.js b/src/areas/elementsCache.js index 9e920745..925be09a 100644 --- a/src/areas/elementsCache.js +++ b/src/areas/elementsCache.js @@ -36,23 +36,38 @@ const elementsCache = { create: (id, props) => { if (!elementsCache.has(id)) { const cacheHost = document.getElementById("elementsCache") - console.log("Creating element, because it doesn't exist: " + id) - console.log("Current host size is " + cacheHost.children.length) + //console.log("Creating element, because it doesn't exist: " + id) + //console.log("Current host size is " + cacheHost.children.length) try { const container = document.getElementById("elementsCache") - const vnode = container.appendChild(document.createElement('div')) - vnode.id = "new_element" + id - const new_vnode = render(, container, vnode) - console.log("Element created: " + id + ", exists in cache: " + elementsCache.has(id)) - console.log("Now Current host size is " + cacheHost.children.length) - console.log(cacheHost.innerHTML) + + //NOTE: https://preactjs.com/guide/v10/api-reference/#render + //the third argument is deprecated and will be removed in a future version + // I do not have idea how to work around this so let see when V11 is out, + // hopefully we will have a working solution to add a new element to the cache + // without erasing the previous one + //const vnode = container.appendChild(document.createElement('div')) + //vnode.id = "new_element" + id + //const new_vnode = render(, container, vnode) + + //Note2: Another solution is to use a host element to hold the new element + //it add a new layer of complexity to the code but seems the way recommended + const vnode = container.querySelector('#host_' + id)?container.querySelector('#host_' + id):container.appendChild(document.createElement('div')) + if (vnode.id !== "host_" + id) { + vnode.id = "host_" + id + vnode.style.display = "contents"; + } + //It use current or a new host vnode + render(, vnode) + //console.log("Element created: " + id + ", exists in cache: " + elementsCache.has(id)) + //console.log("Now Current host size is " + cacheHost.children.length) return true } catch (error) { console.error(`Error creating element ${id}:`, error) return false } } else { - //console.log("Element already exists: " + id) + ////console.log("Element already exists: " + id) return true } }, @@ -66,10 +81,14 @@ const elementsCache = { remove: (id) => { const cacheItem = elementsCache.get(id) if (cacheItem) { + //due to the way render, each new element has div as parent itself connected to the cache + const itemHost = cacheItem.parentNode try { const cacheHost = document.getElementById("elementsCache") if (!cacheHost) return false - removeCache = removeChild(cacheHost, cacheItem) + //sanity check if we have new implementation + if (cacheHost == itemHost) return false + removeCache = removeChild(cacheHost, itemHost) if (removeCache) { removeCache.remove() return true @@ -86,11 +105,12 @@ const elementsCache = { updateState: (id, newState) => { const element = document.getElementById(id); - console.log("Updating state for element " + id) - console.log(newState) + //console.log("Updating state for element " + id) + //console.log(newState) if (element) { if ('isVisible' in newState) { element.style.display = newState.isVisible ? 'block' : 'none'; + if (newState.isVisible) {console.log("Element " + id + " is now visible")} else {console.log("Element " + id + " is now hidden")} } if ('isFullScreen' in newState) { if (newState.isFullScreen) { @@ -117,10 +137,10 @@ const elementsCache = { updatePosition: (id, position) => { const cacheItem = elementsCache.get(id) - //console.log("Updating position for element " + id + ", exists: " + elementsCache.has(id)) + ////console.log("Updating position for element " + id + ", exists: " + elementsCache.has(id)) if (cacheItem) { try { - //console.log("Updating positions to", position) + ////console.log("Updating positions to", position) cacheItem.style.top = `${position.top}px`; cacheItem.style.left = `${position.left}px`; cacheItem.style.width = `${position.width}px`; @@ -131,7 +151,7 @@ const elementsCache = { return false } } else { - //console.log("Element " + id + " doesn't exist") + ////console.log("Element " + id + " doesn't exist") } return false } diff --git a/src/components/ExtraContent/extraContentItem.js b/src/components/ExtraContent/extraContentItem.js index 2aac080a..39c2d22b 100644 --- a/src/components/ExtraContent/extraContentItem.js +++ b/src/components/ExtraContent/extraContentItem.js @@ -21,20 +21,27 @@ import { Fragment, h } from "preact" import { useState, useEffect, useCallback } from "preact/hooks" import { espHttpURL } from "../Helpers" import { useHttpFn } from "../../hooks" -import { - ButtonImg, - FullScreenButton, - CloseButton, - ContainerHelper, -} from "../Controls" +import { ButtonImg, ContainerHelper } from "../Controls" import { T } from "../Translations" import { Play, Pause, Aperture, RefreshCcw } from "preact-feather" import { elementsCache } from "../../areas/elementsCache" -const ExtraContentItem = ({ id, source, type, label, target, refreshtime, isVisible = true, isFullScreen = false, refreshPaused = false }) => { +const ExtraContentItem = ({ + id, + source, + type, + label, + target, + refreshtime, + isVisible = true, + isFullScreen = false, + refreshPaused = false, +}) => { const [contentUrl, setContentUrl] = useState("") const { createNewRequest } = useHttpFn - console.log( id) + console.log(id) + const element_id = id.replace("extra_content_", type) + let error_loading = false //console.log("ExtraContentItem rendering for " + id, "is in cache: " + elementsCache.has(id)) /*const loadContent = useCallback((forceReload = false) => { if ( elementsCache.has(id) && !forceReload) { @@ -80,106 +87,104 @@ const ExtraContentItem = ({ id, source, type, label, target, refreshtime, isVisi const errorBlob = new Blob([errorContent], { type: "text/html" }) setContentUrl(URL.createObjectURL(errorBlob)) }, [id]) + + useEffect(() => { + loadContent() + }, [loadContent()]) */ useEffect(() => { - console.log("ExtraContentItem rendering for " + id, "is in cache: " + elementsCache.has(id)) + if (error_loading) { + const fallback = document.getElementById("fallback_" + element_id) + const element = document.getElementById(element_id) + if (fallback) { + fallback.style.display = "block" + if (element) { + element.style.display = "none" + } + } + } }, []) -/* const handleRefresh = () => loadContent(true) - const toggleFullScreen = () => elementsCache.updateState(id, { isFullScreen: !isFullScreen }) - const toggleRefreshPause = () => elementsCache.updateState(id, { refreshPaused: !refreshPaused }) - - const renderControls = () => ( -
- {parseInt(refreshtime) === 0 && target === "page" && ( + const toggleFullScreen = () => + elementsCache.updateState(id, { isFullScreen: !isFullScreen }) + const toggleRefreshPause = () => + elementsCache.updateState(id, { refreshPaused: !refreshPaused }) + console.log("type", type) + const RenderControls = () => ( +
+ {type == "image" && ( } - onclick={handleRefresh} + tooltip + data-tooltip={T("S186")} + icon={} + onclick={() => { + // Handle screenshot functionality here + }} /> )} + {parseInt(refreshtime) > 0 && type !== "extension" && ( - <> - : } - onclick={toggleRefreshPause} - /> - {type !== "content" && ( - } - onclick={() => { - // Handle screenshot functionality here - }} - /> - )} - - )} - - {target === "panel" && ( - - elementsCache.updateState(id, { isVisible: false })} - /> - - + : } + onclick={toggleRefreshPause} + /> )}
) -*/ - const contentStyle = { - position: 'absolute', - top: 0, - left: 0, - width: '100%', - height: '100%', - zIndex: isFullScreen ? '9999' : 'auto', - display: isVisible ? 'block' : 'none', + + let content = [] + const onErrorLoading = () => { + console.log("Error loading content for " + id) + error_loading = true } - let content if (type === "camera" || type === "image") { - content = {label} + content.push( + {label + ) } else { - content = ( + content.push(