added preview mode

This commit is contained in:
PhilReact 2024-11-20 04:23:14 +02:00
parent a41e5049ba
commit 52f7a3a640
10 changed files with 376 additions and 19 deletions

View File

@ -14,7 +14,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
contextBridge.exposeInMainWorld('electron', { contextBridge.exposeInMainWorld('electron', {
onUpdateAvailable: (callback) => ipcRenderer.on('update_available', callback), onUpdateAvailable: (callback) => ipcRenderer.on('update_available', callback),
onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', 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'); ipcRenderer.send('test-ipc');

View File

@ -6,13 +6,15 @@ import {
} from '@capacitor-community/electron'; } from '@capacitor-community/electron';
import chokidar from 'chokidar'; import chokidar from 'chokidar';
import type { MenuItemConstructorOptions } from 'electron'; 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 electronIsDev from 'electron-is-dev';
import electronServe from 'electron-serve'; import electronServe from 'electron-serve';
import windowStateKeeper from 'electron-window-state'; import windowStateKeeper from 'electron-window-state';
const AdmZip = require('adm-zip');
import { join } from 'path'; import { join } from 'path';
import { myCapacitorApp } from '.'; import { myCapacitorApp } from '.';
const fs = require('fs');
const path = require('path')
const defaultDomains = [ const defaultDomains = [
'capacitor-electron://-', '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
}
});

9
package-lock.json generated
View File

@ -41,6 +41,7 @@
"@tiptap/starter-kit": "^2.5.9", "@tiptap/starter-kit": "^2.5.9",
"@transistorsoft/capacitor-background-fetch": "^6.0.1", "@transistorsoft/capacitor-background-fetch": "^6.0.1",
"@types/chrome": "^0.0.263", "@types/chrome": "^0.0.263",
"adm-zip": "^0.5.16",
"asmcrypto.js": "2.3.2", "asmcrypto.js": "2.3.2",
"axios": "^1.7.7", "axios": "^1.7.7",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
@ -4830,6 +4831,14 @@
"node": ">=0.4.0" "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": { "node_modules/agent-base": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",

View File

@ -45,6 +45,7 @@
"@tiptap/starter-kit": "^2.5.9", "@tiptap/starter-kit": "^2.5.9",
"@transistorsoft/capacitor-background-fetch": "^6.0.1", "@transistorsoft/capacitor-background-fetch": "^6.0.1",
"@types/chrome": "^0.0.263", "@types/chrome": "^0.0.263",
"adm-zip": "^0.5.16",
"asmcrypto.js": "2.3.2", "asmcrypto.js": "2.3.2",
"axios": "^1.7.7", "axios": "^1.7.7",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",

View File

@ -78,9 +78,7 @@ async function computePow(chatBytes, difficulty) {
workBufferPtr = sbrk(workBufferLength); workBufferPtr = sbrk(workBufferLength);
} }
console.log('Starting POW computation...');
const nonce = compute(hashPtr, workBufferPtr, workBufferLength, difficulty); const nonce = compute(hashPtr, workBufferPtr, workBufferLength, difficulty);
console.log('POW computation finished.');
return { nonce, chatBytesArray }; return { nonce, chatBytesArray };
} }

View File

@ -15,31 +15,44 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef)
const { rootHeight } = useContext(MyContext); const { rootHeight } = useContext(MyContext);
// const iframeRef = useRef(null); // const iframeRef = useRef(null);
const { document, window: frameWindow } = useFrame(); 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('') const [url, setUrl] = useState('')
useEffect(()=> { useEffect(()=> {
if(app?.isPreview) return
if(isDevMode){ if(isDevMode){
setUrl(app?.url) setUrl(app?.url)
return return
} }
setUrl(`${getBaseApiReact()}/render/${app?.service}/${app?.name}${app?.path != null ? `/${app?.path}` : ''}?theme=dark&identifier=${(app?.identifier != null && app?.identifier != 'null') ? app?.identifier : ''}`) 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(()=> { const defaultUrl = useMemo(()=> {
return url return url
}, [url, isDevMode]) }, [url, isDevMode])
const refreshAppFunc = (e) => { const refreshAppFunc = (e) => {
const {tabId} = e.detail const {tabId} = e.detail
if(tabId === app?.tabId){ if(tabId === app?.tabId){
if(isDevMode){ if(isDevMode){
setUrl(app?.url + `?time=${Date.now()}`)
resetHistory()
if(!app?.isPreview){
setUrl(app?.url + `?time=${Date.now()}`)
}
return return
} }
const constructUrl = `${getBaseApiReact()}/render/${app?.service}/${app?.name}${path != null ? path : ''}?theme=dark&identifier=${app?.identifier != null ? app?.identifier : ''}&time=${new Date().getMilliseconds()}` 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) setUrl(constructUrl)
} }
@ -123,7 +136,10 @@ export const AppViewer = React.forwardRef(({ app , hide, isDevMode}, iframeRef)
try { try {
await navigationPromise; await navigationPromise;
} catch (error) { } 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`) 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 // iframeRef.current.contentWindow.location.href = previousPath; // Fallback URL update
} }

View File

@ -3,6 +3,7 @@ import { AppsDevModeHome } from "./AppsDevModeHome";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { MyContext, getBaseApiReact } from "../../App"; import { MyContext, getBaseApiReact } from "../../App";
import { AppInfo } from "./AppInfo"; import { AppInfo } from "./AppInfo";
import { import {
executeEvent, executeEvent,
subscribeToEvent, subscribeToEvent,
@ -113,6 +114,38 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
unsubscribeFromEvent("appsDevModeAddTab", addTabFunc); unsubscribeFromEvent("appsDevModeAddTab", addTabFunc);
}; };
}, [tabs]); }, [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 setSelectedTabFunc = (e) => {
const data = e.detail?.data; const data = e.detail?.data;
if(!e.detail?.isDevMode) return if(!e.detail?.isDevMode) return
@ -281,7 +314,7 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsDevModeHome availableQapps={availableQapps} setMode={setMode} myApp={null} myWebsite={null} /> <AppsDevModeHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={null} myWebsite={null} />
</Box> </Box>
)} )}
@ -315,7 +348,7 @@ export const AppsDevMode = ({ mode, setMode, show , myName, goToHome, setDesktop
}}> }}>
<Spacer height="30px" /> <Spacer height="30px" />
<AppsDevModeHome availableQapps={availableQapps} setMode={setMode} myApp={null} myWebsite={null} /> <AppsDevModeHome myName={myName} availableQapps={availableQapps} setMode={setMode} myApp={null} myWebsite={null} />
</Box> </Box>
</> </>
)} )}

View File

@ -7,6 +7,8 @@ import {
AppsContainer, AppsContainer,
AppsParent, AppsParent,
} from "./Apps-styles"; } from "./Apps-styles";
import {Buffer} from 'buffer'
import { import {
Avatar, Avatar,
Box, Box,
@ -24,9 +26,8 @@ import { MyContext, getBaseApiReact, isMobile } from "../../App";
import LogoSelected from "../../assets/svgs/LogoSelected.svg"; import LogoSelected from "../../assets/svgs/LogoSelected.svg";
import { executeEvent } from "../../utils/events"; import { executeEvent } from "../../utils/events";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import { AppsDevModeSortablePinnedApps } from "./AppsDevModeSortablePinnedApps";
import { useModal } from "../../common/useModal"; import { useModal } from "../../common/useModal";
import { isUsingLocal } from "../../background"; import { createEndpoint, isUsingLocal } from "../../background";
import { Label } from "../Group/AddGroup"; import { Label } from "../Group/AddGroup";
export const AppsDevModeHome = ({ export const AppsDevModeHome = ({
@ -34,10 +35,13 @@ export const AppsDevModeHome = ({
myApp, myApp,
myWebsite, myWebsite,
availableQapps, availableQapps,
myName
}) => { }) => {
const [domain, setDomain] = useState("127.0.0.1"); const [domain, setDomain] = useState("127.0.0.1");
const [port, setPort] = useState(""); const [port, setPort] = useState("");
const [selectedPreviewFile, setSelectedPreviewFile] = useState(null);
const { isShow, onCancel, onOk, show, message } = useModal(); const { isShow, onCancel, onOk, show, message } = useModal();
const { const {
openSnackGlobal, openSnackGlobal,
@ -46,6 +50,27 @@ export const AppsDevModeHome = ({
setInfoSnackCustom, setInfoSnackCustom,
} = useContext(MyContext); } = 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 () => { const addDevModeApp = async () => {
try { try {
const usingLocal = await isUsingLocal(); const usingLocal = await isUsingLocal();
@ -82,6 +107,170 @@ export const AppsDevModeHome = ({
}); });
} catch (error) {} } 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 ( return (
<> <>
@ -118,7 +307,39 @@ export const AppsDevModeHome = ({
<AppCircle> <AppCircle>
<Add>+</Add> <Add>+</Add>
</AppCircle> </AppCircle>
<AppCircleLabel>App</AppCircleLabel> <AppCircleLabel>Server</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
<ButtonBase
onClick={() => {
addPreviewApp();
}}
>
<AppCircleContainer
sx={{
gap: !isMobile ? "10px" : "5px",
}}
>
<AppCircle>
<Add>+</Add>
</AppCircle>
<AppCircleLabel>Zip</AppCircleLabel>
</AppCircleContainer>
</ButtonBase>
<ButtonBase
onClick={() => {
addPreviewAppWithDirectory();
}}
>
<AppCircleContainer
sx={{
gap: !isMobile ? "10px" : "5px",
}}
>
<AppCircle>
<Add>+</Add>
</AppCircle>
<AppCircleLabel>Directory</AppCircleLabel>
</AppCircleContainer> </AppCircleContainer>
</ButtonBase> </ButtonBase>
</AppsContainer> </AppsContainer>

View File

@ -189,9 +189,14 @@ export const AppsDevModeNavBar = () => {
<ButtonBase <ButtonBase
onClick={(e) => { onClick={(e) => {
executeEvent("refreshApp", { if(selectedTab?.refreshFunc){
tabId: selectedTab?.tabId, selectedTab.refreshFunc(selectedTab?.tabId)
}); } else {
executeEvent("refreshApp", {
tabId: selectedTab?.tabId,
});
}
}} }}
> >
<RefreshIcon <RefreshIcon

View File

@ -395,6 +395,7 @@ isDOMContentLoaded: false
infoSnackCustom, infoSnackCustom,
setInfoSnackCustom } = useContext(MyContext); setInfoSnackCustom } = useContext(MyContext);
useEffect(()=> { useEffect(()=> {
if(tabId && !isNaN(history?.currentIndex)){ if(tabId && !isNaN(history?.currentIndex)){
@ -509,7 +510,7 @@ isDOMContentLoaded: false
event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){ event?.data?.action === 'QDN_RESOURCE_DISPLAYED'){
const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null const pathUrl = event?.data?.path != null ? (event?.data?.path.startsWith('/') ? '' : '/') + event?.data?.path : null
setPath(pathUrl) setPath(pathUrl)
if(appName.toLowerCase() === 'q-mail'){ if(appName?.toLowerCase() === 'q-mail'){
window.sendMessage("addEnteredQmailTimestamp").catch((error) => { window.sendMessage("addEnteredQmailTimestamp").catch((error) => {
// error // error
}); });