diff --git a/electron/src/preload.ts b/electron/src/preload.ts index f3e42a9..ff9f42d 100644 --- a/electron/src/preload.ts +++ b/electron/src/preload.ts @@ -14,7 +14,11 @@ contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electron', { onUpdateAvailable: (callback) => ipcRenderer.on('update_available', callback), onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', callback), - restartApp: () => ipcRenderer.send('restart_app') + restartApp: () => ipcRenderer.send('restart_app'), + selectFile: async () => ipcRenderer.invoke('dialog:openFile'), + readFile: async (filePath) => ipcRenderer.invoke('fs:readFile', filePath), + selectAndZipDirectory: async (filePath) => ipcRenderer.invoke('fs:selectAndZip', filePath), + }); ipcRenderer.send('test-ipc'); \ No newline at end of file diff --git a/electron/src/setup.ts b/electron/src/setup.ts index 38ca57a..c9a1bcf 100644 --- a/electron/src/setup.ts +++ b/electron/src/setup.ts @@ -6,13 +6,15 @@ import { } from '@capacitor-community/electron'; import chokidar from 'chokidar'; import type { MenuItemConstructorOptions } from 'electron'; -import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session, ipcMain } from 'electron'; +import { app, BrowserWindow, Menu, MenuItem, nativeImage, Tray, session, ipcMain, dialog } from 'electron'; import electronIsDev from 'electron-is-dev'; import electronServe from 'electron-serve'; import windowStateKeeper from 'electron-window-state'; +const AdmZip = require('adm-zip'); import { join } from 'path'; import { myCapacitorApp } from '.'; - +const fs = require('fs'); +const path = require('path') const defaultDomains = [ 'capacitor-electron://-', @@ -361,3 +363,70 @@ ipcMain.on('set-allowed-domains', (event, domains: string[]) => { }); +ipcMain.handle('dialog:openFile', async () => { + const result = await dialog.showOpenDialog({ + properties: ['openFile'], + filters: [ + { name: 'ZIP Files', extensions: ['zip'] } // Restrict to ZIP files + ], + }); + return result.filePaths[0]; +}); + +ipcMain.handle('fs:readFile', async (_, filePath) => { + try { + // Ensure the file exists + if (!fs.existsSync(filePath)) { + throw new Error('File does not exist.'); + } + + // Ensure the filePath is an absolute path (optional but recommended for safety) + const absolutePath = path.resolve(filePath); + + // Read the file as a Buffer + const fileBuffer = fs.readFileSync(absolutePath); + + return fileBuffer + + } catch (error) { + console.error('Error reading file:', error.message); + return null; // Return null on error + } +}); + +ipcMain.handle('fs:selectAndZip', async (_, path) => { + let directoryPath = path + if(!directoryPath){ + const { canceled, filePaths } = await dialog.showOpenDialog({ + properties: ['openDirectory'], + }); + if (canceled || filePaths.length === 0) { + console.log('No directory selected'); + return null; +} + + directoryPath = filePaths[0]; + } + + + + + + +try { + + // Add the entire directory to the zip + const zip = new AdmZip(); + + // Add the entire directory to the zip + zip.addLocalFolder(directoryPath); + + // Generate the zip file as a buffer + const zipBuffer = zip.toBuffer(); + + return {buffer: zipBuffer, directoryPath} +} catch (error) { + return null +} +}); + diff --git a/package-lock.json b/package-lock.json index bd6bd42..0fb4089 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@tiptap/starter-kit": "^2.5.9", "@transistorsoft/capacitor-background-fetch": "^6.0.1", "@types/chrome": "^0.0.263", + "adm-zip": "^0.5.16", "asmcrypto.js": "2.3.2", "axios": "^1.7.7", "bcryptjs": "2.4.3", @@ -4830,6 +4831,14 @@ "node": ">=0.4.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", diff --git a/package.json b/package.json index 0cf5df4..e36681e 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@tiptap/starter-kit": "^2.5.9", "@transistorsoft/capacitor-background-fetch": "^6.0.1", "@types/chrome": "^0.0.263", + "adm-zip": "^0.5.16", "asmcrypto.js": "2.3.2", "axios": "^1.7.7", "bcryptjs": "2.4.3", diff --git a/src/chatComputePow.worker.js b/src/chatComputePow.worker.js index 00f5f72..f42908e 100644 --- a/src/chatComputePow.worker.js +++ b/src/chatComputePow.worker.js @@ -78,9 +78,7 @@ async function computePow(chatBytes, difficulty) { workBufferPtr = sbrk(workBufferLength); } - console.log('Starting POW computation...'); const nonce = compute(hashPtr, workBufferPtr, workBufferLength, difficulty); - console.log('POW computation finished.'); return { nonce, chatBytesArray }; } diff --git a/src/components/Apps/AppViewer.tsx b/src/components/Apps/AppViewer.tsx index e27810e..6b55ed0 100644 --- a/src/components/Apps/AppViewer.tsx +++ b/src/components/Apps/AppViewer.tsx @@ -15,31 +15,44 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) const { rootHeight } = useContext(MyContext); // const iframeRef = useRef(null); const { document, window: frameWindow } = useFrame(); - const {path, history, changeCurrentIndex} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode, app?.name, app?.service) + const {path, history, changeCurrentIndex, resetHistory} = useQortalMessageListener(frameWindow, iframeRef, app?.tabId, isDevMode, app?.name, app?.service) const [url, setUrl] = useState('') + useEffect(()=> { + if(app?.isPreview) return if(isDevMode){ setUrl(app?.url) return } setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`) - }, [app?.service, app?.name, app?.identifier, app?.path]) + }, [app?.service, app?.name, app?.identifier, app?.path, app?.isPreview]) + + useEffect(()=> { + if(app?.isPreview && app?.url){ + resetHistory() + setUrl(app.url) + } + }, [app?.url, app?.isPreview]) const defaultUrl = useMemo(()=> { return url }, [url, isDevMode]) - const refreshAppFunc = (e) => { const {tabId} = e.detail if(tabId === app?.tabId){ if(isDevMode){ - setUrl(app?.url + `?time=${Date.now()}`) + + resetHistory() + if(!app?.isPreview){ + setUrl(app?.url + `?time=${Date.now()}`) + } return } + const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}` setUrl(constructUrl) } @@ -123,7 +136,10 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef) try { await navigationPromise; } catch (error) { - + if(isDevMode){ + setUrl(`${url}${previousPath != null ? previousPath : ''}?theme=dark&time=${new Date().getMilliseconds()}&isManualNavigation=false`) + return + } setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${previousPath != null ? previousPath : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}&time=${new Date().getMilliseconds()}&isManualNavigation=false`) // iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update } diff --git a/src/components/Apps/AppsDevMode.tsx b/src/components/Apps/AppsDevMode.tsx index 08782f0..297db33 100644 --- a/src/components/Apps/AppsDevMode.tsx +++ b/src/components/Apps/AppsDevMode.tsx @@ -3,6 +3,7 @@ import { AppsDevModeHome } from "./AppsDevModeHome"; import { Spacer } from "../../common/Spacer"; import { MyContext, getBaseApiReact } from "../../App"; import { AppInfo } from "./AppInfo"; + import { executeEvent, subscribeToEvent, @@ -113,6 +114,38 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop unsubscribeFromEvent("appsDevModeAddTab", addTabFunc); }; }, [tabs]); + + const updateTabFunc = (e) => { + const data = e.detail?.data; + if(!data.tabId) return + const findIndexTab = tabs.findIndex((tab)=> tab?.tabId === data?.tabId) + if(findIndexTab === -1) return + const copyTabs = [...tabs] + const newTab ={ + ...copyTabs[findIndexTab], + url: data.url + + } + copyTabs[findIndexTab] = newTab + + setTabs(copyTabs); + setSelectedTab(newTab); + setMode("viewer"); + + setIsNewTabWindow(false); + }; + + + + useEffect(() => { + subscribeToEvent("appsDevModeUpdateTab", updateTabFunc); + + return () => { + unsubscribeFromEvent("appsDevModeUpdateTab", updateTabFunc); + }; + }, [tabs]); + + const setSelectedTabFunc = (e) => { const data = e.detail?.data; if(!e.detail?.isDevMode) return @@ -281,7 +314,7 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop }}> - + )} @@ -315,7 +348,7 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop }}> - + )} diff --git a/src/components/Apps/AppsDevModeHome.tsx b/src/components/Apps/AppsDevModeHome.tsx index b1c53c1..db1bab2 100644 --- a/src/components/Apps/AppsDevModeHome.tsx +++ b/src/components/Apps/AppsDevModeHome.tsx @@ -7,6 +7,8 @@ import { AppsContainer, AppsParent, } from "./Apps-styles"; +import {Buffer} from 'buffer' + import { Avatar, Box, @@ -24,9 +26,8 @@ import { MyContext, getBaseApiReact, isMobile } from "../../App"; import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import { executeEvent } from "../../utils/events"; import { Spacer } from "../../common/Spacer"; -import { AppsDevModeSortablePinnedApps } from "./AppsDevModeSortablePinnedApps"; import { useModal } from "../../common/useModal"; -import { isUsingLocal } from "../../background"; +import { createEndpoint, isUsingLocal } from "../../background"; import { Label } from "../Group/AddGroup"; export const AppsDevModeHome = ({ @@ -34,10 +35,13 @@ export const AppsDevModeHome = ({ myApp, myWebsite, availableQapps, + myName }) => { const [domain, setDomain] = useState("127.0.0.1"); const [port, setPort] = useState(""); + const [selectedPreviewFile, setSelectedPreviewFile] = useState(null); + const { isShow, onCancel, onOk, show, message } = useModal(); const { openSnackGlobal, @@ -46,6 +50,27 @@ export const AppsDevModeHome = ({ setInfoSnackCustom, } = useContext(MyContext); + const handleSelectFile = async (existingFilePath) => { + const filePath = existingFilePath || await window.electron.selectFile(); + if (filePath) { + + const content = await window.electron.readFile(filePath); + return {buffer: content, filePath} + } else { + console.log('No file selected.'); + } + }; + const handleSelectDirectry = async (existingDirectoryPath) => { + const {buffer, directoryPath} = await window.electron.selectAndZipDirectory(existingDirectoryPath); + if (buffer) { + + + return {buffer, directoryPath} + } else { + console.log('No file selected.'); + } + }; + const addDevModeApp = async () => { try { const usingLocal = await isUsingLocal(); @@ -82,6 +107,170 @@ export const AppsDevModeHome = ({ }); } catch (error) {} }; + + const addPreviewApp = async (isRefresh, existingFilePath, tabId) => { + try { + const usingLocal = await isUsingLocal(); + if (!usingLocal) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "Please use your local node for dev mode! Logout and use Local node.", + }); + return; + } + if (!myName) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "You need a name to use preview", + }); + return; + } + + + const {buffer, filePath} = await handleSelectFile(existingFilePath) + + if (!buffer) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "Please select a file", + }); + return; + } + const postBody = Buffer.from(buffer).toString('base64') + + const endpoint = await createEndpoint(`/arbitrary/APP/${myName}/zip?preview=true`) + const response = await fetch( + endpoint + , + { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: postBody, + } + ); + if(!response?.ok) throw new Error('Invalid zip') + const previewPath = await response.text(); + if(tabId){ + executeEvent("appsDevModeUpdateTab", { + data: { + url: "http://127.0.0.1:12391" + previewPath, + isPreview: true, + filePath, + refreshFunc: (tabId)=> { + addPreviewApp(true, filePath, tabId) + }, + tabId + }, + }); + return + } + executeEvent("appsDevModeAddTab", { + data: { + url: "http://127.0.0.1:12391" + previewPath, + isPreview: true, + filePath, + refreshFunc: (tabId)=> { + addPreviewApp(true, filePath, tabId) + } + }, + }); + } catch (error) { + console.error(error) + } + }; + + const addPreviewAppWithDirectory = async (isRefresh, existingDir, tabId) => { + try { + const usingLocal = await isUsingLocal(); + if (!usingLocal) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "Please use your local node for dev mode! Logout and use Local node.", + }); + return; + } + if (!myName) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "You need a name to use preview", + }); + return; + } + + + const {buffer, directoryPath} = await handleSelectDirectry(existingDir) + + if (!buffer) { + setOpenSnackGlobal(true); + + setInfoSnackCustom({ + type: "error", + message: + "Please select a file", + }); + return; + } + const postBody = Buffer.from(buffer).toString('base64') + + const endpoint = await createEndpoint(`/arbitrary/APP/${myName}/zip?preview=true`) + const response = await fetch( + endpoint + , + { + method: "POST", + headers: { + "Content-Type": "text/plain", + }, + body: postBody, + } + ); + if(!response?.ok) throw new Error('Invalid zip') + const previewPath = await response.text(); + if(tabId){ + executeEvent("appsDevModeUpdateTab", { + data: { + url: "http://127.0.0.1:12391" + previewPath, + isPreview: true, + directoryPath, + refreshFunc: (tabId)=> { + addPreviewAppWithDirectory(true, directoryPath, tabId) + }, + tabId + }, + }); + return + } + executeEvent("appsDevModeAddTab", { + data: { + url: "http://127.0.0.1:12391" + previewPath, + isPreview: true, + directoryPath, + refreshFunc: (tabId)=> { + addPreviewAppWithDirectory(true, directoryPath, tabId) + } + }, + }); + } catch (error) { + console.error(error) + } + }; return ( <> @@ -118,7 +307,39 @@ export const AppsDevModeHome = ({ + - App + Server + + + { + addPreviewApp(); + }} + > + + + + + + Zip + + + { + addPreviewAppWithDirectory(); + }} + > + + + + + + Directory diff --git a/src/components/Apps/AppsDevModeNavBar.tsx b/src/components/Apps/AppsDevModeNavBar.tsx index 6908504..983aef3 100644 --- a/src/components/Apps/AppsDevModeNavBar.tsx +++ b/src/components/Apps/AppsDevModeNavBar.tsx @@ -189,9 +189,14 @@ export const AppsDevModeNavBar = () => { { - executeEvent("refreshApp", { - tabId: selectedTab?.tabId, - }); + if(selectedTab?.refreshFunc){ + selectedTab.refreshFunc(selectedTab?.tabId) + } else { + executeEvent("refreshApp", { + tabId: selectedTab?.tabId, + }); + } + }} > { if(tabId && !isNaN(history?.currentIndex)){ @@ -509,7 +510,7 @@ isDOMContentLoaded: false event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null setPath(pathUrl) - if(appName.toLowerCase() === 'q-mail'){ + if(appName?.toLowerCase() === 'q-mail'){ window.sendMessage("addEnteredQmailTimestamp").catch((error) => { // error });