From b88476631bf715ffab77bc2393635f7008c55985 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:43:44 +0800 Subject: [PATCH 01/12] Refactor remote workspace --- {remote-instance => remote-workspace}/.dockerignore | 0 {remote-instance => remote-workspace}/.env.example | 0 {remote-instance => remote-workspace}/.gitignore | 0 {remote-instance => remote-workspace}/README.md | 0 {remote-instance => remote-workspace}/dockerfile | 0 {remote-instance => remote-workspace}/package-lock.json | 0 {remote-instance => remote-workspace}/package.json | 0 {remote-instance => remote-workspace}/src/index.ts | 0 .../src/servers/api-server/index.ts | 0 .../src/servers/api-server/platform-api/handler.ts | 0 .../src/servers/node-pty/index.ts | 0 {remote-instance => remote-workspace}/tsconfig.json | 0 .../utils/generate-self-signed.ps1 | 0 .../utils/generate-self-signed.sh | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename {remote-instance => remote-workspace}/.dockerignore (100%) rename {remote-instance => remote-workspace}/.env.example (100%) rename {remote-instance => remote-workspace}/.gitignore (100%) rename {remote-instance => remote-workspace}/README.md (100%) rename {remote-instance => remote-workspace}/dockerfile (100%) rename {remote-instance => remote-workspace}/package-lock.json (100%) rename {remote-instance => remote-workspace}/package.json (100%) rename {remote-instance => remote-workspace}/src/index.ts (100%) rename {remote-instance => remote-workspace}/src/servers/api-server/index.ts (100%) rename {remote-instance => remote-workspace}/src/servers/api-server/platform-api/handler.ts (100%) rename {remote-instance => remote-workspace}/src/servers/node-pty/index.ts (100%) rename {remote-instance => remote-workspace}/tsconfig.json (100%) rename {remote-instance => remote-workspace}/utils/generate-self-signed.ps1 (100%) rename {remote-instance => remote-workspace}/utils/generate-self-signed.sh (100%) diff --git a/remote-instance/.dockerignore b/remote-workspace/.dockerignore similarity index 100% rename from remote-instance/.dockerignore rename to remote-workspace/.dockerignore diff --git a/remote-instance/.env.example b/remote-workspace/.env.example similarity index 100% rename from remote-instance/.env.example rename to remote-workspace/.env.example diff --git a/remote-instance/.gitignore b/remote-workspace/.gitignore similarity index 100% rename from remote-instance/.gitignore rename to remote-workspace/.gitignore diff --git a/remote-instance/README.md b/remote-workspace/README.md similarity index 100% rename from remote-instance/README.md rename to remote-workspace/README.md diff --git a/remote-instance/dockerfile b/remote-workspace/dockerfile similarity index 100% rename from remote-instance/dockerfile rename to remote-workspace/dockerfile diff --git a/remote-instance/package-lock.json b/remote-workspace/package-lock.json similarity index 100% rename from remote-instance/package-lock.json rename to remote-workspace/package-lock.json diff --git a/remote-instance/package.json b/remote-workspace/package.json similarity index 100% rename from remote-instance/package.json rename to remote-workspace/package.json diff --git a/remote-instance/src/index.ts b/remote-workspace/src/index.ts similarity index 100% rename from remote-instance/src/index.ts rename to remote-workspace/src/index.ts diff --git a/remote-instance/src/servers/api-server/index.ts b/remote-workspace/src/servers/api-server/index.ts similarity index 100% rename from remote-instance/src/servers/api-server/index.ts rename to remote-workspace/src/servers/api-server/index.ts diff --git a/remote-instance/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts similarity index 100% rename from remote-instance/src/servers/api-server/platform-api/handler.ts rename to remote-workspace/src/servers/api-server/platform-api/handler.ts diff --git a/remote-instance/src/servers/node-pty/index.ts b/remote-workspace/src/servers/node-pty/index.ts similarity index 100% rename from remote-instance/src/servers/node-pty/index.ts rename to remote-workspace/src/servers/node-pty/index.ts diff --git a/remote-instance/tsconfig.json b/remote-workspace/tsconfig.json similarity index 100% rename from remote-instance/tsconfig.json rename to remote-workspace/tsconfig.json diff --git a/remote-instance/utils/generate-self-signed.ps1 b/remote-workspace/utils/generate-self-signed.ps1 similarity index 100% rename from remote-instance/utils/generate-self-signed.ps1 rename to remote-workspace/utils/generate-self-signed.ps1 diff --git a/remote-instance/utils/generate-self-signed.sh b/remote-workspace/utils/generate-self-signed.sh similarity index 100% rename from remote-instance/utils/generate-self-signed.sh rename to remote-workspace/utils/generate-self-signed.sh From 508f9f2b10f5a7dfcbea6a31f689c4777814e79e Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:11:24 +0800 Subject: [PATCH 02/12] Organize remote workspace & platform API code --- desktop/lib/node-pty-server.js | 2 +- desktop/main.mjs | 4 +- desktop/package-lock.json | 8 +- desktop/package.json | 2 +- desktop/preload.mjs | 9 +- remote-workspace/.env.example | 7 +- remote-workspace/package-lock.json | 18 +- remote-workspace/package.json | 7 +- remote-workspace/src/index.ts | 76 +++++++- remote-workspace/src/lib/get-network.ts | 14 ++ .../src/servers/api-server/index.ts | 73 +++----- .../api-server/platform-api/handler.ts | 168 ++++++++++++------ .../src/servers/node-pty/index.ts | 25 ++- remote-workspace/tsconfig.json | 2 +- web/lib/platform-api/cloud/cloud-api.ts | 12 +- web/lib/platform-api/electron/electron-api.ts | 2 +- 16 files changed, 275 insertions(+), 154 deletions(-) create mode 100644 remote-workspace/src/lib/get-network.ts diff --git a/desktop/lib/node-pty-server.js b/desktop/lib/node-pty-server.js index 0296b9ae..2c2ff911 100644 --- a/desktop/lib/node-pty-server.js +++ b/desktop/lib/node-pty-server.js @@ -37,7 +37,7 @@ const handleTerminalConnection = (ws) => { } }); - ptyProcess.on("data", (rawOutput) => { + ptyProcess.onData((rawOutput) => { ws.send(JSON.stringify({ type: "output", payload: rawOutput })); }); diff --git a/desktop/main.mjs b/desktop/main.mjs index 8cf17e1f..99755035 100644 --- a/desktop/main.mjs +++ b/desktop/main.mjs @@ -370,8 +370,8 @@ app.whenReady().then(() => { ipcMain.handle("copy-files", handleCopyFiles); - ipcMain.handle("load-settings", handleLoadSettings); - ipcMain.handle("save-settings", handleSaveSettings); + ipcMain.handle("get-persistent-settings", handleLoadSettings); + ipcMain.handle("set-persistent-settings", handleSaveSettings); ipcMain.handle("get-installation-path", handleGetInstallationPath); diff --git a/desktop/package-lock.json b/desktop/package-lock.json index c992641e..aca031c9 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -12,7 +12,7 @@ "fs-extra": "^7.0.1", "ignore": "^5.3.2", "node-addon-api": "^7.1.1", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.2" }, "devDependencies": { @@ -4256,9 +4256,9 @@ } }, "node_modules/node-pty": { - "version": "1.1.0-beta9", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta9.tgz", - "integrity": "sha512-/Ue38pvXJdgRZ3+me1FgfglLd301GhJN0NStiotdt61tm43N5htUyR/IXOUzOKuNaFmCwIhy6nwb77Ky41LMbw==", + "version": "1.1.0-beta37", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta37.tgz", + "integrity": "sha512-Ys8AW98Atyu9cLV5QLQshvSTF+YMDksVi2ULkYNPAKLGzaDQbXuOEjStg4ZZuL4c8saOTh1+2PNlCoyuagBr1Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index a123f5c3..205170ad 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -15,7 +15,7 @@ "fs-extra": "^7.0.1", "ignore": "^5.3.2", "node-addon-api": "^7.1.1", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.2" }, "devDependencies": { diff --git a/desktop/preload.mjs b/desktop/preload.mjs index 2f49128d..ab56b490 100644 --- a/desktop/preload.mjs +++ b/desktop/preload.mjs @@ -1,6 +1,7 @@ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("electronAPI", { + // #region Platform API selectDir: () => ipcRenderer.invoke("select-dir"), selectFile: (fileExtension) => ipcRenderer.invoke("select-file", fileExtension), @@ -25,13 +26,17 @@ contextBridge.exposeInMainWorld("electronAPI", { copyFiles: (from, to) => ipcRenderer.invoke("copy-files", from, to), - loadSettings: () => ipcRenderer.invoke("load-settings"), - saveSettings: (settings) => ipcRenderer.invoke("save-settings", settings), + getPersistentSettings: () => ipcRenderer.invoke("get-persistent-settings"), + setPersistentSettings: (settings) => ipcRenderer.invoke("set-persistent-settings", settings), getInstallationPath: () => ipcRenderer.invoke("get-installation-path"), createTerminal: () => ipcRenderer.invoke("create-terminal"), + // #endregion + + // #region Auth API login: () => ipcRenderer.invoke("login"), logout: () => ipcRenderer.invoke("logout"), + // #endregion }); diff --git a/remote-workspace/.env.example b/remote-workspace/.env.example index a29b64f2..5c00bfd2 100644 --- a/remote-workspace/.env.example +++ b/remote-workspace/.env.example @@ -1,4 +1,9 @@ SSL_CERT_PATH=path_to_cert SSL_KEY_PATH=path_to_key # You can modify this to custom frontend deployment URL -FRONTEND_URL=https://editor.pulse-editor.com +FRONTEND_URL=https://web.pulse-editor.com +# Name of the workspace instance +WORKSPACE_ID=test-workspace +# Port for the server to listen on. +# Default to 6080 +SERVER_PORT=6080 diff --git a/remote-workspace/package-lock.json b/remote-workspace/package-lock.json index 002ef76a..17b57ef4 100644 --- a/remote-workspace/package-lock.json +++ b/remote-workspace/package-lock.json @@ -1,19 +1,18 @@ { - "name": "pulse-editor-remote-instance", + "name": "pulse-editor-remote-workspace", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "pulse-editor-remote-instance", + "name": "pulse-editor-remote-workspace", "version": "1.0.0", "license": "MIT", "dependencies": { - "@pulse-editor/shared-utils": "^0.1.1-alpha.24", "dotenv": "^17.2.0", "express": "^5.1.0", "ignore": "^7.0.5", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.3" }, "devDependencies": { @@ -466,11 +465,6 @@ "node": ">=18" } }, - "node_modules/@pulse-editor/shared-utils": { - "version": "0.1.1-alpha.24", - "resolved": "https://registry.npmjs.org/@pulse-editor/shared-utils/-/shared-utils-0.1.1-alpha.24.tgz", - "integrity": "sha512-aHSrc1Ntpvvs8npeSa543v0imjd/20UdXiJtLS5g2CsJAWAayzMjEU/nL6aWaE2rn6xThq8xgfy2Iu1Wr/SclQ==" - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1179,9 +1173,9 @@ "license": "MIT" }, "node_modules/node-pty": { - "version": "1.1.0-beta34", - "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta34.tgz", - "integrity": "sha512-RraDtX9RS1G1I5iO7e4YIOIA4arzd4ZVCD4mZr7+szaNupoTg9fxDCRr0EanqS0Qlzgm3PIdHNbPmblJguJuyg==", + "version": "1.1.0-beta37", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.1.0-beta37.tgz", + "integrity": "sha512-Ys8AW98Atyu9cLV5QLQshvSTF+YMDksVi2ULkYNPAKLGzaDQbXuOEjStg4ZZuL4c8saOTh1+2PNlCoyuagBr1Q==", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/remote-workspace/package.json b/remote-workspace/package.json index 2ddffcd7..862881d0 100644 --- a/remote-workspace/package.json +++ b/remote-workspace/package.json @@ -1,7 +1,7 @@ { - "name": "pulse-editor-remote-instance", + "name": "pulse-editor-remote-workspace", "version": "1.0.0", - "description": "Remote instance backend for Pulse Editor", + "description": "Remote workspace backend for Pulse Editor", "license": "MIT", "author": "ClayPulse", "main": "src/index.ts", @@ -11,11 +11,10 @@ "start": "node dist/index.js" }, "dependencies": { - "@pulse-editor/shared-utils": "^0.1.1-alpha.24", "dotenv": "^17.2.0", "express": "^5.1.0", "ignore": "^7.0.5", - "node-pty": "^1.1.0-beta34", + "node-pty": "^1.1.0-beta37", "ws": "^8.18.3" }, "devDependencies": { diff --git a/remote-workspace/src/index.ts b/remote-workspace/src/index.ts index e4cce2ea..cdb54667 100644 --- a/remote-workspace/src/index.ts +++ b/remote-workspace/src/index.ts @@ -1,8 +1,68 @@ -import { createTerminalServer } from "./servers/node-pty"; -import { createAPIServer } from "./servers/api-server"; - -/* Create servers */ -createAPIServer().then((server) => { - // After API server is created, the terminal server can use it - createTerminalServer(server); -}); +import dotenv from "dotenv"; +import express from "express"; +import fs from "fs"; +import http from "http"; +import https from "https"; +import { getLocalNetworkIP } from "./lib/get-network"; +import { addAPIServer } from "./servers/api-server"; +import { addTerminalServer } from "./servers/node-pty"; + +dotenv.config(); + +const expressApp = express(); +const serverPort = process.env.SERVER_PORT + ? parseInt(process.env.SERVER_PORT) + : 6080; +const certPath = process.env.SSL_CERT_PATH; +const keyPath = process.env.SSL_KEY_PATH; +const workspaceId = process.env.WORKSPACE_ID; +const frontendUrl = process.env.FRONTEND_URL ?? "https://web.pulse-editor.com"; + +async function startServers() { + if (!workspaceId) { + console.error("WORKSPACE_ID is not set in environment variables."); + process.exit(1); + } + + /* Create servers */ + const server = await createServer(); + const address = getLocalNetworkIP(); + + const isHttps = + certPath && keyPath && fs.existsSync(certPath) && fs.existsSync(keyPath) + ? true + : false; + + await addAPIServer(server, expressApp, workspaceId, serverPort, frontendUrl); + console.log( + `API server is running at ${isHttps ? "https" : "http"}://${address}:${serverPort}/${workspaceId}`, + ); + + await addTerminalServer(server, workspaceId); + console.log( + `Terminal server is running at ${isHttps ? "wss" : "ws"}://${address}:${serverPort}/${workspaceId}/terminal/ws`, + ); +} + +async function createServer() { + if ( + certPath && + keyPath && + fs.existsSync(certPath) && + fs.existsSync(keyPath) + ) { + const server = https.createServer( + { + key: fs.readFileSync(keyPath), + cert: fs.readFileSync(certPath), + }, + expressApp, + ); + return server; + } else { + const server = http.createServer(expressApp); + return server; + } +} + +startServers(); diff --git a/remote-workspace/src/lib/get-network.ts b/remote-workspace/src/lib/get-network.ts new file mode 100644 index 00000000..a8a46f05 --- /dev/null +++ b/remote-workspace/src/lib/get-network.ts @@ -0,0 +1,14 @@ +import { networkInterfaces } from "os"; + +export function getLocalNetworkIP() { + const interfaces = networkInterfaces(); + for (const iface of Object.values(interfaces)) { + if (!iface) continue; + for (const config of iface) { + if (config.family === "IPv4" && !config.internal) { + return config.address; // Returns the first non-internal IPv4 address + } + } + } + return "localhost"; // Fallback +} \ No newline at end of file diff --git a/remote-workspace/src/servers/api-server/index.ts b/remote-workspace/src/servers/api-server/index.ts index 6b03be52..d1b4575a 100644 --- a/remote-workspace/src/servers/api-server/index.ts +++ b/remote-workspace/src/servers/api-server/index.ts @@ -1,54 +1,35 @@ +import dotenv from "dotenv"; import express from "express"; -import https from "https"; import http from "http"; -import fs from "fs"; -import dotenv from "dotenv"; +import https from "https"; import { handlePlatformAPIRequest } from "./platform-api/handler"; dotenv.config(); -const app = express(); const HOST = "0.0.0.0"; -const HTTP_SERVER_PORT = 6080; -const HTTPS_SERVER_PORT = 6443; -const certPath = process.env.SSL_CERT_PATH; -const keyPath = process.env.SSL_KEY_PATH; -export async function createAPIServer() { - await createEndpoints(app); +export async function addAPIServer( + server: http.Server | https.Server, + expressApp: express.Express, + instanceId: string, + port: number, + frontendUrl: string, +) { + await createEndpoints(expressApp, instanceId, frontendUrl); - if ( - certPath && - keyPath && - fs.existsSync(certPath) && - fs.existsSync(keyPath) - ) { - const server = https.createServer( - { - key: fs.readFileSync(keyPath), - cert: fs.readFileSync(certPath), - }, - app - ); - server.listen(HTTPS_SERVER_PORT, HOST, () => { - console.log(`HTTPS server is running on port ${HTTPS_SERVER_PORT}`); - }); - return server; - } else { - const server = http.createServer(app); - server.listen(HTTP_SERVER_PORT, HOST, () => { - console.log(`HTTP server is running on port ${HTTP_SERVER_PORT}`); - }); - return server; - } + server.listen(port, HOST); } -async function createEndpoints(app: express.Express) { +async function createEndpoints( + app: express.Express, + instanceId: string, + frontendUrl: string, +) { app.use(express.json()); app.get("/:instanceId/", (req, res) => { - const instanceId = req.params.instanceId; - if (instanceId !== process.env.INSTANCE_ID) { + const id = req.params.instanceId; + if (id !== instanceId) { return res.status(400).send("Invalid instance ID"); } // Get the requested URL @@ -56,24 +37,22 @@ async function createEndpoints(app: express.Express) { // Redirect to https://editor.pulse-editor.com and append // this instance's URL as a query parameter - const url = new URL( - process.env.FRONTEND_URL ?? "https://editor.pulse-editor.com" - ); + const url = new URL(frontendUrl); url.searchParams.append("instance", serverUrl); res.redirect(url.toString()); }); app.get("/:instanceId/test", (req, res) => { - const instanceId = req.params.instanceId; - if (instanceId !== process.env.INSTANCE_ID) { + const id = req.params.instanceId; + if (id !== instanceId) { return res.status(400).send("Invalid instance ID"); } res.send("Remote instance is running!"); }); app.post("/:instanceId/platform-api", async (req, res) => { - const instanceId = req.params.instanceId; - if (instanceId !== process.env.INSTANCE_ID) { + const id = req.params.instanceId; + if (id !== instanceId) { return res.status(400).send("Invalid instance ID"); } @@ -84,11 +63,7 @@ async function createEndpoints(app: express.Express) { const host = req.host; - const result = await handlePlatformAPIRequest( - body, - host, - instanceId - ); + const result = await handlePlatformAPIRequest(body, host, id); // Process the request and send a response if (result && result.error) { diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts index 4aae2ea1..8ce188be 100644 --- a/remote-workspace/src/servers/api-server/platform-api/handler.ts +++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts @@ -5,10 +5,12 @@ import path from "path"; // Define a safe root directory for projects. Can be overridden by env or configured as needed. const PROJECTS_ROOT = process.env.PROJECTS_ROOT ?? "/srv/projects"; +const settingsPath = path.join(PROJECTS_ROOT, "settings.json"); + // Utility to resolve and validate user-supplied uri inside PROJECTS_ROOT function getSafePath(uri: string): string { // Prevent empty/undefined input - if (!uri || typeof uri !== 'string') { + if (!uri || typeof uri !== "string") { throw new Error("Invalid project path"); } // Resolve against the root directory @@ -45,7 +47,7 @@ async function handleListProjects(uri: string) { async function listPathContent( uri: string, options: any, - baseUri: string | undefined = undefined + baseUri: string | undefined = undefined, ) { const rootPath = getSafePath(uri); const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); @@ -56,7 +58,7 @@ async function listPathContent( (file) => (options?.include === "folders" && file.isDirectory()) || (options?.include === "files" && file.isFile()) || - options?.include === "all" + options?.include === "all", ) // Filter by gitignore .filter((file) => { @@ -107,6 +109,22 @@ async function handleCreateProject(uri: string) { await fs.promises.mkdir(getSafePath(uri)); } +async function handleDeleteProject(uri: string) { + // Delete the folder at the validated path + await fs.promises.rm(uri, { recursive: true, force: true }); +} + +async function handleUpdateProject( + uri: string, + updatedInfo: { + name: string; + ctime?: Date; + }, +) { + const newUri = path.join(path.dirname(uri), updatedInfo.name); + await fs.promises.rename(uri, newUri); +} + async function handleCreateFolder(uri: string) { // Create a folder at the validated path await fs.promises.mkdir(getSafePath(uri)); @@ -173,60 +191,100 @@ async function handleGetInstallationPath() { export async function handlePlatformAPIRequest( data: any, host: string, - instanceId: string + instanceId: string, ): Promise { const { operation, args } = data; - if (operation === "select-dir") { - throw new Error("Method not implemented."); - } else if (operation === "select-file") { - throw new Error("Method not implemented."); - } else if (operation === "list-projects") { - const { uri }: { uri: string } = args; - return await handleListProjects(uri); - } else if (operation === "list-path-content") { - const { uri, options }: { uri: string; options?: any } = args; - - return await handleListPathContent(uri, options); - } else if (operation === "create-project") { - const { uri }: { uri: string } = args; - await handleCreateProject(uri); - } else if (operation === "create-folder") { - const { uri }: { uri: string } = args; - await handleCreateFolder(uri); - } else if (operation === "create-file") { - const { uri }: { uri: string } = args; - await handleCreateFile(uri); - } else if (operation === "rename") { - const { oldUri, newUri }: { oldUri: string; newUri: string } = args; - await handleRename(oldUri, newUri); - } else if (operation === "delete") { - const { uri }: { uri: string } = args; - await handleDelete(uri); - } else if (operation === "has-path") { - const { uri }: { uri: string } = args; - return await handleHasPath(uri); - } else if (operation === "read-file") { - const { uri }: { uri: string } = args; - return handleReadFile(uri); - } else if (operation === "write-file") { - const { data, uri }: { data: any; uri: string } = args; - await handleWriteFile(data, uri); - } else if (operation === "copy-files") { - const { from, to }: { from: string; to: string } = args; - await handleCopyFiles(from, to); - } else if (operation === "get-persistent-settings") { - return handleLoadSettings(); - } else if (operation === "set-persistent-settings") { - const { settings }: { settings: any } = args; - await handleSaveSettings(settings); - } else if (operation === "reset-persistent-settings") { - await handleSaveSettings({}); - } else if (operation === "get-installation-path") { - return await handleGetInstallationPath(); - } else if (operation === "create-terminal") { - return `${host}/${instanceId}/terminal/ws`; + switch (operation) { + case "select-dir": + // Folder picker is done via web interface + throw new Error("Method not implemented."); + case "select-file": + // File picker is done via web interface + throw new Error("Method not implemented."); + case "list-projects": { + const { uri }: { uri: string } = args; + return await handleListProjects(uri); + } + case "list-path-content": { + const { uri, options }: { uri: string; options?: any } = args; + return await handleListPathContent(uri, options); + } + case "create-project": { + const { uri }: { uri: string } = args; + await handleCreateProject(uri); + return; + } + case "delete-project": { + const { uri }: { uri: string } = args; + await handleDeleteProject(uri); + return; + } + case "update-project": { + const { + uri, + updatedInfo, + }: { + uri: string; + updatedInfo: { + name: string; + ctime?: Date; + }; + } = args; + await handleUpdateProject(uri, updatedInfo); + return; + } + case "create-folder": { + const { uri }: { uri: string } = args; + await handleCreateFolder(uri); + return; + } + case "create-file": { + const { uri }: { uri: string } = args; + await handleCreateFile(uri); + return; + } + case "rename": { + const { oldUri, newUri }: { oldUri: string; newUri: string } = args; + await handleRename(oldUri, newUri); + return; + } + case "delete": { + const { uri }: { uri: string } = args; + await handleDelete(uri); + return; + } + case "has-path": { + const { uri }: { uri: string } = args; + return await handleHasPath(uri); + } + case "read-file": { + const { uri }: { uri: string } = args; + return handleReadFile(uri); + } + case "write-file": { + const { data, uri }: { data: any; uri: string } = args; + await handleWriteFile(data, uri); + return; + } + case "copy-files": { + const { from, to }: { from: string; to: string } = args; + await handleCopyFiles(from, to); + return; + } + case "get-persistent-settings": + return handleLoadSettings(); + case "set-persistent-settings": { + const { settings }: { settings: any } = args; + await handleSaveSettings(settings); + return; + } + case "get-installation-path": + return await handleGetInstallationPath(); + case "create-terminal": + return `${host}/${instanceId}/terminal/ws`; + default: + // Do not reflect input data back to the client, return an explicit error message. + return { error: "Unknown operation" }; } - // Do not reflect input data back to the client, return an explicit error message. - return { error: "Unknown operation" }; } diff --git a/remote-workspace/src/servers/node-pty/index.ts b/remote-workspace/src/servers/node-pty/index.ts index 807fcd6e..6c8f1b0d 100644 --- a/remote-workspace/src/servers/node-pty/index.ts +++ b/remote-workspace/src/servers/node-pty/index.ts @@ -26,14 +26,20 @@ const setSharedTerminalMode = (useSharedTerminal: boolean) => { const handleTerminalConnection = (ws: WebSocket) => { let ptyProcess = sharedTerminalMode ? sharedPtyProcess : spawnShell(); - ws.on("message", (command: string) => { - const processedCommand = commandProcessor(command); - ptyProcess?.write(processedCommand); + ws.on("message", (data: string) => { + const dataObj = JSON.parse(data); + + if (dataObj.type === "input") { + const command = dataObj.payload; + ptyProcess?.write(command); + } else if (dataObj.type === "resize") { + const { cols, rows } = dataObj.payload; + ptyProcess?.resize(cols, rows); + } }); ptyProcess?.onData((rawOutput) => { - const processedOutput = outputProcessor(rawOutput); - ws.send(processedOutput); + ws.send(JSON.stringify({ type: "output", payload: rawOutput })); }); ws.on("close", () => { @@ -56,7 +62,10 @@ const outputProcessor = (output: string) => { /* Host ws node-pty server */ setSharedTerminalMode(false); // Set this to false to allow a shared session -export function createTerminalServer(server: http.Server | https.Server) { +export function addTerminalServer( + server: http.Server | https.Server, + instanceId: string, +) { const wss = new WebSocketServer({ noServer: true }); wss.on("connection", handleTerminalConnection); @@ -80,8 +89,8 @@ export function createTerminalServer(server: http.Server | https.Server) { return; } - const instanceId = match?.[1]; - if (instanceId !== process.env.INSTANCE_ID) { + const id = match?.[1]; + if (id !== instanceId) { socket.write("HTTP/1.1 400 Bad Request: Invalid instance ID\r\n\r\n"); socket.destroy(); return; diff --git a/remote-workspace/tsconfig.json b/remote-workspace/tsconfig.json index fe93931e..cd690105 100644 --- a/remote-workspace/tsconfig.json +++ b/remote-workspace/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { "target": "ES2020", - "module": "node20", + "module": "nodenext", "moduleResolution": "nodenext", "outDir": "dist", "rootDir": "src", diff --git a/web/lib/platform-api/cloud/cloud-api.ts b/web/lib/platform-api/cloud/cloud-api.ts index 34bc620b..1d10dd49 100644 --- a/web/lib/platform-api/cloud/cloud-api.ts +++ b/web/lib/platform-api/cloud/cloud-api.ts @@ -217,10 +217,12 @@ export class CloudAPI extends AbstractPlatformAPI { } async createTerminal(): Promise { - if (!this.workspace) { - toast.error("No workspace selected"); - throw new Error("No workspace selected"); - } - throw new Error("Method not implemented."); + // if (!this.workspace) { + // toast.error("No workspace selected"); + // throw new Error("No workspace selected"); + // } + // throw new Error("Method not implemented."); + + return "ws://localhost:6080/test-workspace/terminal/ws"; } } diff --git a/web/lib/platform-api/electron/electron-api.ts b/web/lib/platform-api/electron/electron-api.ts index 071bf6f2..2912d0f6 100644 --- a/web/lib/platform-api/electron/electron-api.ts +++ b/web/lib/platform-api/electron/electron-api.ts @@ -93,7 +93,7 @@ export class ElectronAPI extends AbstractPlatformAPI { async getPersistentSettings(): Promise { const persistentSettings: PersistentSettings = - await this.electronAPI?.loadSettings(); + await this.electronAPI?.getPersistentSettings(); return persistentSettings; } From 43f66d4f918ab5caa0e66155fe3343ef65a5a0a0 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:28:27 +0800 Subject: [PATCH 03/12] Add workspace specs selection during creation --- .../src/servers/node-pty/index.ts | 9 -- .../modals/workspace-settings-model.tsx | 139 ++++++++++++++++-- web/lib/hooks/use-workspace.ts | 53 ++++++- web/lib/types.ts | 5 +- 4 files changed, 180 insertions(+), 26 deletions(-) diff --git a/remote-workspace/src/servers/node-pty/index.ts b/remote-workspace/src/servers/node-pty/index.ts index 6c8f1b0d..f0385797 100644 --- a/remote-workspace/src/servers/node-pty/index.ts +++ b/remote-workspace/src/servers/node-pty/index.ts @@ -49,15 +49,6 @@ const handleTerminalConnection = (ws: WebSocket) => { }); }; -// Utility function to process commands -const commandProcessor = (command: string) => { - return command; -}; - -// Utility function to process output -const outputProcessor = (output: string) => { - return output; -}; /* Host ws node-pty server */ setSharedTerminalMode(false); // Set this to false to allow a shared session diff --git a/web/components/modals/workspace-settings-model.tsx b/web/components/modals/workspace-settings-model.tsx index 3eb796c7..bc5e07ea 100644 --- a/web/components/modals/workspace-settings-model.tsx +++ b/web/components/modals/workspace-settings-model.tsx @@ -1,12 +1,34 @@ "use client"; -import { Button, Input, Select, SelectItem, Switch } from "@heroui/react"; -import ModalWrapper from "./modal-wrapper"; -import { useEffect, useState } from "react"; -import toast from "react-hot-toast"; import { usePlatformApi } from "@/lib/hooks/use-platform-api"; -import { RemoteWorkspace } from "@/lib/types"; import { useWorkspace } from "@/lib/hooks/use-workspace"; +import { RemoteWorkspace } from "@/lib/types"; +import { + addToast, + Button, + Input, + NumberInput, + Select, + SelectItem, +} from "@heroui/react"; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; +import ModalWrapper from "./modal-wrapper"; + +const specsOptions = [ + { key: "cpu-1-2", vCPU: 1, ram: 2 }, + { key: "cpu-2-4", vCPU: 2, ram: 4 }, + { key: "cpu-4-8", vCPU: 4, ram: 8 }, +]; + +function getNumberFromUnitString(value: string) { + // Assumes the value is in the format "10Gi", "512Mi", etc. + return parseInt(value.replace(/\D/g, "")); +} + +function getUnitFromUnitString(value: string, unit: string) { + return `${value}${unit}`; +} export default function WorkspaceSettingsModal({ isOpen, @@ -19,11 +41,24 @@ export default function WorkspaceSettingsModal({ }) { const { platformApi } = usePlatformApi(); const [workspaceName, setWorkspaceName] = useState(""); - const { workspace, createWorkspace, updateWorkspace } = workspaceHook; + const { workspace, createWorkspace, updateWorkspace, deleteWorkspace } = + workspaceHook; + + const [storage, setStorage] = useState(5); + const [selectedSpec, setSelectedSpec] = useState(specsOptions[0]); useEffect(() => { if (workspace) { + console.log("Workspace loaded:", workspace); setWorkspaceName(workspace.name); + setStorage(getNumberFromUnitString(workspace.volumeSize)); + setSelectedSpec( + specsOptions.find( + (option) => + option.vCPU === getNumberFromUnitString(workspace.cpuLimit) && + option.ram === getNumberFromUnitString(workspace.memoryLimit), + ) ?? specsOptions[0], + ); } }, [workspace]); @@ -48,7 +83,32 @@ export default function WorkspaceSettingsModal({ updateWorkspace(newWorkspace); } - function handleCreateProject() { + async function handleDeleteWorkspace() { + if (!platformApi) { + toast.error("Unknown platform."); + return; + } else if (!workspace) { + toast.error("Workspace is not available."); + return; + } + try { + // Delete workspace + await deleteWorkspace(workspace.id); + addToast({ + title: "Workspace deleted", + description: `Workspace ${workspace.name} has been deleted successfully.`, + color: "success", + }); + } catch (error: any) { + addToast({ + title: "Error deleting workspace", + description: error.message, + color: "danger", + }); + } + } + + async function handleCreateProject() { if (!platformApi) { toast.error("Unknown platform."); return; @@ -61,7 +121,28 @@ export default function WorkspaceSettingsModal({ } // Create workspace - createWorkspace(workspaceName); + try { + const vCPU = selectedSpec.vCPU.toString(); + const ram = getUnitFromUnitString(selectedSpec.ram.toString(), "Gi"); + const volumeSize = getUnitFromUnitString(storage.toString(), "Gi"); + + addToast({ + title: "Creating workspace", + description: `Creating workspace ${workspaceName}. Specifications: ${vCPU} vCPU, ${ram} RAM, ${volumeSize} storage.`, + }); + await createWorkspace(workspaceName, vCPU.toString(), ram, volumeSize); + addToast({ + title: "Workspace created", + description: `Workspace ${workspaceName} has been created successfully.`, + color: "success", + }); + } catch (error: any) { + addToast({ + title: "Error creating workspace", + description: error.message, + color: "danger", + }); + } } return ( @@ -77,11 +158,47 @@ export default function WorkspaceSettingsModal({ value={workspaceName} onValueChange={setWorkspaceName} /> - { + const spec = specsOptions.find( + (option) => option.key === key.currentKey, + ); + if (spec) { + setSelectedSpec(spec); + } + }} + disabledKeys={["more to come"]} + isDisabled={workspace ? true : false} + > + <> + {specsOptions.map((option) => ( + {`${option.vCPU} vCPU, ${option.ram} GB RAM`} + ))} + +

More to come

+
+ + {workspace ? ( - +
+ + + +
) : ( )} diff --git a/web/lib/hooks/use-workspace.ts b/web/lib/hooks/use-workspace.ts index 9df911f2..4dd507b9 100644 --- a/web/lib/hooks/use-workspace.ts +++ b/web/lib/hooks/use-workspace.ts @@ -3,7 +3,7 @@ import { PlatformEnum } from "@/lib/enums"; import { useContext, useEffect, useState } from "react"; import useSWR from "swr"; import { getPlatform } from "../platform-api/platform-checker"; -import { fetchAPI, getAPIUrl } from "../pulse-editor-website/backend"; +import { fetchAPI } from "../pulse-editor-website/backend"; import { RemoteWorkspace } from "../types"; import { useAuth } from "./use-auth"; @@ -38,7 +38,12 @@ export function useWorkspace() { } }, [editorContext?.editorStates?.currentWorkspace]); - async function createWorkspace(name: string) { + async function createWorkspace( + name: string, + cpuLimit: string, + memoryLimit: string, + volumeSize: string, + ) { if (!editorContext) { throw new Error("Editor context is not available"); } else if ( @@ -53,7 +58,17 @@ export function useWorkspace() { } // Request to create a new workspace - const response = await fetchAPI(`/api/workspace/create`); + const response = await fetchAPI(`/api/workspace/create`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, cpuLimit, memoryLimit, volumeSize }), + }); + + if (!response.ok) { + throw new Error(await response.text()); + } const { id, @@ -71,7 +86,9 @@ export function useWorkspace() { currentWorkspace: { id, name, - address: getAPIUrl(`/workspace/${id}`).toString(), + cpuLimit, + memoryLimit, + volumeSize, createdAt, updatedAt, }, @@ -113,11 +130,39 @@ export function useWorkspace() { }); } + async function deleteWorkspace(workspaceId: string) { + if (!editorContext) { + throw new Error("Editor context is not available"); + } + + editorContext.setEditorStates((prev) => { + return { + ...prev, + currentWorkspace: undefined, + }; + }); + + const response = await fetchAPI(`/api/workspace/delete`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ workspaceId }), + }); + + if (!response.ok) { + throw new Error("Failed to delete workspace"); + } + + setWorkspace(undefined); + } + return { workspace, cloudWorkspaces, createWorkspace, updateWorkspace, selectWorkspace, + deleteWorkspace, }; } diff --git a/web/lib/types.ts b/web/lib/types.ts index e7eea679..aa2d06b2 100644 --- a/web/lib/types.ts +++ b/web/lib/types.ts @@ -325,9 +325,10 @@ export type IMCContextType = { export type RemoteWorkspace = { id: string; name: string; - address: string; + cpuLimit: string; + memoryLimit: string; + volumeSize: string; createdAt?: Date; - updatedAt?: Date; }; export type Session = { From 2e10421b93a9e6dcd8dfc7007c3be5e319bab6d3 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:06:07 +0800 Subject: [PATCH 04/12] Fix useWorkspace hook state updates --- .../interface/navigation/nav-top-bar.tsx | 26 ++-- web/components/modals/login-modal.tsx | 12 +- .../modals/workspace-settings-model.tsx | 20 +-- web/lib/hooks/use-platform-api.ts | 12 +- web/lib/hooks/use-workspace.ts | 117 ++++++++---------- 5 files changed, 96 insertions(+), 91 deletions(-) diff --git a/web/components/interface/navigation/nav-top-bar.tsx b/web/components/interface/navigation/nav-top-bar.tsx index 3c05959e..9c56b5f8 100644 --- a/web/components/interface/navigation/nav-top-bar.tsx +++ b/web/components/interface/navigation/nav-top-bar.tsx @@ -1,6 +1,6 @@ import { PlatformEnum } from "@/lib/enums"; -import { useAuth } from "@/lib/hooks/use-auth"; import { useMenuActions } from "@/lib/hooks/menu-actions/use-menu-actions"; +import { useAuth } from "@/lib/hooks/use-auth"; import { useWorkspace } from "@/lib/hooks/use-workspace"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { @@ -104,19 +104,25 @@ export default function NavTopBar({ } size="sm" disabledKeys={workspaceHook.workspace ? [] : ["settings"]} + onSelectionChange={(key) => { + if ( + key.currentKey === "__internal-create-new" || + key.currentKey === "__internal-settings" + ) { + return; + } + const selectedWorkspace = workspaceHook.cloudWorkspaces?.find( + (workspace) => workspace.id === key.currentKey, + ); + workspaceHook.selectWorkspace(selectedWorkspace?.id); + }} > <> {workspaceHook.cloudWorkspaces?.map((workspace) => ( - { - workspaceHook.selectWorkspace(workspace.id); - }} - > - {workspace.name} - + {workspace.name} )) ?? []} { @@ -131,7 +137,7 @@ export default function NavTopBar({ Create New { setIsWorkspaceSettingsModalOpen(true); diff --git a/web/components/modals/login-modal.tsx b/web/components/modals/login-modal.tsx index e348364f..a72d9ff4 100644 --- a/web/components/modals/login-modal.tsx +++ b/web/components/modals/login-modal.tsx @@ -1,4 +1,5 @@ import { PlatformEnum } from "@/lib/enums"; +import { useWorkspace } from "@/lib/hooks/use-workspace"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { Button, Divider, Input } from "@heroui/react"; import { useContext, useEffect, useState } from "react"; @@ -8,6 +9,9 @@ import ModalWrapper from "./modal-wrapper"; export default function LoginModal({ signIn }: { signIn: () => void }) { const editorContext = useContext(EditorContext); + + const { workspace } = useWorkspace(); + const [workspaceAddress, setWorkspaceAddress] = useState( undefined, ); @@ -16,10 +20,10 @@ export default function LoginModal({ signIn }: { signIn: () => void }) { // Open remote instance selection if the current platform is web useEffect(() => { - if (!editorContext?.editorStates?.currentWorkspace) { + if (!workspace) { setIsModelOpen(true); } - }, [editorContext?.editorStates?.currentWorkspace]); + }, [workspace]); return ( void }) { currentWorkspace: { id: "self-hosted", name: "Self-hosted Workspace", - address: workspaceAddress, + cpuLimit: "N/A", + memoryLimit: "N/A", + volumeSize: "N/A", }, }; }); diff --git a/web/components/modals/workspace-settings-model.tsx b/web/components/modals/workspace-settings-model.tsx index bc5e07ea..fa2aeaff 100644 --- a/web/components/modals/workspace-settings-model.tsx +++ b/web/components/modals/workspace-settings-model.tsx @@ -2,7 +2,6 @@ import { usePlatformApi } from "@/lib/hooks/use-platform-api"; import { useWorkspace } from "@/lib/hooks/use-workspace"; -import { RemoteWorkspace } from "@/lib/types"; import { addToast, Button, @@ -62,7 +61,7 @@ export default function WorkspaceSettingsModal({ } }, [workspace]); - function handleUpdateWorkspace() { + async function handleUpdateWorkspace() { if (!platformApi) { toast.error("Unknown platform."); return; @@ -74,13 +73,15 @@ export default function WorkspaceSettingsModal({ return; } - const newWorkspace: RemoteWorkspace = { - ...workspace, - name: workspaceName, - }; - // Update workspace - updateWorkspace(newWorkspace); + await updateWorkspace(workspace.id, workspaceName); + + addToast({ + title: "Workspace updated", + description: `Workspace ${workspaceName} has been updated successfully.`, + color: "success", + }); + setIsOpen(false); } async function handleDeleteWorkspace() { @@ -99,6 +100,8 @@ export default function WorkspaceSettingsModal({ description: `Workspace ${workspace.name} has been deleted successfully.`, color: "success", }); + + setIsOpen(false); } catch (error: any) { addToast({ title: "Error deleting workspace", @@ -136,6 +139,7 @@ export default function WorkspaceSettingsModal({ description: `Workspace ${workspaceName} has been created successfully.`, color: "success", }); + setIsOpen(false); } catch (error: any) { addToast({ title: "Error creating workspace", diff --git a/web/lib/hooks/use-platform-api.ts b/web/lib/hooks/use-platform-api.ts index 7b1dd010..29bdc5d7 100644 --- a/web/lib/hooks/use-platform-api.ts +++ b/web/lib/hooks/use-platform-api.ts @@ -1,14 +1,15 @@ -import { EditorContext } from "@/components/providers/editor-context-provider"; import { PlatformEnum } from "@/lib/enums"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { AbstractPlatformAPI } from "../platform-api/abstract-platform-api"; import { CapacitorAPI } from "../platform-api/capacitor/capacitor-api"; import { CloudAPI } from "../platform-api/cloud/cloud-api"; import { ElectronAPI } from "../platform-api/electron/electron-api"; import { getPlatform } from "../platform-api/platform-checker"; +import { useWorkspace } from "./use-workspace"; export function usePlatformApi() { - const editorContext = useContext(EditorContext); + const { workspace } = useWorkspace(); + const [platformApi, setPlatformApi] = useState< AbstractPlatformAPI | undefined >(undefined); @@ -20,11 +21,11 @@ export function usePlatformApi() { // When workspace changes, reset platform API if needed useEffect(() => { - if (platformApi && editorContext?.editorStates.currentWorkspace) { + if (platformApi && workspace) { const api = getAbstractPlatformAPI(); setPlatformApi(api); } - }, [editorContext?.editorStates.currentWorkspace]); + }, [workspace]); function getAbstractPlatformAPI(): AbstractPlatformAPI { const platform = getPlatform(); @@ -37,7 +38,6 @@ export function usePlatformApi() { platform === PlatformEnum.Web || platform === PlatformEnum.WebMobile ) { - const workspace = editorContext?.editorStates.currentWorkspace; return new CloudAPI(workspace); } else if (platform === PlatformEnum.VSCode) { // platformApi.current = new VSCodeAPI(); diff --git a/web/lib/hooks/use-workspace.ts b/web/lib/hooks/use-workspace.ts index 4dd507b9..6981a47a 100644 --- a/web/lib/hooks/use-workspace.ts +++ b/web/lib/hooks/use-workspace.ts @@ -1,6 +1,6 @@ import { EditorContext } from "@/components/providers/editor-context-provider"; import { PlatformEnum } from "@/lib/enums"; -import { useContext, useEffect, useState } from "react"; +import { useContext } from "react"; import useSWR from "swr"; import { getPlatform } from "../platform-api/platform-checker"; import { fetchAPI } from "../pulse-editor-website/backend"; @@ -9,34 +9,36 @@ import { useAuth } from "./use-auth"; export function useWorkspace() { const editorContext = useContext(EditorContext); - const [workspace, setWorkspace] = useState( - undefined, - ); const { session } = useAuth(); - const { data: cloudWorkspaces } = useSWR( - session ? `/api/workspace/list` : null, - async (url: string) => { - const res = await fetchAPI(url); - if (!res.ok) { - throw new Error("Failed to fetch workspace data"); - } - const { - workspaces, - }: { - workspaces: RemoteWorkspace[]; - } = await res.json(); - - return workspaces; - }, - ); - - // Update workspace state when the editor context changes - useEffect(() => { - if (editorContext?.editorStates?.currentWorkspace) { - setWorkspace(editorContext.editorStates.currentWorkspace); + const { data: cloudWorkspaces, mutate: mutateCloudWorkspaces } = useSWR< + RemoteWorkspace[] + >(session ? `/api/workspace/list` : null, async (url: string) => { + const res = await fetchAPI(url); + if (!res.ok) { + throw new Error("Failed to fetch workspace data"); } - }, [editorContext?.editorStates?.currentWorkspace]); + const { + workspaces, + }: { + workspaces: RemoteWorkspace[]; + } = await res.json(); + + return workspaces; + }); + + const workspace = editorContext?.editorStates?.currentWorkspace; + const setWorkspace = (ws: RemoteWorkspace | undefined) => { + if (!editorContext) { + throw new Error("Editor context is not available"); + } + editorContext.setEditorStates((prev) => { + return { + ...prev, + currentWorkspace: ws, + }; + }); + }; async function createWorkspace( name: string, @@ -72,48 +74,46 @@ export function useWorkspace() { const { id, - createdAt, - updatedAt, }: { id: string; - createdAt: Date; - updatedAt: Date; } = await response.json(); - editorContext.setEditorStates((prev) => { - return { - ...prev, - currentWorkspace: { - id, - name, - cpuLimit, - memoryLimit, - volumeSize, - createdAt, - updatedAt, - }, - }; - }); + const updated = await mutateCloudWorkspaces(); + const newWorkspace = updated?.find((ws) => ws.id === id); + setWorkspace(newWorkspace); } - function updateWorkspace(updatedWorkspace: RemoteWorkspace) { + async function updateWorkspace(workspaceId: string, name: string) { if (!editorContext) { throw new Error("Editor context is not available"); } - editorContext.setEditorStates((prev) => { - return { - ...prev, - currentWorkspace: updatedWorkspace, - }; + // Request to update the workspace + await fetchAPI(`/api/workspace/update`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + workspaceId, + }), }); + + await mutateCloudWorkspaces(); } - function selectWorkspace(workspaceId: string) { + function selectWorkspace(workspaceId: string | undefined) { if (!editorContext) { throw new Error("Editor context is not available"); } + if (!workspaceId) { + // Unselect workspace + setWorkspace(undefined); + return; + } + const selectedWorkspace = cloudWorkspaces?.find( (ws) => ws.id === workspaceId, ); @@ -122,12 +122,7 @@ export function useWorkspace() { throw new Error("Workspace not found"); } - editorContext.setEditorStates((prev) => { - return { - ...prev, - currentWorkspace: selectedWorkspace, - }; - }); + setWorkspace(selectedWorkspace); } async function deleteWorkspace(workspaceId: string) { @@ -135,13 +130,6 @@ export function useWorkspace() { throw new Error("Editor context is not available"); } - editorContext.setEditorStates((prev) => { - return { - ...prev, - currentWorkspace: undefined, - }; - }); - const response = await fetchAPI(`/api/workspace/delete`, { method: "DELETE", headers: { @@ -155,6 +143,7 @@ export function useWorkspace() { } setWorkspace(undefined); + mutateCloudWorkspaces(); } return { From c6a71fc330211e4e902a6fd0957318dd911fe7c2 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:51:06 +0800 Subject: [PATCH 05/12] Allow using workspace websocket for terminal --- npm-packages/cli/package.json | 6 +++--- .../cli/source/components/commands/create.tsx | 3 ++- .../source/components/commands/publish.tsx | 14 ++++++++++++-- remote-workspace/dockerfile | 10 ++++++++-- .../src/servers/node-pty/index.ts | 3 ++- web/components/providers/imc-provider.tsx | 2 +- web/components/tools/voice.tsx | 2 +- web/lib/hooks/use-platform-ai-assistant.ts | 4 ++-- web/lib/platform-api/cloud/cloud-api.ts | 19 ++++++++++--------- 9 files changed, 41 insertions(+), 22 deletions(-) diff --git a/npm-packages/cli/package.json b/npm-packages/cli/package.json index 46768f7a..dc20f3bb 100644 --- a/npm-packages/cli/package.json +++ b/npm-packages/cli/package.json @@ -1,9 +1,9 @@ { "name": "@pulse-editor/cli", - "version": "0.1.0-beta.5", + "version": "0.1.0-beta.8", "license": "MIT", "bin": { - "pulse": "./dist/cli.js" + "pulse": "dist/cli.js" }, "type": "module", "engines": { @@ -60,4 +60,4 @@ } }, "prettier": "@vdemedes/prettier-config" -} \ No newline at end of file +} diff --git a/npm-packages/cli/source/components/commands/create.tsx b/npm-packages/cli/source/components/commands/create.tsx index 8ae10632..7f9ac339 100644 --- a/npm-packages/cli/source/components/commands/create.tsx +++ b/npm-packages/cli/source/components/commands/create.tsx @@ -165,8 +165,9 @@ export default function Create({cli}: {cli: Result}) { try { await execa(`npm install`, { cwd: path.join(process.cwd(), name), + shell: true, }); - } catch (error) { + } catch (error: any) { setCreateMessage( ❌ Failed to install dependencies. Please check your internet diff --git a/npm-packages/cli/source/components/commands/publish.tsx b/npm-packages/cli/source/components/commands/publish.tsx index e364175d..0781edc9 100644 --- a/npm-packages/cli/source/components/commands/publish.tsx +++ b/npm-packages/cli/source/components/commands/publish.tsx @@ -54,13 +54,23 @@ export default function Publish({cli}: {cli: Result}) { // Build the extension useEffect(() => { async function buildExtension() { + setIsBuilding(true); try { - setIsBuilding(true); await $`npm run build`; - // Zip the dist folder + } + catch (error) { + setIsBuildingError(true); + setIsBuilding(false); + setFailureMessage('Build failed. Please run `npm run build` to see the error.'); + return; + } + // Zip the dist folder + try { await $({cwd: 'dist'})`zip -r ../node_modules/@pulse-editor/dist.zip *`; } catch (error) { setIsBuildingError(true); + setIsBuilding(false); + setFailureMessage('Failed to zip the build output.'); return; } finally { setIsBuilding(false); diff --git a/remote-workspace/dockerfile b/remote-workspace/dockerfile index ecd9727b..d76a87d3 100644 --- a/remote-workspace/dockerfile +++ b/remote-workspace/dockerfile @@ -1,7 +1,7 @@ FROM ubuntu:latest ENV NODE_VERSION=20 -WORKDIR /app +WORKDIR /pulse-editor/server # install curl RUN apt update && apt install -y curl make python3 build-essential @@ -12,11 +12,17 @@ RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | b # set env ENV NVM_DIR=/root/.nvm -COPY . /app +COPY . /pulse-editor/server # install node RUN bash -c "source $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && npm install && npm run build" +# install pulse cli +RUN bash -c "source $NVM_DIR/nvm.sh && npm install -g @pulse-editor/cli@beta" + +# install git, zip, unzip +RUN apt install -y git zip unzip + # Generate self-signed certificate # RUN bash -c "./utils/generate-self-signed.sh" diff --git a/remote-workspace/src/servers/node-pty/index.ts b/remote-workspace/src/servers/node-pty/index.ts index f0385797..6810742d 100644 --- a/remote-workspace/src/servers/node-pty/index.ts +++ b/remote-workspace/src/servers/node-pty/index.ts @@ -13,6 +13,7 @@ const spawnShell = () => { return spawn(shell, [], { name: "xterm-color", env: process.env, + cwd: "/workspace" }); }; @@ -51,7 +52,7 @@ const handleTerminalConnection = (ws: WebSocket) => { /* Host ws node-pty server */ -setSharedTerminalMode(false); // Set this to false to allow a shared session +setSharedTerminalMode(true); export function addTerminalServer( server: http.Server | https.Server, diff --git a/web/components/providers/imc-provider.tsx b/web/components/providers/imc-provider.tsx index ae4d92fc..937fe583 100644 --- a/web/components/providers/imc-provider.tsx +++ b/web/components/providers/imc-provider.tsx @@ -169,7 +169,7 @@ export default function InterModuleCommunicationProvider({ throw new Error("Agent method not found."); } - if (editorContext?.persistSettings?.isUseManagedCloud) { + if (editorContext?.persistSettings?.isUseManagedCloud ?? true) { const result = await runAgentMethodCloud(agent, methodName, args); return result; diff --git a/web/components/tools/voice.tsx b/web/components/tools/voice.tsx index 6e79d0ae..eaa64254 100644 --- a/web/components/tools/voice.tsx +++ b/web/components/tools/voice.tsx @@ -63,7 +63,7 @@ export default function Voice({ } } - if (isUseManagedCloud) { + if (isUseManagedCloud ?? true) { return ( { - // if (!this.workspace) { - // toast.error("No workspace selected"); - // throw new Error("No workspace selected"); - // } - // throw new Error("Method not implemented."); + if (!this.workspace) { + toast.error("No workspace selected"); + throw new Error("No workspace selected"); + } + + const url = `wss://${this.workspace?.id}.workspace.pulse-editor.com/${this.workspace?.id}/terminal/ws`; - return "ws://localhost:6080/test-workspace/terminal/ws"; + return url; } } From 3eff3fb63722df6829491f9237f5a83272370754 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Sun, 26 Oct 2025 01:00:36 +0800 Subject: [PATCH 06/12] Rework project & workspace UX --- .../source/components/commands/publish.tsx | 4 +- .../src/servers/api-server/index.ts | 17 +- .../api-server/platform-api/handler.ts | 264 +++++++++--------- .../explorer/file-system/fs-explorer.tsx | 43 +-- .../explorer/file-system/tree-view.tsx | 45 +-- .../explorer/project/project-explorer.tsx | 49 +++- .../explorer/project/project-item.tsx | 28 +- .../explorer/workspace/workspace-explorer.tsx | 153 +++++++++- .../interface/navigation/nav-side-menu.tsx | 44 +-- .../interface/navigation/nav-top-bar.tsx | 80 ------ web/components/interface/navigation/nav.tsx | 15 +- .../interface/project-indicator.tsx | 8 +- .../marketplace/app/app-gallery.tsx | 10 +- .../modals/project-settings-modal.tsx | 18 +- web/components/modals/sharing-modal.tsx | 45 ++- web/lib/enums.ts | 4 +- web/lib/hooks/use-extension-manager.ts | 2 +- web/lib/hooks/use-workspace.ts | 31 ++ web/lib/platform-api/cloud/cloud-api.ts | 206 ++++++++++++-- web/lib/platform-api/remote/remote-api.ts | 7 + web/lib/types.ts | 22 +- 21 files changed, 654 insertions(+), 441 deletions(-) create mode 100644 web/lib/platform-api/remote/remote-api.ts diff --git a/npm-packages/cli/source/components/commands/publish.tsx b/npm-packages/cli/source/components/commands/publish.tsx index 0781edc9..b3b43f5f 100644 --- a/npm-packages/cli/source/components/commands/publish.tsx +++ b/npm-packages/cli/source/components/commands/publish.tsx @@ -103,8 +103,8 @@ export default function Publish({cli}: {cli: Result}) { // Send the file to the server const res = await fetch( cli.flags.dev - ? 'https://localhost:8080/api/extension/publish' - : 'https://pulse-editor.com/api/extension/publish', + ? 'https://localhost:8080/api/app/publish' + : 'https://pulse-editor.com/api/app/publish', { method: 'POST', headers: { diff --git a/remote-workspace/src/servers/api-server/index.ts b/remote-workspace/src/servers/api-server/index.ts index d1b4575a..f39e7bb9 100644 --- a/remote-workspace/src/servers/api-server/index.ts +++ b/remote-workspace/src/servers/api-server/index.ts @@ -57,19 +57,22 @@ async function createEndpoints( } // Get json body - const body = req.body; + const { operation, args } = req.body; - console.log("Received platform API request:", body); - - const host = req.host; - - const result = await handlePlatformAPIRequest(body, host, id); + const result = await handlePlatformAPIRequest( + { + operation, + args, + }, + req.get("host") ?? "", + instanceId, + ); // Process the request and send a response if (result && result.error) { res.status(400).json(result); } else { - res.send(result); + res.send(JSON.stringify(result)); } }); } diff --git a/remote-workspace/src/servers/api-server/platform-api/handler.ts b/remote-workspace/src/servers/api-server/platform-api/handler.ts index 8ce188be..e8cdea24 100644 --- a/remote-workspace/src/servers/api-server/platform-api/handler.ts +++ b/remote-workspace/src/servers/api-server/platform-api/handler.ts @@ -3,35 +3,117 @@ import ignore from "ignore"; import path from "path"; // Define a safe root directory for projects. Can be overridden by env or configured as needed. -const PROJECTS_ROOT = process.env.PROJECTS_ROOT ?? "/srv/projects"; -const settingsPath = path.join(PROJECTS_ROOT, "settings.json"); +const settingsPath = path.join("/pulse-editor", "settings.json"); -// Utility to resolve and validate user-supplied uri inside PROJECTS_ROOT -function getSafePath(uri: string): string { - // Prevent empty/undefined input - if (!uri || typeof uri !== "string") { - throw new Error("Invalid project path"); - } - // Resolve against the root directory - const resolved = path.resolve(PROJECTS_ROOT, uri); - // Use fs.realpathSync to follow symlinks - let normalized; - try { - normalized = fs.realpathSync(resolved); - } catch { - // If path does not exist yet (e.g., on creation), just use resolved. - normalized = resolved; - } - // Ensure the normalized path is inside the root - if (!normalized.startsWith(PROJECTS_ROOT)) { - throw new Error("Access to paths outside projects root denied"); +export async function handlePlatformAPIRequest( + data: { + operation: string; + args: any; + }, + host: string, + instanceId: string, +): Promise { + const { operation, args } = data; + + switch (operation) { + case "select-dir": + // Folder picker is done via web interface + throw new Error("Method not implemented."); + case "select-file": + // File picker is done via web interface + throw new Error("Method not implemented."); + case "list-projects": { + const { uri }: { uri: string } = args; + return await handleListProjects(uri); + } + case "list-path-content": { + const { uri, options }: { uri: string; options?: any } = args; + return await handleListPathContent(uri, options); + } + case "create-project": { + const { uri }: { uri: string } = args; + await handleCreateProject(uri); + return; + } + case "delete-project": { + const { uri }: { uri: string } = args; + await handleDeleteProject(uri); + return; + } + case "update-project": { + const { + uri, + updatedInfo, + }: { + uri: string; + updatedInfo: { + name: string; + ctime?: Date; + }; + } = args; + await handleUpdateProject(uri, updatedInfo); + return; + } + case "create-folder": { + const { uri }: { uri: string } = args; + await handleCreateFolder(uri); + return; + } + case "create-file": { + const { uri }: { uri: string } = args; + await handleCreateFile(uri); + return; + } + case "rename": { + const { oldUri, newUri }: { oldUri: string; newUri: string } = args; + await handleRename(oldUri, newUri); + return; + } + case "delete": { + const { uri }: { uri: string } = args; + await handleDelete(uri); + return; + } + case "has-path": { + const { uri }: { uri: string } = args; + return await handleHasPath(uri); + } + case "read-file": { + const { uri }: { uri: string } = args; + return await handleReadFile(uri); + } + case "write-file": { + const { data, uri }: { data: any; uri: string } = args; + await handleWriteFile(data, uri); + return; + } + case "copy-files": { + const { from, to }: { from: string; to: string } = args; + await handleCopyFiles(from, to); + return; + } + case "get-persistent-settings": + return handleLoadSettings(); + case "set-persistent-settings": { + const { settings }: { settings: any } = args; + await handleSaveSettings(settings); + return; + } + case "get-installation-path": + return await handleGetInstallationPath(); + case "create-terminal": + return `${host}/${instanceId}/terminal/ws`; + default: + // Do not reflect input data back to the client, return an explicit error message. + return { error: "Unknown operation" }; } - return normalized; } + + // List all folders in a path async function handleListProjects(uri: string) { - const rootPath = getSafePath(uri); + const rootPath = uri; const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); const folders = files .filter((file) => file.isDirectory()) @@ -49,7 +131,7 @@ async function listPathContent( options: any, baseUri: string | undefined = undefined, ) { - const rootPath = getSafePath(uri); + const rootPath = uri; const files = await fs.promises.readdir(rootPath, { withFileTypes: true }); const promise: Promise[] = files @@ -106,7 +188,7 @@ async function handleListPathContent(uri: string, options: any) { async function handleCreateProject(uri: string) { // Create a folder at the validated path - await fs.promises.mkdir(getSafePath(uri)); + await fs.promises.mkdir(uri); } async function handleDeleteProject(uri: string) { @@ -127,36 +209,45 @@ async function handleUpdateProject( async function handleCreateFolder(uri: string) { // Create a folder at the validated path - await fs.promises.mkdir(getSafePath(uri)); + await fs.promises.mkdir(uri); } async function handleCreateFile(uri: string) { // Create a file at the validated path - await fs.promises.writeFile(getSafePath(uri), ""); + await fs.promises.writeFile(uri, ""); } async function handleRename(oldUri: string, newUri: string) { - await fs.promises.rename(getSafePath(oldUri), getSafePath(newUri)); + await fs.promises.rename( + oldUri, + newUri, + ); } async function handleDelete(uri: string) { - await fs.promises.rm(getSafePath(uri), { recursive: true, force: true }); + await fs.promises.rm(uri, { + recursive: true, + force: true, + }); } async function handleHasPath(uri: string) { - return fs.existsSync(getSafePath(uri)); + return fs.existsSync(uri); } async function handleReadFile(uri: string) { // Read the file at validated path - const data = await fs.promises.readFile(getSafePath(uri), "utf-8"); + const data = await fs.promises.readFile( + uri, + "utf-8", + ); return data; } async function handleWriteFile(data: any, uri: string) { // Write the data at validated path - const safePath = getSafePath(uri); + const safePath = uri; // create parent directory if it doesn't exist const dir = path.dirname(safePath); if (!fs.existsSync(dir)) { @@ -168,7 +259,13 @@ async function handleWriteFile(data: any, uri: string) { async function handleCopyFiles(from: string, to: string) { // Copy the files from the validated from path to the validated to path - await fs.promises.cp(getSafePath(from), getSafePath(to), { recursive: true }); + await fs.promises.cp( + from, + to, + { + recursive: true, + }, + ); } async function handleLoadSettings() { @@ -187,104 +284,3 @@ async function handleGetInstallationPath() { const uri = "~/pulse-editor"; return uri; } - -export async function handlePlatformAPIRequest( - data: any, - host: string, - instanceId: string, -): Promise { - const { operation, args } = data; - - switch (operation) { - case "select-dir": - // Folder picker is done via web interface - throw new Error("Method not implemented."); - case "select-file": - // File picker is done via web interface - throw new Error("Method not implemented."); - case "list-projects": { - const { uri }: { uri: string } = args; - return await handleListProjects(uri); - } - case "list-path-content": { - const { uri, options }: { uri: string; options?: any } = args; - return await handleListPathContent(uri, options); - } - case "create-project": { - const { uri }: { uri: string } = args; - await handleCreateProject(uri); - return; - } - case "delete-project": { - const { uri }: { uri: string } = args; - await handleDeleteProject(uri); - return; - } - case "update-project": { - const { - uri, - updatedInfo, - }: { - uri: string; - updatedInfo: { - name: string; - ctime?: Date; - }; - } = args; - await handleUpdateProject(uri, updatedInfo); - return; - } - case "create-folder": { - const { uri }: { uri: string } = args; - await handleCreateFolder(uri); - return; - } - case "create-file": { - const { uri }: { uri: string } = args; - await handleCreateFile(uri); - return; - } - case "rename": { - const { oldUri, newUri }: { oldUri: string; newUri: string } = args; - await handleRename(oldUri, newUri); - return; - } - case "delete": { - const { uri }: { uri: string } = args; - await handleDelete(uri); - return; - } - case "has-path": { - const { uri }: { uri: string } = args; - return await handleHasPath(uri); - } - case "read-file": { - const { uri }: { uri: string } = args; - return handleReadFile(uri); - } - case "write-file": { - const { data, uri }: { data: any; uri: string } = args; - await handleWriteFile(data, uri); - return; - } - case "copy-files": { - const { from, to }: { from: string; to: string } = args; - await handleCopyFiles(from, to); - return; - } - case "get-persistent-settings": - return handleLoadSettings(); - case "set-persistent-settings": { - const { settings }: { settings: any } = args; - await handleSaveSettings(settings); - return; - } - case "get-installation-path": - return await handleGetInstallationPath(); - case "create-terminal": - return `${host}/${instanceId}/terminal/ws`; - default: - // Do not reflect input data back to the client, return an explicit error message. - return { error: "Unknown operation" }; - } -} diff --git a/web/components/explorer/file-system/fs-explorer.tsx b/web/components/explorer/file-system/fs-explorer.tsx index 3703c721..506bc138 100644 --- a/web/components/explorer/file-system/fs-explorer.tsx +++ b/web/components/explorer/file-system/fs-explorer.tsx @@ -24,10 +24,17 @@ export default function FileSystemExplorer({ const platform = getPlatform(); const { platformApi } = usePlatformApi(); - const { activeTabView, closeAllTabViews } = useTabViewManager(); + const { activeTabView } = useTabViewManager(); const rootGroupRef = useRef(null); + const content = editorContext?.editorStates.workspaceContent ?? []; + + const fsPath = + editorContext?.persistSettings?.projectHomePath + + "/" + + editorContext?.editorStates.project; + // Reset root group ref when there are other nodes selected useEffect(() => { const selectedNodes = @@ -172,22 +179,6 @@ export default function FileSystemExplorer({
- {/* @@ -200,24 +191,18 @@ export default function FileSystemExplorer({
- {editorContext?.editorStates.projectContent?.length === 0 && ( -
-

- Empty content. Create a new file to get started. -

-
+ {content?.length === 0 && ( +

+ Empty content. Create a new file to get started. +

)}
diff --git a/web/components/explorer/file-system/tree-view.tsx b/web/components/explorer/file-system/tree-view.tsx index 57580360..c828d030 100644 --- a/web/components/explorer/file-system/tree-view.tsx +++ b/web/components/explorer/file-system/tree-view.tsx @@ -4,11 +4,11 @@ import ContextMenu from "@/components/interface/context-menu"; import Icon from "@/components/misc/icon"; import { EditorContext } from "@/components/providers/editor-context-provider"; import { DragEventTypeEnum, PlatformEnum } from "@/lib/enums"; +import { useWorkspace } from "@/lib/hooks/use-workspace"; import { AbstractPlatformAPI } from "@/lib/platform-api/abstract-platform-api"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { ContextMenuState, - EditorContextType, FileDragData, FileSystemObject, TreeViewGroupRef, @@ -27,34 +27,6 @@ import { } from "react"; import toast from "react-hot-toast"; -function refreshProjectContent( - platformApi: AbstractPlatformAPI, - editorContext: EditorContextType | undefined, -) { - const projectUri = - editorContext?.persistSettings?.projectHomePath + - "/" + - editorContext?.editorStates.project; - platformApi - ?.listPathContent(projectUri, { - include: "all", - isRecursive: true, - }) - .then((objects) => { - editorContext?.setEditorStates((prev) => { - return { - ...prev, - projectContent: objects, - explorerSelectedNodeRefs: [], - }; - }); - - console.log("Found project content:", objects); - - toast.success("Project content updated."); - }); -} - // A tree view node that represents a single file or folder const TreeViewNode = forwardRef(function TreeViewNode( { @@ -82,6 +54,8 @@ const TreeViewNode = forwardRef(function TreeViewNode( }, })); + const { refreshWorkspaceContent } = useWorkspace(); + const [isFolderCollapsed, setIsFolderCollapsed] = useState(true); const [isSelected, setIsSelected] = useState(false); const editorContext = useContext(EditorContext); @@ -218,7 +192,7 @@ const TreeViewNode = forwardRef(function TreeViewNode( parentGroupRef.current?.getFolderUri() + "/" + newName; platformApi?.rename(object.uri, newUri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setIsRenaming(false); @@ -230,7 +204,7 @@ const TreeViewNode = forwardRef(function TreeViewNode( parentGroupRef.current?.getFolderUri() + "/" + newName; platformApi?.rename(object.uri, newUri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setIsRenaming(false); @@ -335,7 +309,7 @@ const TreeViewNode = forwardRef(function TreeViewNode( color="danger" onPress={(e) => { platformApi?.delete(object.uri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setContextMenuState({ x: 0, y: 0, isOpen: false }); }} @@ -452,12 +426,13 @@ const TreeViewGroup = forwardRef(function TreeViewGroup( }, })); + const { refreshWorkspaceContent } = useWorkspace(); + const [isCreatingNewFile, setIsCreatingNewFile] = useState(false); const [isCreatingNewFolder, setIsCreatingNewFolder] = useState(false); const [folderNameInputValue, setFolderNameInputValue] = useState(""); const [fileNameInputValue, setFileNameInputValue] = useState(""); - const editorContext = useContext(EditorContext); function createNewFolder(uri: string) { console.log("Creating new folder with uri:", uri); @@ -468,7 +443,7 @@ const TreeViewGroup = forwardRef(function TreeViewGroup( } platformApi.createFolder(uri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setFolderNameInputValue(""); @@ -484,7 +459,7 @@ const TreeViewGroup = forwardRef(function TreeViewGroup( } platformApi.createFile(uri).then(() => { - refreshProjectContent(platformApi, editorContext); + refreshWorkspaceContent(platformApi); }); setFileNameInputValue(""); diff --git a/web/components/explorer/project/project-explorer.tsx b/web/components/explorer/project/project-explorer.tsx index 272f8214..ae14b001 100644 --- a/web/components/explorer/project/project-explorer.tsx +++ b/web/components/explorer/project/project-explorer.tsx @@ -1,7 +1,9 @@ "use client"; +import { SideMenuTabEnum } from "@/lib/enums"; import { usePlatformApi } from "@/lib/hooks/use-platform-api"; import { ProjectInfo } from "@/lib/types"; +import { Button } from "@heroui/react"; import { useContext, useEffect, useState } from "react"; import ProjectSettingsModal from "../../modals/project-settings-modal"; import { EditorContext } from "../../providers/editor-context-provider"; @@ -17,6 +19,12 @@ export default function ProjectExplorer() { ProjectInfo | undefined >(undefined); + useEffect(() => { + if (editorContext?.editorStates.project) { + // Get workflows stored either on cloud or locally in project/.workflows + } + }, [editorContext?.editorStates.project]); + useEffect(() => { if (platformApi) { const homePath = editorContext?.persistSettings?.projectHomePath; @@ -34,19 +42,36 @@ export default function ProjectExplorer() { return (
- {editorContext?.editorStates.projectsInfo?.map((project, index) => ( - +

View Projects

+ + {editorContext?.editorStates.projectsInfo?.map((project, index) => ( + { + editorContext.setEditorStates((prev) => ({ + ...prev, + sideMenuTab: SideMenuTabEnum.Apps, + })); + }} + /> + ))} + - ))} - +
); } diff --git a/web/components/explorer/project/project-item.tsx b/web/components/explorer/project/project-item.tsx index b732f8d2..f565c15d 100644 --- a/web/components/explorer/project/project-item.tsx +++ b/web/components/explorer/project/project-item.tsx @@ -1,6 +1,4 @@ -import { PlatformEnum, SideMenuTabEnum } from "@/lib/enums"; import { usePlatformApi } from "@/lib/hooks/use-platform-api"; -import { getPlatform, isWeb } from "@/lib/platform-api/platform-checker"; import { ContextMenuState, ProjectInfo } from "@/lib/types"; import { Button } from "@heroui/react"; import { useContext, useState } from "react"; @@ -11,10 +9,12 @@ export default function ProjectItem({ project, setSettingsOpen, setSettingsProject, + onOpen, }: { project: ProjectInfo; setSettingsOpen: (isOpen: boolean) => void; setSettingsProject: (project: ProjectInfo) => void; + onOpen?: () => void; }) { const editorContext = useContext(EditorContext); @@ -38,27 +38,6 @@ export default function ProjectItem({ project: projectName, }; }); - - if (getPlatform() === PlatformEnum.Electron) { - const uri = - editorContext?.persistSettings?.projectHomePath + "/" + projectName; - platformApi - ?.listPathContent(uri, { - include: "all", - isRecursive: true, - }) - .then((objects) => { - editorContext?.setEditorStates((prev) => { - return { - ...prev, - project: projectName, - projectContent: objects, - }; - }); - }); - } else if (isWeb()) { - // TODO: move this to when workspace is loaded - } } function formatDateTime(date: Date) { @@ -80,6 +59,9 @@ export default function ProjectItem({ // Only open project if context menu is not open if (!contextMenuState.isOpen) { openProject(projectName); + if (onOpen) { + onOpen(); + } } }} onContextMenu={(e) => { diff --git a/web/components/explorer/workspace/workspace-explorer.tsx b/web/components/explorer/workspace/workspace-explorer.tsx index bd2f35a8..1b4e9777 100644 --- a/web/components/explorer/workspace/workspace-explorer.tsx +++ b/web/components/explorer/workspace/workspace-explorer.tsx @@ -1,5 +1,154 @@ -import WIP from "@/components/interface/status-screens/wip"; +import Icon from "@/components/misc/icon"; +import WorkspaceSettingsModal from "@/components/modals/workspace-settings-model"; +import { EditorContext } from "@/components/providers/editor-context-provider"; +import { usePlatformApi } from "@/lib/hooks/use-platform-api"; +import { useWorkspace } from "@/lib/hooks/use-workspace"; +import { Select, SelectItem } from "@heroui/react"; +import { useContext, useEffect, useState } from "react"; +import FileSystemExplorer from "../file-system/fs-explorer"; export default function WorkspaceExplorer() { - return ; + const editorContext = useContext(EditorContext); + + const workspaceHook = useWorkspace(); + const { platformApi } = usePlatformApi(); + + const [isWorkspaceSettingsModalOpen, setIsWorkspaceSettingsModalOpen] = + useState(false); + + const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false); + + useEffect(() => { + async function openProjectInWorkspace() { + if (isCreatingWorkspace && workspaceHook.workspace && platformApi) { + const homePath = editorContext?.persistSettings?.projectHomePath; + const projectName = editorContext?.editorStates.project; + if (!projectName) { + return; + } + + const uri = homePath + "/" + projectName; + const hasPath = await platformApi.hasPath(uri); + + if (!hasPath) { + await platformApi.createFolder(uri); + } + + await workspaceHook.refreshWorkspaceContent(platformApi); + setIsCreatingWorkspace(false); + } + } + + openProjectInWorkspace(); + }, [platformApi]); + + return ( +
+ {editorContext?.editorStates.project ? ( +
+
+ +
+ {workspaceHook.workspace ? ( + { + editorContext?.setEditorStates((prev) => ({ + ...prev, + isSideMenuOpen: false, + })); + }} + /> + ) : ( +
+

+ To browse files in workspace, please select a local or remote + workspace. +

+
+ )} +
+ ) : ( +
+

To open project in workspace, please select a project first.

+
+ )} + + {isWorkspaceSettingsModalOpen && ( + + )} +
+ ); } diff --git a/web/components/interface/navigation/nav-side-menu.tsx b/web/components/interface/navigation/nav-side-menu.tsx index d0cbe378..14ae98f3 100644 --- a/web/components/interface/navigation/nav-side-menu.tsx +++ b/web/components/interface/navigation/nav-side-menu.tsx @@ -1,9 +1,7 @@ import AppExplorer from "@/components/explorer/app/app-explorer"; -import FileSystemExplorer from "@/components/explorer/file-system/fs-explorer"; import ProjectExplorer from "@/components/explorer/project/project-explorer"; import WorkspaceExplorer from "@/components/explorer/workspace/workspace-explorer"; import Tabs from "@/components/misc/tabs"; -import ProjectSettingsModal from "@/components/modals/project-settings-modal"; import { EditorContext } from "@/components/providers/editor-context-provider"; import { SideMenuTabEnum } from "@/lib/enums"; import useExplorer from "@/lib/hooks/use-explorer"; @@ -12,7 +10,7 @@ import { isWeb } from "@/lib/platform-api/platform-checker"; import { TabItem } from "@/lib/types"; import { Button } from "@heroui/react"; import { AnimatePresence, motion } from "framer-motion"; -import { useContext, useState } from "react"; +import { useContext } from "react"; import Icon from "../../misc/icon"; export default function NavSideMenu({ @@ -26,7 +24,7 @@ export default function NavSideMenu({ {isMenuOpen && ( -
+
- +
@@ -102,18 +100,11 @@ function MenuPanel({ children }: { children?: React.ReactNode }) { ); } -function PanelContent({ - setIsMenuOpen, -}: { - setIsMenuOpen: (isOpen: boolean) => void; -}) { +function PanelContent() { const editorContext = useContext(EditorContext); const { selectAndSetProjectHome } = useExplorer(); - const [isProjectSettingsModalOpen, setIsProjectSettingsModalOpen] = - useState(false); - const tabItems: TabItem[] = [ { name: SideMenuTabEnum.Projects, @@ -126,7 +117,7 @@ function PanelContent({ icon: "apps", }, { - name: SideMenuTabEnum.Workspaces, + name: SideMenuTabEnum.Workspace, description: "Project workspace", icon: "folder", }, @@ -164,7 +155,7 @@ function PanelContent({ } return ( -
+
{selectedTab === SideMenuTabEnum.Apps ? ( - ) : selectedTab === SideMenuTabEnum.Workspaces ? ( - // + ) : selectedTab === SideMenuTabEnum.Workspace ? ( ) : ( - selectedTab === SideMenuTabEnum.Projects && - (editorContext?.editorStates.project ? ( - - ) : ( + selectedTab === SideMenuTabEnum.Projects && (
-

View Projects

-
- )) + ) )}
-
); } diff --git a/web/components/interface/navigation/nav-top-bar.tsx b/web/components/interface/navigation/nav-top-bar.tsx index 9c56b5f8..1544b227 100644 --- a/web/components/interface/navigation/nav-top-bar.tsx +++ b/web/components/interface/navigation/nav-top-bar.tsx @@ -1,16 +1,11 @@ -import { PlatformEnum } from "@/lib/enums"; import { useMenuActions } from "@/lib/hooks/menu-actions/use-menu-actions"; import { useAuth } from "@/lib/hooks/use-auth"; -import { useWorkspace } from "@/lib/hooks/use-workspace"; -import { getPlatform } from "@/lib/platform-api/platform-checker"; import { Button, Dropdown, DropdownItem, DropdownMenu, DropdownTrigger, - Select, - SelectItem, } from "@heroui/react"; import { useTheme } from "next-themes"; import { useSearchParams } from "next/navigation"; @@ -25,12 +20,10 @@ import ViewMenuDropDown from "./menu-dropdown/view-menu"; export default function NavTopBar({ isMenuOpen, setIsMenuOpen, - setIsWorkspaceSettingsModalOpen, setIsSharingOpen, }: { isMenuOpen: boolean; setIsMenuOpen: (isOpen: boolean) => void; - setIsWorkspaceSettingsModalOpen: (isOpen: boolean) => void; setIsSharingOpen: (isOpen: boolean) => void; }) { const editorContext = useContext(EditorContext); @@ -38,8 +31,6 @@ export default function NavTopBar({ const { session, signOut } = useAuth(); const { theme, setTheme } = useTheme(); - const workspaceHook = useWorkspace(); - // #region Load specified app if app query parameter is present const params = useSearchParams(); // Use the 'app' query parameter to load specific extension app upon loading page @@ -82,77 +73,6 @@ export default function NavTopBar({ - - {/* Do not show workspace selector when the app is open in web, and session is not available */} - {(getPlatform() === PlatformEnum.Web || - getPlatform() === PlatformEnum.WebMobile) && - !session ? null : ( - - )}
{editorContext?.editorStates.project && } diff --git a/web/components/interface/navigation/nav.tsx b/web/components/interface/navigation/nav.tsx index cf8a87a0..41639951 100644 --- a/web/components/interface/navigation/nav.tsx +++ b/web/components/interface/navigation/nav.tsx @@ -2,7 +2,6 @@ import { PlatformEnum } from "@/lib/enums"; import { useAuth } from "@/lib/hooks/use-auth"; -import { useWorkspace } from "@/lib/hooks/use-workspace"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { SafeArea } from "@capacitor-community/safe-area"; import { addToast, Button } from "@heroui/react"; @@ -13,7 +12,6 @@ import AppInfoModal from "../../modals/app-info-modal"; import LoginModal from "../../modals/login-modal"; import PasswordModal from "../../modals/password-modal"; import SharingModal from "../../modals/sharing-modal"; -import WorkspaceSettingsModal from "../../modals/workspace-settings-model"; import { EditorContext } from "../../providers/editor-context-provider"; import Loading from "../status-screens/loading"; import NavSideMenu from "./nav-side-menu"; @@ -27,12 +25,10 @@ export default function Nav({ children }: { children: React.ReactNode }) { const { setTheme, resolvedTheme } = useTheme(); const { session, isLoading: isLoadingSession, signIn } = useAuth(); - const workspaceHook = useWorkspace(); const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false); const [isShowNavbar, setIsShowNavbar] = useState(true); - const [isWorkspaceSettingsModalOpen, setIsWorkspaceSettingsModalOpen] = - useState(false); + const [isSharingOpen, setIsSharingOpen] = useState(false); const [isGranted, setIsGranted] = useState(true); @@ -155,14 +151,6 @@ export default function Nav({ children }: { children: React.ReactNode }) { )} - {isWorkspaceSettingsModalOpen && ( - - )} - {isSharingOpen && ( )} @@ -183,7 +171,6 @@ export default function Nav({ children }: { children: React.ReactNode }) { )} diff --git a/web/components/interface/project-indicator.tsx b/web/components/interface/project-indicator.tsx index 764d5d7f..e0679235 100644 --- a/web/components/interface/project-indicator.tsx +++ b/web/components/interface/project-indicator.tsx @@ -1,7 +1,6 @@ "use client"; -import { Key, useContext, useState } from "react"; -import { EditorContext } from "../providers/editor-context-provider"; +import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager"; import { Button, Dropdown, @@ -9,9 +8,10 @@ import { DropdownMenu, DropdownTrigger, } from "@heroui/react"; +import { Key, useContext, useState } from "react"; import Icon from "../misc/icon"; import ProjectSettingsModal from "../modals/project-settings-modal"; -import { useTabViewManager } from "@/lib/hooks/use-tab-view-manager"; +import { EditorContext } from "../providers/editor-context-provider"; export default function ProjectIndicator() { const editorContext = useContext(EditorContext); @@ -24,7 +24,7 @@ export default function ProjectIndicator() { return { ...prev, project: "", - projectContent: [], + workspaceContent: [], }; }); diff --git a/web/components/marketplace/app/app-gallery.tsx b/web/components/marketplace/app/app-gallery.tsx index 5f88d201..969be061 100644 --- a/web/components/marketplace/app/app-gallery.tsx +++ b/web/components/marketplace/app/app-gallery.tsx @@ -35,7 +35,7 @@ export default function AppGallery() { } = useSWR( selectLabels[selectedIndex]?.name === "All" || selectLabels[selectedIndex]?.name === "Published by Me" - ? `/api/extension/list${selectLabels[selectedIndex].name === "Published by Me" ? "?published=true" : ""}` + ? `/api/app/list${selectLabels[selectedIndex].name === "Published by Me" ? "?published=true" : ""}` : null, async (url: string) => { const res = await fetchAPI(url); @@ -107,7 +107,7 @@ export default function AppGallery() { }, extGroup[0]); return ( -
+
+
- {workspaceHook.workspace ? ( + {getPlatform() === PlatformEnum.Electron || + workspaceHook.workspace ? ( { editorContext?.setEditorStates((prev) => ({ @@ -130,15 +150,17 @@ export default function WorkspaceExplorer() { ) : (

- To browse files in workspace, please select a local or remote - workspace. + To browse files in workspace, please open in desktop client or + select remote workspace.

)}
) : (
-

To open project in workspace, please select a project first.

+

+ To view project content in workspace, please select a project first. +

)} diff --git a/web/components/interface/navigation/nav-side-menu.tsx b/web/components/interface/navigation/nav-side-menu.tsx index 14ae98f3..797e2487 100644 --- a/web/components/interface/navigation/nav-side-menu.tsx +++ b/web/components/interface/navigation/nav-side-menu.tsx @@ -3,10 +3,10 @@ import ProjectExplorer from "@/components/explorer/project/project-explorer"; import WorkspaceExplorer from "@/components/explorer/workspace/workspace-explorer"; import Tabs from "@/components/misc/tabs"; import { EditorContext } from "@/components/providers/editor-context-provider"; -import { SideMenuTabEnum } from "@/lib/enums"; +import { PlatformEnum, SideMenuTabEnum } from "@/lib/enums"; import useExplorer from "@/lib/hooks/use-explorer"; import { useScreenSize } from "@/lib/hooks/use-screen-size"; -import { isWeb } from "@/lib/platform-api/platform-checker"; +import { getPlatform } from "@/lib/platform-api/platform-checker"; import { TabItem } from "@/lib/types"; import { Button } from "@heroui/react"; import { AnimatePresence, motion } from "framer-motion"; @@ -135,7 +135,10 @@ function PanelContent() { } // Choose project home path - if (!isWeb() && !editorContext?.persistSettings?.projectHomePath) { + if ( + getPlatform() === PlatformEnum.Electron && + !editorContext?.persistSettings?.projectHomePath + ) { return (

diff --git a/web/components/interface/subscription/plan-picker.tsx b/web/components/interface/subscription/plan-picker.tsx new file mode 100644 index 00000000..34b84949 --- /dev/null +++ b/web/components/interface/subscription/plan-picker.tsx @@ -0,0 +1,17 @@ +import { Button } from "@heroui/react"; + +export default function PlanPicker() { + return ( +

+
+ Free: Use free plan and enjoy basic features. Access to self-hosted + workspaces and bring your own API keys. This option will remain + available forever and we are committed to keeping it free and open + source. + +
+
+ ); +} diff --git a/web/components/modals/open-source-info-modal.tsx b/web/components/modals/open-source-info-modal.tsx new file mode 100644 index 00000000..58ef2699 --- /dev/null +++ b/web/components/modals/open-source-info-modal.tsx @@ -0,0 +1,39 @@ +import { Button } from "@heroui/react"; +import Link from "next/link"; +import ModalWrapper from "./modal-wrapper"; +export default function OpenSourceInfoModal({ + isOpen, + setIsOpen, +}: { + isOpen: boolean; + setIsOpen: (isOpen: boolean) => void; +}) { + return ( + { + setIsOpen(open); + }} + title={"Open Source Information"} + > +
+ We believe in open software. Pulse Editor’s core and many of its + official apps are fully open source, built to empower developers + everywhere. +
+ While we offer hosted services like remote workspaces and AI inference + to make it easier to use at scale, we also believe in giving users full + control so that you can self-host or bring your own keys anytime. Check + out tutorials: + + + + Love what we are doing? Please consider ⭐ starring us on GitHub or + donating to support open source development. + + + +
+
+ ); +} diff --git a/web/lib/hooks/use-workspace.ts b/web/lib/hooks/use-workspace.ts index 09398373..f197f781 100644 --- a/web/lib/hooks/use-workspace.ts +++ b/web/lib/hooks/use-workspace.ts @@ -1,6 +1,5 @@ import { EditorContext } from "@/components/providers/editor-context-provider"; import { PlatformEnum } from "@/lib/enums"; -import { addToast } from "@heroui/react"; import { useContext } from "react"; import useSWR from "swr"; import { AbstractPlatformAPI } from "../platform-api/abstract-platform-api"; @@ -167,13 +166,6 @@ export function useWorkspace() { }); console.log("Found project content:", objects); - - // toast.success("Project content updated."); - addToast({ - title: "Project content updated.", - description: "The project content has been refreshed.", - color: "success", - }); } return { diff --git a/web/lib/platform-api/electron/electron-api.ts b/web/lib/platform-api/electron/electron-api.ts index 2912d0f6..55eb7d47 100644 --- a/web/lib/platform-api/electron/electron-api.ts +++ b/web/lib/platform-api/electron/electron-api.ts @@ -99,11 +99,11 @@ export class ElectronAPI extends AbstractPlatformAPI { } async setPersistentSettings(settings: PersistentSettings): Promise { - await this.electronAPI?.saveSettings(settings); + await this.electronAPI?.setPersistentSettings(settings); } async resetPersistentSettings(): Promise { - await this.electronAPI?.saveSettings({}); + await this.electronAPI?.setPersistentSettings({}); } async getInstallationPath(): Promise { diff --git a/web/lib/types.ts b/web/lib/types.ts index 9fbaf271..7faad206 100644 --- a/web/lib/types.ts +++ b/web/lib/types.ts @@ -317,15 +317,6 @@ export type IMCContextType = { // #endregion // #region Pulse Editor Cloud -export type RemoteWorkspace = { - id: string; - name: string; - cpuLimit: string; - memoryLimit: string; - volumeSize: string; - createdAt?: Date; -}; - export type Session = { user: { name: string; @@ -447,3 +438,16 @@ export type ProjectAsset = { uri: string; }; // #endregion + +// #region Workspace +export type RemoteWorkspace = { + id: string; + name: string; + cpuLimit: string; + memoryLimit: string; + volumeSize: string; + createdAt?: Date; +}; + + +// #endregion From 13a6be12e70b133cfa24334c3c81bf56d4349e61 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Mon, 27 Oct 2025 02:39:20 +0800 Subject: [PATCH 08/12] Remove capacitor storage plugin & add deep link --- capacitor-plugin/.eslintignore | 3 - capacitor-plugin/.gitignore | 70 ----- capacitor-plugin/.prettierignore | 1 - capacitor-plugin/CONTRIBUTING.md | 52 ---- capacitor-plugin/Package.swift | 28 -- .../PulseEditorCapacitorPlugin.podspec | 17 -- capacitor-plugin/README.md | 59 ---- capacitor-plugin/android/.gitignore | 1 - capacitor-plugin/android/build.gradle | 58 ---- capacitor-plugin/android/gradle.properties | 22 -- .../android/gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - capacitor-plugin/android/gradlew | 252 ------------------ capacitor-plugin/android/gradlew.bat | 94 ------- capacitor-plugin/android/proguard-rules.pro | 21 -- capacitor-plugin/android/settings.gradle | 2 - .../android/ExampleInstrumentedTest.java | 26 -- .../android/src/main/AndroidManifest.xml | 2 - .../plugin/PulseEditorCapacitor.java | 25 -- .../plugin/PulseEditorCapacitorPlugin.java | 50 ---- .../android/src/main/res/.gitkeep | 0 .../com/getcapacitor/ExampleUnitTest.java | 18 -- capacitor-plugin/ios/.gitignore | 8 - .../PulseEditorCapacitor.swift | 8 - .../PulseEditorCapacitorPlugin.swift | 23 -- .../PulseEditorCapacitorPluginTests.swift | 15 -- capacitor-plugin/package.json | 81 ------ capacitor-plugin/rollup.config.mjs | 22 -- capacitor-plugin/src/definitions.ts | 5 - capacitor-plugin/src/index.ts | 10 - capacitor-plugin/src/web.ts | 18 -- capacitor-plugin/tsconfig.json | 20 -- mobile/android/.gitignore | 2 +- mobile/android/app/capacitor.build.gradle | 1 - .../android/app/src/main/AndroidManifest.xml | 18 +- mobile/android/capacitor.settings.gradle | 3 - mobile/package.json | 3 +- package-lock.json | 11 +- package.json | 2 +- web/components/interface/navigation/nav.tsx | 52 ---- .../providers/capacitor-provider.tsx | 28 +- web/lib/hooks/use-auth.ts | 6 + web/package.json | 2 +- 43 files changed, 58 insertions(+), 1088 deletions(-) delete mode 100644 capacitor-plugin/.eslintignore delete mode 100644 capacitor-plugin/.gitignore delete mode 100644 capacitor-plugin/.prettierignore delete mode 100644 capacitor-plugin/CONTRIBUTING.md delete mode 100644 capacitor-plugin/Package.swift delete mode 100644 capacitor-plugin/PulseEditorCapacitorPlugin.podspec delete mode 100644 capacitor-plugin/README.md delete mode 100644 capacitor-plugin/android/.gitignore delete mode 100644 capacitor-plugin/android/build.gradle delete mode 100644 capacitor-plugin/android/gradle.properties delete mode 100644 capacitor-plugin/android/gradle/wrapper/gradle-wrapper.jar delete mode 100644 capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties delete mode 100644 capacitor-plugin/android/gradlew delete mode 100644 capacitor-plugin/android/gradlew.bat delete mode 100644 capacitor-plugin/android/proguard-rules.pro delete mode 100644 capacitor-plugin/android/settings.gradle delete mode 100644 capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java delete mode 100644 capacitor-plugin/android/src/main/AndroidManifest.xml delete mode 100644 capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java delete mode 100644 capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java delete mode 100644 capacitor-plugin/android/src/main/res/.gitkeep delete mode 100644 capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java delete mode 100644 capacitor-plugin/ios/.gitignore delete mode 100644 capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift delete mode 100644 capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift delete mode 100644 capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift delete mode 100644 capacitor-plugin/package.json delete mode 100644 capacitor-plugin/rollup.config.mjs delete mode 100644 capacitor-plugin/src/definitions.ts delete mode 100644 capacitor-plugin/src/index.ts delete mode 100644 capacitor-plugin/src/web.ts delete mode 100644 capacitor-plugin/tsconfig.json diff --git a/capacitor-plugin/.eslintignore b/capacitor-plugin/.eslintignore deleted file mode 100644 index 23f89447..00000000 --- a/capacitor-plugin/.eslintignore +++ /dev/null @@ -1,3 +0,0 @@ -build -dist -example-app diff --git a/capacitor-plugin/.gitignore b/capacitor-plugin/.gitignore deleted file mode 100644 index df9f0c20..00000000 --- a/capacitor-plugin/.gitignore +++ /dev/null @@ -1,70 +0,0 @@ -# node files -dist -node_modules - -# iOS files -Pods -Podfile.lock -Package.resolved -Build -xcuserdata -/.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc - - -# macOS files -.DS_Store - - - -# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore - -# Built application files -*.apk -*.ap_ - -# Files for the ART/Dalvik VM -*.dex - -# Java class files -*.class - -# Generated files -bin -gen -out - -# Gradle files -.gradle -build - -# Local configuration file (sdk path, etc) -local.properties - -# Proguard folder generated by Eclipse -proguard - -# Log Files -*.log - -# Android Studio Navigation editor temp files -.navigation - -# Android Studio captures folder -captures - -# IntelliJ -*.iml -.idea - -# Keystore files -# Uncomment the following line if you do not want to check your keystore files in. -#*.jks - -# External native build folder generated in Android Studio 2.2 and later -.externalNativeBuild diff --git a/capacitor-plugin/.prettierignore b/capacitor-plugin/.prettierignore deleted file mode 100644 index 5ab6e14d..00000000 --- a/capacitor-plugin/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -example-app diff --git a/capacitor-plugin/CONTRIBUTING.md b/capacitor-plugin/CONTRIBUTING.md deleted file mode 100644 index 3f875180..00000000 --- a/capacitor-plugin/CONTRIBUTING.md +++ /dev/null @@ -1,52 +0,0 @@ -# Contributing - -This guide provides instructions for contributing to this Capacitor plugin. - -## Developing - -### Local Setup - -1. Fork and clone the repo. -1. Install the dependencies. - - ```shell - npm install - ``` - -1. Install SwiftLint if you're on macOS. - - ```shell - brew install swiftlint - ``` - -### Scripts - -#### `npm run build` - -Build the plugin web assets and generate plugin API documentation using [`@capacitor/docgen`](https://github.com/ionic-team/capacitor-docgen). - -It will compile the TypeScript code from `src/` into ESM JavaScript in `dist/esm/`. These files are used in apps with bundlers when your plugin is imported. - -Then, Rollup will bundle the code into a single file at `dist/plugin.js`. This file is used in apps without bundlers by including it as a script in `index.html`. - -#### `npm run verify` - -Build and validate the web and native projects. - -This is useful to run in CI to verify that the plugin builds for all platforms. - -#### `npm run lint` / `npm run fmt` - -Check formatting and code quality, autoformat/autofix if possible. - -This template is integrated with ESLint, Prettier, and SwiftLint. Using these tools is completely optional, but the [Capacitor Community](https://github.com/capacitor-community/) strives to have consistent code style and structure for easier cooperation. - -## Publishing - -There is a `prepublishOnly` hook in `package.json` which prepares the plugin before publishing, so all you need to do is run: - -```shell -npm publish -``` - -> **Note**: The [`files`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#files) array in `package.json` specifies which files get published. If you rename files/directories or add files elsewhere, you may need to update it. diff --git a/capacitor-plugin/Package.swift b/capacitor-plugin/Package.swift deleted file mode 100644 index cabd9ee1..00000000 --- a/capacitor-plugin/Package.swift +++ /dev/null @@ -1,28 +0,0 @@ -// swift-tools-version: 5.9 -import PackageDescription - -let package = Package( - name: "PulseEditorCapacitorPlugin", - platforms: [.iOS(.v14)], - products: [ - .library( - name: "PulseEditorCapacitorPlugin", - targets: ["PulseEditorCapacitorPlugin"]) - ], - dependencies: [ - .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.0.0") - ], - targets: [ - .target( - name: "PulseEditorCapacitorPlugin", - dependencies: [ - .product(name: "Capacitor", package: "capacitor-swift-pm"), - .product(name: "Cordova", package: "capacitor-swift-pm") - ], - path: "ios/Sources/PulseEditorCapacitorPlugin"), - .testTarget( - name: "PulseEditorCapacitorPluginTests", - dependencies: ["PulseEditorCapacitorPlugin"], - path: "ios/Tests/PulseEditorCapacitorPluginTests") - ] -) \ No newline at end of file diff --git a/capacitor-plugin/PulseEditorCapacitorPlugin.podspec b/capacitor-plugin/PulseEditorCapacitorPlugin.podspec deleted file mode 100644 index db20bf67..00000000 --- a/capacitor-plugin/PulseEditorCapacitorPlugin.podspec +++ /dev/null @@ -1,17 +0,0 @@ -require 'json' - -package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) - -Pod::Spec.new do |s| - s.name = 'PulseEditorCapacitorPlugin' - s.version = package['version'] - s.summary = package['description'] - s.license = package['license'] - s.homepage = package['repository']['url'] - s.author = package['author'] - s.source = { :git => package['repository']['url'], :tag => s.version.to_s } - s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}' - s.ios.deployment_target = '14.0' - s.dependency 'Capacitor' - s.swift_version = '5.1' -end diff --git a/capacitor-plugin/README.md b/capacitor-plugin/README.md deleted file mode 100644 index d4c17e4d..00000000 --- a/capacitor-plugin/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# @pulse-editor/capacitor-plugin - -Capacitor plugins for Pulse Editor - -## Install - -```bash -npm install @pulse-editor/capacitor-plugin -npx cap sync -``` - -## API - - - -* [`echo(...)`](#echo) -* [`startManageStorageIntent()`](#startmanagestorageintent) -* [`isManageStoragePermissionGranted()`](#ismanagestoragepermissiongranted) - - - - - - -### echo(...) - -```typescript -echo(options: { value: string; }) => Promise<{ value: string; }> -``` - -| Param | Type | -| ------------- | ------------------------------- | -| **`options`** | { value: string; } | - -**Returns:** Promise<{ value: string; }> - --------------------- - - -### startManageStorageIntent() - -```typescript -startManageStorageIntent() => Promise -``` - --------------------- - - -### isManageStoragePermissionGranted() - -```typescript -isManageStoragePermissionGranted() => Promise<{ isGranted: boolean; }> -``` - -**Returns:** Promise<{ isGranted: boolean; }> - --------------------- - - diff --git a/capacitor-plugin/android/.gitignore b/capacitor-plugin/android/.gitignore deleted file mode 100644 index 796b96d1..00000000 --- a/capacitor-plugin/android/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/capacitor-plugin/android/build.gradle b/capacitor-plugin/android/build.gradle deleted file mode 100644 index 8bfe171f..00000000 --- a/capacitor-plugin/android/build.gradle +++ /dev/null @@ -1,58 +0,0 @@ -ext { - junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' - androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0' - androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1' - androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1' -} - -buildscript { - repositories { - google() - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:8.7.2' - } -} - -apply plugin: 'com.android.library' - -android { - namespace "com.pulse.capacitor.plugin" - compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35 - defaultConfig { - minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23 - targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - lintOptions { - abortOnError false - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_21 - targetCompatibility JavaVersion.VERSION_21 - } -} - -repositories { - google() - mavenCentral() -} - - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(':capacitor-android') - implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" - testImplementation "junit:junit:$junitVersion" - androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" - androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" -} diff --git a/capacitor-plugin/android/gradle.properties b/capacitor-plugin/android/gradle.properties deleted file mode 100644 index 2e87c52f..00000000 --- a/capacitor-plugin/android/gradle.properties +++ /dev/null @@ -1,22 +0,0 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true diff --git a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.jar b/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9530d66f5e68d973ea569d8e19de379189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X diff --git a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties b/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index c1d5e018..00000000 --- a/capacitor-plugin/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/capacitor-plugin/android/gradlew b/capacitor-plugin/android/gradlew deleted file mode 100644 index f5feea6d..00000000 --- a/capacitor-plugin/android/gradlew +++ /dev/null @@ -1,252 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/capacitor-plugin/android/gradlew.bat b/capacitor-plugin/android/gradlew.bat deleted file mode 100644 index 9d21a218..00000000 --- a/capacitor-plugin/android/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/capacitor-plugin/android/proguard-rules.pro b/capacitor-plugin/android/proguard-rules.pro deleted file mode 100644 index f1b42451..00000000 --- a/capacitor-plugin/android/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/capacitor-plugin/android/settings.gradle b/capacitor-plugin/android/settings.gradle deleted file mode 100644 index e558db63..00000000 --- a/capacitor-plugin/android/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../../node_modules/@capacitor/android/capacitor') \ No newline at end of file diff --git a/capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java b/capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java deleted file mode 100644 index 58020e16..00000000 --- a/capacitor-plugin/android/src/androidTest/java/com/getcapacitor/android/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.getcapacitor.android; - -import static org.junit.Assert.*; - -import android.content.Context; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - - assertEquals("com.getcapacitor.android", appContext.getPackageName()); - } -} diff --git a/capacitor-plugin/android/src/main/AndroidManifest.xml b/capacitor-plugin/android/src/main/AndroidManifest.xml deleted file mode 100644 index a2f47b60..00000000 --- a/capacitor-plugin/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java b/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java deleted file mode 100644 index 4f018709..00000000 --- a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitor.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.pulse.capacitor.plugin; - -import android.util.Log; -import android.content.Intent; -import android.app.Activity; -import android.net.Uri; -import android.provider.Settings; -import android.os.Environment; - -public class PulseEditorCapacitor { - - public String echo(String value) { - Log.i("Echo", value); - return value; - } - - public void startManageStorageIntent(Activity activity, String packageName) { - Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION); - activity.startActivity(intent); - } - - public boolean isManageStoragePermissionGranted() { - return Environment.isExternalStorageManager(); - } -} diff --git a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java b/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java deleted file mode 100644 index 192caa65..00000000 --- a/capacitor-plugin/android/src/main/java/com/pulse/capacitor/plugin/PulseEditorCapacitorPlugin.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.pulse.capacitor.plugin; - -import com.getcapacitor.JSObject; -import com.getcapacitor.Plugin; -import com.getcapacitor.PluginCall; -import com.getcapacitor.PluginMethod; -import com.getcapacitor.annotation.CapacitorPlugin; -import com.getcapacitor.annotation.Permission; -import android.app.Activity; - -@CapacitorPlugin( - name = "PulseEditorCapacitor", - permissions = { - @Permission( - alias = "storage", - strings = { - "Manifest.permission.MANAGE_EXTERNAL_STORAGE" - } - ) - } -) -public class PulseEditorCapacitorPlugin extends Plugin { - - private PulseEditorCapacitor implementation = new PulseEditorCapacitor(); - - @PluginMethod - public void echo(PluginCall call) { - String value = call.getString("value"); - - JSObject ret = new JSObject(); - ret.put("value", implementation.echo(value)); - call.resolve(ret); - } - - @PluginMethod - public void startManageStorageIntent(PluginCall call) { - Activity activity = getActivity(); - String packageName = activity.getPackageName(); - implementation.startManageStorageIntent(activity, packageName); - call.resolve(); - } - - @PluginMethod - public void isManageStoragePermissionGranted(PluginCall call) { - boolean isGranted = implementation.isManageStoragePermissionGranted(); - JSObject ret = new JSObject(); - ret.put("isGranted", isGranted); - call.resolve(ret); - } -} diff --git a/capacitor-plugin/android/src/main/res/.gitkeep b/capacitor-plugin/android/src/main/res/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java b/capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java deleted file mode 100644 index a0fed0cf..00000000 --- a/capacitor-plugin/android/src/test/java/com/getcapacitor/ExampleUnitTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.getcapacitor; - -import static org.junit.Assert.*; - -import org.junit.Test; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} diff --git a/capacitor-plugin/ios/.gitignore b/capacitor-plugin/ios/.gitignore deleted file mode 100644 index afb34f83..00000000 --- a/capacitor-plugin/ios/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -.DS_Store -.build -/Packages -xcuserdata/ -DerivedData/ -.swiftpm/configuration/registries.json -.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc \ No newline at end of file diff --git a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift b/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift deleted file mode 100644 index 8357a184..00000000 --- a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitor.swift +++ /dev/null @@ -1,8 +0,0 @@ -import Foundation - -@objc public class PulseEditorCapacitor: NSObject { - @objc public func echo(_ value: String) -> String { - print(value) - return value - } -} diff --git a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift b/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift deleted file mode 100644 index de2208a7..00000000 --- a/capacitor-plugin/ios/Sources/PulseEditorCapacitorPlugin/PulseEditorCapacitorPlugin.swift +++ /dev/null @@ -1,23 +0,0 @@ -import Foundation -import Capacitor - -/** - * Please read the Capacitor iOS Plugin Development Guide - * here: https://capacitorjs.com/docs/plugins/ios - */ -@objc(PulseEditorCapacitorPlugin) -public class PulseEditorCapacitorPlugin: CAPPlugin, CAPBridgedPlugin { - public let identifier = "PulseEditorCapacitorPlugin" - public let jsName = "PulseEditorCapacitor" - public let pluginMethods: [CAPPluginMethod] = [ - CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise) - ] - private let implementation = PulseEditorCapacitor() - - @objc func echo(_ call: CAPPluginCall) { - let value = call.getString("value") ?? "" - call.resolve([ - "value": implementation.echo(value) - ]) - } -} diff --git a/capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift b/capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift deleted file mode 100644 index 4b07cc5c..00000000 --- a/capacitor-plugin/ios/Tests/PulseEditorCapacitorPluginTests/PulseEditorCapacitorPluginTests.swift +++ /dev/null @@ -1,15 +0,0 @@ -import XCTest -@testable import PulseEditorCapacitorPlugin - -class PulseEditorCapacitorTests: XCTestCase { - func testEcho() { - // This is an example of a functional test case for a plugin. - // Use XCTAssert and related functions to verify your tests produce the correct results. - - let implementation = PulseEditorCapacitor() - let value = "Hello, World!" - let result = implementation.echo(value) - - XCTAssertEqual(value, result) - } -} diff --git a/capacitor-plugin/package.json b/capacitor-plugin/package.json deleted file mode 100644 index 7406e126..00000000 --- a/capacitor-plugin/package.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "name": "@pulse-editor/capacitor-plugin", - "version": "0.0.1", - "private": true, - "description": "Capacitor plugins for Pulse Editor", - "main": "dist/plugin.cjs.js", - "module": "dist/esm/index.js", - "types": "dist/esm/index.d.ts", - "unpkg": "dist/plugin.js", - "files": [ - "android/src/main/", - "android/build.gradle", - "dist/", - "ios/Sources", - "ios/Tests", - "Package.swift", - "PulseEditorCapacitorPlugin.podspec" - ], - "author": "ClayPulse", - "license": "MIT", - "repository": { - "type": "git", - "url": "git+https://github.com/ClayPulse/pulse-editor.git" - }, - "bugs": { - "url": "https://github.com/ClayPulse/pulse-editor/issues" - }, - "keywords": [ - "capacitor", - "plugin", - "native" - ], - "scripts": { - "verify": "npm run verify:ios && npm run verify:android && npm run verify:web", - "verify:ios": "xcodebuild -scheme PulseEditorCapacitorPlugin -destination generic/platform=iOS", - "verify:android": "cd android && gradlew clean build test && cd ..", - "verify:web": "npm run build", - "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint", - "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format", - "eslint": "eslint . --ext ts", - "prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java", - "swiftlint": "node-swiftlint", - "docgen": "docgen --api PulseEditorCapacitorPlugin --output-readme README.md --output-json dist/docs.json", - "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs", - "clean": "rimraf ./dist", - "watch": "tsc --watch", - "prepublishOnly": "npm run build" - }, - "devDependencies": { - "@capacitor/android": "^7.0.0", - "@capacitor/core": "^7.0.0", - "@capacitor/docgen": "^0.3.0", - "@capacitor/ios": "^7.0.0", - "@ionic/eslint-config": "^0.4.0", - "@ionic/prettier-config": "^4.0.0", - "@ionic/swiftlint-config": "^2.0.0", - "eslint": "^8.57.0", - "prettier": "^3.4.2", - "prettier-plugin-java": "^2.6.6", - "rimraf": "^6.0.1", - "rollup": "^4.30.1", - "swiftlint": "^2.0.0", - "typescript": "^5.8.3" - }, - "peerDependencies": { - "@capacitor/core": ">=7.0.0" - }, - "prettier": "@ionic/prettier-config", - "swiftlint": "@ionic/swiftlint-config", - "eslintConfig": { - "extends": "@ionic/eslint-config/recommended" - }, - "capacitor": { - "ios": { - "src": "ios" - }, - "android": { - "src": "android" - } - } -} diff --git a/capacitor-plugin/rollup.config.mjs b/capacitor-plugin/rollup.config.mjs deleted file mode 100644 index 8cc2a1a1..00000000 --- a/capacitor-plugin/rollup.config.mjs +++ /dev/null @@ -1,22 +0,0 @@ -export default { - input: 'dist/esm/index.js', - output: [ - { - file: 'dist/plugin.js', - format: 'iife', - name: 'capacitorPulseEditorCapacitor', - globals: { - '@capacitor/core': 'capacitorExports', - }, - sourcemap: true, - inlineDynamicImports: true, - }, - { - file: 'dist/plugin.cjs.js', - format: 'cjs', - sourcemap: true, - inlineDynamicImports: true, - }, - ], - external: ['@capacitor/core'], -}; diff --git a/capacitor-plugin/src/definitions.ts b/capacitor-plugin/src/definitions.ts deleted file mode 100644 index 0a3941f8..00000000 --- a/capacitor-plugin/src/definitions.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface PulseEditorCapacitorPlugin { - echo(options: { value: string }): Promise<{ value: string }>; - startManageStorageIntent(): Promise; - isManageStoragePermissionGranted(): Promise<{ isGranted: boolean }>; -} diff --git a/capacitor-plugin/src/index.ts b/capacitor-plugin/src/index.ts deleted file mode 100644 index 8ea4deb7..00000000 --- a/capacitor-plugin/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { registerPlugin } from '@capacitor/core'; - -import type { PulseEditorCapacitorPlugin } from './definitions'; - -const PulseEditorCapacitor = registerPlugin('PulseEditorCapacitor', { - web: () => import('./web').then((m) => new m.PulseEditorCapacitorWeb()), -}); - -export * from './definitions'; -export { PulseEditorCapacitor }; diff --git a/capacitor-plugin/src/web.ts b/capacitor-plugin/src/web.ts deleted file mode 100644 index 9b31b7ef..00000000 --- a/capacitor-plugin/src/web.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { WebPlugin } from '@capacitor/core'; - -import type { PulseEditorCapacitorPlugin } from './definitions'; - -export class PulseEditorCapacitorWeb extends WebPlugin implements PulseEditorCapacitorPlugin { - async echo(options: { value: string }): Promise<{ value: string }> { - console.log('ECHO', options); - return options; - } - - async startManageStorageIntent(): Promise { - throw new Error('Method not implemented.'); - } - - async isManageStoragePermissionGranted(): Promise<{ isGranted: boolean }> { - throw new Error('Method not implemented.'); - } -} diff --git a/capacitor-plugin/tsconfig.json b/capacitor-plugin/tsconfig.json deleted file mode 100644 index f2e88e6a..00000000 --- a/capacitor-plugin/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "allowUnreachableCode": false, - "declaration": true, - "esModuleInterop": true, - "inlineSources": true, - "lib": ["dom", "es2017"], - "module": "esnext", - "moduleResolution": "node", - "noFallthroughCasesInSwitch": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "outDir": "dist/esm", - "pretty": true, - "sourceMap": true, - "strict": true, - "target": "es2017" - }, - "files": ["src/index.ts"] -} diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore index 7d82a7ca..89399e18 100644 --- a/mobile/android/.gitignore +++ b/mobile/android/.gitignore @@ -56,7 +56,7 @@ captures/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks -#*.keystore +*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 6d702635..5d884b70 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -15,7 +15,6 @@ dependencies { implementation project(':capacitor-screen-orientation') implementation project(':capacitor-status-bar') implementation project(':capawesome-capacitor-file-picker') - implementation project(':pulse-editor-capacitor-plugin') } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 479d6703..c3b90406 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + @@ -22,6 +22,14 @@ + + + + + + + - + @@ -43,7 +52,4 @@ - - - + \ No newline at end of file diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index 99cbf8fb..6ac1fd0e 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -19,6 +19,3 @@ project(':capacitor-status-bar').projectDir = new File('../../node_modules/@capa include ':capawesome-capacitor-file-picker' project(':capawesome-capacitor-file-picker').projectDir = new File('../../node_modules/@capawesome/capacitor-file-picker/android') - -include ':pulse-editor-capacitor-plugin' -project(':pulse-editor-capacitor-plugin').projectDir = new File('../../capacitor-plugin/android') diff --git a/mobile/package.json b/mobile/package.json index b32ff734..36764a4c 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "android-dev": "node dev.js", - "android-build": "npx cap run android" + "android-run": "npx cap run android", + "android-build": "npx cap build android" }, "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", diff --git a/package-lock.json b/package-lock.json index 91cd85a4..25aa9b30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1830,6 +1830,15 @@ "@capacitor/core": "^7.4.0" } }, + "node_modules/@capacitor/app": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@capacitor/app/-/app-7.1.0.tgz", + "integrity": "sha512-W7m09IWrUjZbo7AKeq+rc/KyucxrJekTBg0l4QCm/yDtCejE3hebxp/W2esU26KKCzMc7H3ClkUw32E9lZkwRA==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.4.3.tgz", @@ -26526,6 +26535,7 @@ "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", + "@capacitor/app": "^7.1.0", "@capacitor/cli": "^7.4.3", "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", @@ -26539,7 +26549,6 @@ "@langchain/community": "^0.3.49", "@langchain/core": "^0.3.66", "@langchain/openai": "^0.6.3", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin", "@pulse-editor/shared-utils": "^0.1.1-beta.55", "@ricky0123/vad-web": "^0.0.28", "@vercel/analytics": "^1.5.0", diff --git a/package.json b/package.json index caaa7494..81640d67 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "desktop-dev": "npm --prefix ./desktop run dev-https", "desktop-build": "npm run web-build && npm --prefix ./desktop run build", "android-dev": "npm run web-build && npm run android-dev --workspace=mobile", - "android-build": "npm run web-build && npm run android-build --workspace=mobile", + "android-run": "npm run web-build && npm run android-run --workspace=mobile", "react-api-build": "npm run build --workspace=npm-packages/react-api", "shared-utils-build": "npm run build --workspace=npm-packages/shared-utils", "cli-dev": "npm run dev --workspace=cli", diff --git a/web/components/interface/navigation/nav.tsx b/web/components/interface/navigation/nav.tsx index 41639951..f65faefb 100644 --- a/web/components/interface/navigation/nav.tsx +++ b/web/components/interface/navigation/nav.tsx @@ -4,8 +4,6 @@ import { PlatformEnum } from "@/lib/enums"; import { useAuth } from "@/lib/hooks/use-auth"; import { getPlatform } from "@/lib/platform-api/platform-checker"; import { SafeArea } from "@capacitor-community/safe-area"; -import { addToast, Button } from "@heroui/react"; -import { PulseEditorCapacitor } from "@pulse-editor/capacitor-plugin"; import { useTheme } from "next-themes"; import { useContext, useEffect, useState } from "react"; import AppInfoModal from "../../modals/app-info-modal"; @@ -31,56 +29,6 @@ export default function Nav({ children }: { children: React.ReactNode }) { const [isSharingOpen, setIsSharingOpen] = useState(false); - const [isGranted, setIsGranted] = useState(true); - - useEffect(() => { - async function checkStoragePermission() { - const { isGranted }: { isGranted: boolean } = - await PulseEditorCapacitor.isManageStoragePermissionGranted(); - setIsGranted(isGranted); - } - - if (getPlatform() === PlatformEnum.Capacitor) { - checkStoragePermission(); - } - }, []); - - useEffect(() => { - if (!isGranted) { - addToast({ - title: "Storage Permission Required", - classNames: { - base: "flex flex-col items-start", - }, - description: - "To use local storage management feature, please grant storage permission in settings. ", - icon: "warning", - color: "warning", - size: "lg", - shouldShowTimeoutProgress: true, - timeout: 30000, - endContent: ( -
- - -
- ), - }); - } - }, [isGranted]); - useEffect(() => { const platform = getPlatform(); // Hide NavMenu if opened in VSCode Extension, diff --git a/web/components/providers/capacitor-provider.tsx b/web/components/providers/capacitor-provider.tsx index fc70ecb0..0d0a6596 100644 --- a/web/components/providers/capacitor-provider.tsx +++ b/web/components/providers/capacitor-provider.tsx @@ -1,15 +1,20 @@ "use client"; -import { useEffect } from "react"; -import { StatusBar } from "@capacitor/status-bar"; -import { ScreenOrientation } from "@capacitor/screen-orientation"; +import { App, URLOpenListenerEvent } from "@capacitor/app"; import { Capacitor } from "@capacitor/core"; +import { ScreenOrientation } from "@capacitor/screen-orientation"; +import { StatusBar } from "@capacitor/status-bar"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; export default function CapacitorProvider({ children, }: { children: React.ReactNode; }) { + const router = useRouter(); + + // Set status bar based on orientation useEffect(() => { async function setStatusBar() { const orientation = await ScreenOrientation.orientation(); @@ -31,5 +36,22 @@ export default function CapacitorProvider({ ScreenOrientation.addListener("screenOrientationChange", setStatusBar); } }, []); + + // Set deep linking listener + useEffect(() => { + App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => { + // Check if the url has our scheme "https://mobile.pulse-editor.com" + if (event.url.startsWith("https://pulse-editor.com/mobile")) { + const slug = event.url.replace("https://pulse-editor.com/mobile", ""); + // Navigate to the slug + if (slug) { + router.push(slug); + } + } + // If no match, do nothing - let regular routing + // logic take over + }); + }, []); + return
{children}
; } diff --git a/web/lib/hooks/use-auth.ts b/web/lib/hooks/use-auth.ts index cc56e96a..9d4e2d92 100644 --- a/web/lib/hooks/use-auth.ts +++ b/web/lib/hooks/use-auth.ts @@ -64,6 +64,12 @@ export function useAuth() { // TODO: move this to the platform API layer // @ts-expect-error window.electronAPI is exposed by the Electron main process window.electronAPI.login(); + } else if (getPlatform() === PlatformEnum.Capacitor) { + // In Capacitor, open the sign-in page in the system browser. + const url = getAPIUrl(`/api/auth/signin`); + // Set the callback URL to the deeplink URL that Capacitor can handle. + url.searchParams.set("callbackUrl", window.location.href); + window.location.href = url.toString(); } else { const url = getAPIUrl(`/api/auth/signin`); url.searchParams.set("callbackUrl", window.location.href); diff --git a/web/package.json b/web/package.json index 5bdc18f7..6858615c 100644 --- a/web/package.json +++ b/web/package.json @@ -13,6 +13,7 @@ "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", + "@capacitor/app": "^7.1.0", "@capacitor/cli": "^7.4.3", "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", @@ -26,7 +27,6 @@ "@langchain/community": "^0.3.49", "@langchain/core": "^0.3.66", "@langchain/openai": "^0.6.3", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin", "@pulse-editor/shared-utils": "^0.1.1-beta.55", "@ricky0123/vad-web": "^0.0.28", "@vercel/analytics": "^1.5.0", From 20380e45eedc01f078f4c675c9e53e27517408a2 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:02:30 +0800 Subject: [PATCH 09/12] Update capacitor workflow to use signing keystore --- .github/workflows/build-mobile.yml | 4 ++-- .github/workflows/release-mobile.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index 46cd344d..a4e063af 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -42,7 +42,7 @@ jobs: - name: Get Keystore run: | mkdir ~/.keystore - echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/keystore.jks + echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/pulse-editor.keystore - name: Setup Node.js uses: actions/setup-node@v4 @@ -78,5 +78,5 @@ jobs: working-directory: mobile - name: Build Capacitor App - run: npx cap build android --keystorepath ~/.keystore/keystore.jks --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK + run: npx cap build android --keystorepath ~/.keystore/pulse-editor.keystore --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK working-directory: mobile diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index 00823cf4..d330e806 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -39,7 +39,7 @@ jobs: - name: Get Keystore run: | mkdir ~/.keystore - echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/keystore.jks + echo ${{ secrets.ANDROID_KEYSTORE }} | base64 --decode > ~/.keystore/pulse-editor.keystore - name: Setup Node.js uses: actions/setup-node@v4 @@ -75,7 +75,7 @@ jobs: working-directory: mobile - name: Build Capacitor App - run: npx cap build android --keystorepath ~/.keystore/keystore.jks --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK + run: npx cap build android --keystorepath ~/.keystore/pulse-editor.keystore --keystorepass ${{ secrets.ANDROID_KEYSTORE_PASS }} --androidreleasetype APK working-directory: mobile - name: Move APK From 65bbca94592fe0916bca37c184ba5557e910f6c3 Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Tue, 28 Oct 2025 00:01:53 +0800 Subject: [PATCH 10/12] Add capacitorjs auth --- mobile/android/app/build.gradle | 2 +- mobile/android/app/capacitor.build.gradle | 2 + .../android/app/src/main/AndroidManifest.xml | 10 +- .../main/res/xml/network_security_config.xml | 11 +- mobile/android/capacitor.settings.gradle | 6 + mobile/package.json | 5 +- package-lock.json | 280 +----------------- web/README.md | 66 ++++- web/app/(main-layout)/layout.tsx | 32 +- .../interface/navigation/nav-top-bar.tsx | 32 +- .../providers/capacitor-provider.tsx | 51 +++- web/lib/hooks/use-auth.ts | 43 ++- web/lib/types.ts | 3 +- web/package.json | 1 + 14 files changed, 218 insertions(+), 326 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 9ae01b72..73b79861 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -8,7 +8,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "v0.0.1-alpha" + versionName "v0.1.1-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 5d884b70..554981fe 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -10,6 +10,8 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { implementation project(':capacitor-community-safe-area') + implementation project(':capacitor-app') + implementation project(':capacitor-browser') implementation project(':capacitor-filesystem') implementation project(':capacitor-keyboard') implementation project(':capacitor-screen-orientation') diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index c3b90406..c60fa1c4 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -10,7 +10,7 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme" - android:usesCleartextTraffic="true"> + android:networkSecurityConfig="@xml/network_security_config"> + + + + + + + + - - localhost - - + + + + + + + \ No newline at end of file diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index 6ac1fd0e..582ac845 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -5,6 +5,12 @@ project(':capacitor-android').projectDir = new File('../../node_modules/@capacit include ':capacitor-community-safe-area' project(':capacitor-community-safe-area').projectDir = new File('../node_modules/@capacitor-community/safe-area/android') +include ':capacitor-app' +project(':capacitor-app').projectDir = new File('../../node_modules/@capacitor/app/android') + +include ':capacitor-browser' +project(':capacitor-browser').projectDir = new File('../../node_modules/@capacitor/browser/android') + include ':capacitor-filesystem' project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capacitor/filesystem/android') diff --git a/mobile/package.json b/mobile/package.json index 36764a4c..5d052b7b 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -11,13 +11,14 @@ "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.2.0", + "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.2.0", "@capacitor/core": "^7.2.0", "@capacitor/filesystem": "^7.0.1", "@capacitor/keyboard": "^7.0.1", "@capacitor/screen-orientation": "^7.0.1", "@capacitor/status-bar": "^7.0.1", - "@capawesome/capacitor-file-picker": "^7.0.1", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin" + "@capawesome/capacitor-file-picker": "^7.0.1" } } diff --git a/package-lock.json b/package-lock.json index 25aa9b30..fbac190a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "capacitor-plugin": { "name": "@pulse-editor/capacitor-plugin", "version": "0.0.1", + "extraneous": true, "license": "MIT", "devDependencies": { "@capacitor/android": "^7.0.0", @@ -46,90 +47,21 @@ "@capacitor/core": ">=7.0.0" } }, - "capacitor-plugin/node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "capacitor-plugin/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "capacitor-plugin/node_modules/prettier-plugin-java": { - "version": "2.6.7", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "java-parser": "2.3.3", - "lodash": "4.17.21" - }, - "peerDependencies": { - "prettier": "^3.0.0" - } - }, - "capacitor-plugin/node_modules/rimraf": { - "version": "6.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^11.0.0", - "package-json-from-dist": "^1.0.0" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "mobile": { "name": "@pulse-editor/mobile", "version": "0.0.1", "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.2.0", + "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.2.0", "@capacitor/core": "^7.2.0", "@capacitor/filesystem": "^7.0.1", "@capacitor/keyboard": "^7.0.1", "@capacitor/screen-orientation": "^7.0.1", "@capacitor/status-bar": "^7.0.1", - "@capawesome/capacitor-file-picker": "^7.0.1", - "@pulse-editor/capacitor-plugin": "file:../capacitor-plugin" + "@capawesome/capacitor-file-picker": "^7.0.1" } }, "mobile/node_modules/@capacitor-community/safe-area": { @@ -1839,6 +1771,15 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/browser": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/browser/-/browser-7.0.2.tgz", + "integrity": "sha512-5kySTunCtH+2sezmTjgDfwvspW7GW/hslQECZeLIRM2qefnxjGTc3fmCTeILYK5EuvcxMs+8sF5BhmzzKqOzuQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/cli": { "version": "7.4.3", "resolved": "https://registry.npmjs.org/@capacitor/cli/-/cli-7.4.3.tgz", @@ -1972,47 +1913,6 @@ "tslib": "^2.1.0" } }, - "node_modules/@capacitor/docgen": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@capacitor/docgen/-/docgen-0.3.0.tgz", - "integrity": "sha512-WPggobo5Ql70F+2xOIUwNSApJXaL9F/9+Al6B+sNuSAmcg484OAksyUPKgiynF4BVlxeY5a0sDkgdVkmmA3ElQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "^14.18.0", - "colorette": "^2.0.20", - "github-slugger": "^1.5.0", - "minimist": "^1.2.8", - "typescript": "~4.2.4" - }, - "bin": { - "docgen": "bin/docgen" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@capacitor/docgen/node_modules/@types/node": { - "version": "14.18.63", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", - "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@capacitor/docgen/node_modules/typescript": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", - "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/@capacitor/filesystem": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/@capacitor/filesystem/-/filesystem-7.1.4.tgz", @@ -2025,16 +1925,6 @@ "@capacitor/core": ">=7.0.0" } }, - "node_modules/@capacitor/ios": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.4.3.tgz", - "integrity": "sha512-VNm7cHODgh3KK/4ZC2rXU9gBlvHii/mYFLI+XMXwq24nhB679QxHhz+pUuI7PatYoM2q4MAL0NR/dRgehKCaSA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@capacitor/core": "^7.4.0" - } - }, "node_modules/@capacitor/keyboard": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@capacitor/keyboard/-/keyboard-7.0.3.tgz", @@ -4773,39 +4663,6 @@ "node": ">=16.0.0" } }, - "node_modules/@ionic/eslint-config": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@ionic/eslint-config/-/eslint-config-0.4.0.tgz", - "integrity": "sha512-L8OXY29D3iGqNtteFj0iz3eoZIVgokBiVjCO8WMssNZa4GTHjYsase0rC9ASXGefMnLJu6rbNl3Gbx7NNxJRZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "^5.58.0", - "@typescript-eslint/parser": "^5.58.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.0" - }, - "peerDependencies": { - "eslint": ">=7" - } - }, - "node_modules/@ionic/prettier-config": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@ionic/prettier-config/-/prettier-config-4.0.0.tgz", - "integrity": "sha512-0DqL6CggVdgeJAWOLPUT73rF1VD5p0tVlCpC5GXz5vTIUBxNwsJ5085Q7wXjKiE5Odx3aOHGTcuRWCawFsLFag==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "prettier": "^2.4.0 || ^3.0.0" - } - }, - "node_modules/@ionic/swiftlint-config": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@ionic/swiftlint-config/-/swiftlint-config-2.0.0.tgz", - "integrity": "sha512-TXy76ALSKhUZzBziHz7aoEtSQwHofBIDRNzM9x4sndtC7fefbZsBw3UgGwXFTOc7hoj72EAGyqZNUhj9LlhaNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@ionic/utils-array": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@ionic/utils-array/-/utils-array-2.1.6.tgz", @@ -6299,10 +6156,6 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", "license": "BSD-3-Clause" }, - "node_modules/@pulse-editor/capacitor-plugin": { - "resolved": "capacitor-plugin", - "link": true - }, "node_modules/@pulse-editor/mobile": { "resolved": "mobile", "link": true @@ -11322,33 +11175,6 @@ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "license": "MIT" }, - "node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/cross-fetch": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", @@ -11955,23 +11781,6 @@ "node": ">=4" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/error-ex/node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true, - "license": "MIT" - }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -12294,19 +12103,6 @@ } } }, - "node_modules/eslint-config-prettier": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", - "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, "node_modules/eslint-import-resolver-node": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", @@ -13360,13 +13156,6 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "dev": true, - "license": "ISC" - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -15632,13 +15421,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true, - "license": "MIT" - }, "node_modules/loader-runner": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", @@ -20677,25 +20459,6 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -23170,22 +22933,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/swiftlint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/swiftlint/-/swiftlint-2.0.0.tgz", - "integrity": "sha512-MMVuyZ4/6WcIJlk0z6GM0pZjRuwnyUJqRPbJBFW3oACN/qjAvRbolCWEu+zE2MycF/cEgqfUpI+oLECNfjfOJA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "@ionic/utils-fs": "^3.1.7", - "@ionic/utils-subprocess": "^3.0.1", - "cosmiconfig": "^9.0.0" - }, - "bin": { - "node-swiftlint": "bin.js" - } - }, "node_modules/swr": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", @@ -26536,6 +26283,7 @@ "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.4.3", "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", diff --git a/web/README.md b/web/README.md index d788fa9b..ad78fd69 100644 --- a/web/README.md +++ b/web/README.md @@ -1,12 +1,66 @@ +# Generate certificates for local dev + +## Do the below in `web/certificates` folder + +1. Generate a local CA. + Use devCA.key to install on device later. + +```bash +openssl genrsa -out devCA.key 2048 +openssl req -x509 -new -nodes -key devCA.key -sha256 -days 3650 -out devCA.crt -subj "/CN=Local Development CA" +``` + +2. Create a .cnf + e.g. + +```bash +[req] +default_bits = 2048 +prompt = no +default_md = sha256 +distinguished_name = dn +req_extensions = req_ext + +[dn] +CN = 192.168.1.100 + +[req_ext] +subjectAltName = @alt_names + +[alt_names] +IP.1 = 192.168.1.100 +DNS.1 = mypc +DNS.2 = mypc.local +DNS.3 = localhost +``` + +3. Generate private key + +```bash +openssl genrsa -out localhost-key.pem 2048 +openssl req -new -key localhost-key.pem -out localhost.csr -config localhost.cnf +``` + +4. Sign the certificate with CA + +```bash +openssl x509 -req -in localhost.csr -CA devCA.crt -CAkey devCA.key -CAcreateserial -out localhost.pem -days 365 -sha256 -extensions req_ext -extfile localhost.cnf +``` + +5. Install `devCA.crt` on Android. + First, copy `devCA.crt` from PC to you android device. On Android, go to "settings -> security and privacy -> more security settings -> install from device storage -> CA certificate", then locate `devCA.crt` and install. + # Web Client User Guide + ### Installation -You must then configure settings in the app. Specifically, to use Voice Chat, you need to have all STT, LLM, TTS configured; to only use Agentic Chat Terminal or Code Completion, you need to configure LLM. + +You must then configure settings in the app. Specifically, to use Voice Chat, you need to have all STT, LLM, TTS configured; to only use Agentic Chat Terminal or Code Completion, you need to configure LLM. | Modality | Supported Provider | -| --- | --- | -| STT | OpenAI | -| LLM | OpenAI | -| TTS | OpenAI, ElevenLabs | +| -------- | ------------------ | +| STT | OpenAI | +| LLM | OpenAI | +| TTS | OpenAI, ElevenLabs | (For TTS, you need to enter a voice name or voice ID which you can find from your provider. e.g. “alloy” for OpenAI TTS1, “Maltida” for ElevenLabs.) @@ -34,4 +88,4 @@ Click the “Open Chat View” icon in the bottom toolbar. Then select your desi (Make sure you have configured LLM provider and API key) -Type anything in an open file, then a suggestion would become available in grey text. Press tab key to accept changes, or keep typing to refresh new suggest. \ No newline at end of file +Type anything in an open file, then a suggestion would become available in grey text. Press tab key to accept changes, or keep typing to refresh new suggest. diff --git a/web/app/(main-layout)/layout.tsx b/web/app/(main-layout)/layout.tsx index 48165913..f0c5e57f 100644 --- a/web/app/(main-layout)/layout.tsx +++ b/web/app/(main-layout)/layout.tsx @@ -1,16 +1,16 @@ -import type { Metadata } from "next"; -import "./globals.css"; -import WrappedHeroUIProvider from "@/components/providers/wrapped-hero-ui-provider"; -import EditorContextProvider from "@/components/providers/editor-context-provider"; -import { Toaster } from "react-hot-toast"; -import "material-icons/iconfont/material-icons.css"; +import Nav from "@/components/interface/navigation/nav"; import CapacitorProvider from "@/components/providers/capacitor-provider"; -import RemoteModuleProvider from "@/components/providers/remote-module-provider"; +import EditorContextProvider from "@/components/providers/editor-context-provider"; import InterModuleCommunicationProvider from "@/components/providers/imc-provider"; -import Nav from "@/components/interface/navigation/nav"; -import { Suspense } from "react"; -import { Analytics } from "@vercel/analytics/next"; import PlatformAssistantProvider from "@/components/providers/platform-assistant-provider"; +import RemoteModuleProvider from "@/components/providers/remote-module-provider"; +import WrappedHeroUIProvider from "@/components/providers/wrapped-hero-ui-provider"; +import { Analytics } from "@vercel/analytics/next"; +import "material-icons/iconfont/material-icons.css"; +import type { Metadata } from "next"; +import { Suspense } from "react"; +import { Toaster } from "react-hot-toast"; +import "./globals.css"; export const metadata: Metadata = { title: "Pulse Editor", @@ -27,9 +27,9 @@ export default function RootLayout({ - - - + + + @@ -40,9 +40,9 @@ export default function RootLayout({ - - - + + + diff --git a/web/components/interface/navigation/nav-top-bar.tsx b/web/components/interface/navigation/nav-top-bar.tsx index 1544b227..6a0d9430 100644 --- a/web/components/interface/navigation/nav-top-bar.tsx +++ b/web/components/interface/navigation/nav-top-bar.tsx @@ -29,7 +29,7 @@ export default function NavTopBar({ const editorContext = useContext(EditorContext); const { session, signOut } = useAuth(); - const { theme, setTheme } = useTheme(); + const { resolvedTheme, setTheme } = useTheme(); // #region Load specified app if app query parameter is present const params = useSearchParams(); @@ -99,6 +99,21 @@ export default function NavTopBar({ + {!session && ( )} - {session && ( diff --git a/web/components/providers/capacitor-provider.tsx b/web/components/providers/capacitor-provider.tsx index 0d0a6596..a250c004 100644 --- a/web/components/providers/capacitor-provider.tsx +++ b/web/components/providers/capacitor-provider.tsx @@ -1,18 +1,18 @@ "use client"; import { App, URLOpenListenerEvent } from "@capacitor/app"; -import { Capacitor } from "@capacitor/core"; +import { Capacitor, CapacitorCookies } from "@capacitor/core"; import { ScreenOrientation } from "@capacitor/screen-orientation"; import { StatusBar } from "@capacitor/status-bar"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { useContext, useEffect } from "react"; +import { EditorContext } from "./editor-context-provider"; export default function CapacitorProvider({ children, }: { children: React.ReactNode; }) { - const router = useRouter(); + const editorContext = useContext(EditorContext); // Set status bar based on orientation useEffect(() => { @@ -40,14 +40,47 @@ export default function CapacitorProvider({ // Set deep linking listener useEffect(() => { App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => { - // Check if the url has our scheme "https://mobile.pulse-editor.com" - if (event.url.startsWith("https://pulse-editor.com/mobile")) { - const slug = event.url.replace("https://pulse-editor.com/mobile", ""); + /* + Custom Scheme + */ + // check if the url has our custom scheme "pulse-editor://open" + if (event.url.startsWith("pulse-editor://open")) { + const params = new URLSearchParams( + event.url.replace("pulse-editor://open?", ""), + ); + const token = params.get("token"); + const exp = params.get("exp"); + // Navigate to the slug - if (slug) { - router.push(slug); + if (token) { + // Set token in cookie + CapacitorCookies.setCookie({ + url: "https://192.168.2.103:3000", // must match your WebView origin + key: "pulse-editor.session-token", + value: token, + path: "/", + expires: exp ?? undefined, + }).then(() => { + editorContext?.setEditorStates((prev) => ({ + ...prev, + isRefreshSession: true, + isSigningIn: false, + })); + }); } } + /* + Google Verified Links + Check if the url has our scheme "https://mobile.pulse-editor.com" + */ + // else if (event.url.startsWith("https://pulse-editor.com/mobile")) { + // const slug = event.url.replace("https://pulse-editor.com/mobile", ""); + // // Navigate to the slug + // if (slug) { + // router.push(slug); + // } + // } + // If no match, do nothing - let regular routing // logic take over }); diff --git a/web/lib/hooks/use-auth.ts b/web/lib/hooks/use-auth.ts index 9d4e2d92..e96dcf87 100644 --- a/web/lib/hooks/use-auth.ts +++ b/web/lib/hooks/use-auth.ts @@ -1,7 +1,9 @@ "use client"; import { EditorContext } from "@/components/providers/editor-context-provider"; -import { useContext } from "react"; +import { Browser } from "@capacitor/browser"; +import { CapacitorCookies } from "@capacitor/core"; +import { useContext, useEffect } from "react"; import useSWR from "swr"; import { PlatformEnum } from "../enums"; import { getPlatform } from "../platform-api/platform-checker"; @@ -11,7 +13,11 @@ import { CreditBalance, Session, Subscription } from "../types"; export function useAuth() { const editorContext = useContext(EditorContext); // --- Auth --- - const { data: session, isLoading } = useSWR( + const { + data: session, + isLoading, + mutate, + } = useSWR( !editorContext?.editorStates.isSigningIn ? `/api/auth/session` : null, async (url: string) => { const res = await fetchAPI(url); @@ -53,6 +59,17 @@ export function useAuth() { }, ); + useEffect(() => { + if (editorContext?.editorStates.isRefreshSession) { + mutate().then(() => { + editorContext.setEditorStates((prev) => ({ + ...prev, + isRefreshSession: false, + })); + }); + } + }, [editorContext?.editorStates.isRefreshSession]); + // Open a sign-in page if the user is not signed in. async function signIn() { if (session) { @@ -68,8 +85,12 @@ export function useAuth() { // In Capacitor, open the sign-in page in the system browser. const url = getAPIUrl(`/api/auth/signin`); // Set the callback URL to the deeplink URL that Capacitor can handle. - url.searchParams.set("callbackUrl", window.location.href); - window.location.href = url.toString(); + url.searchParams.set( + "callbackUrl", + process.env.NEXT_PUBLIC_BACKEND_URL + "/api/mobile", + ); + + await Browser.open({ url: url.toString() }); } else { const url = getAPIUrl(`/api/auth/signin`); url.searchParams.set("callbackUrl", window.location.href); @@ -88,6 +109,20 @@ export function useAuth() { // TODO: move this to the platform API layer // @ts-expect-error window.electronAPI is exposed by the Electron main process window.electronAPI.logout(); + } else if (getPlatform() === PlatformEnum.Capacitor) { + // In Capacitor, open the sign-out page in the system browser. + const url = getAPIUrl(`/api/auth/signout`); + // Set the callback URL to the deeplink URL that Capacitor can handle. + url.searchParams.set( + "callbackUrl", + process.env.NEXT_PUBLIC_BACKEND_URL + "/api/mobile", + ); + await Browser.open({ url: url.toString() }); + + await CapacitorCookies.deleteCookie({ + url: "https://192.168.2.103:3000", + key: "pulse-editor.session-token", + }); } else { const url = getAPIUrl(`/api/auth/signout`); url.searchParams.set("callbackUrl", window.location.href); diff --git a/web/lib/types.ts b/web/lib/types.ts index 7faad206..f10599f5 100644 --- a/web/lib/types.ts +++ b/web/lib/types.ts @@ -70,7 +70,9 @@ export type EditorStates = { currentWorkspace?: RemoteWorkspace; workspaceContent?: FileSystemObject[]; + /* Auth */ isSigningIn?: boolean; + isRefreshSession?: boolean; /* Modals */ isAppInfoModalOpen?: boolean; @@ -449,5 +451,4 @@ export type RemoteWorkspace = { createdAt?: Date; }; - // #endregion diff --git a/web/package.json b/web/package.json index 6858615c..3c625f4f 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,7 @@ "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", "@capacitor/app": "^7.1.0", + "@capacitor/browser": "^7.0.2", "@capacitor/cli": "^7.4.3", "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", From 198fddcec7f2e7707e8e67f0ca9dd20ae1732b7e Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Tue, 28 Oct 2025 18:44:41 +0800 Subject: [PATCH 11/12] Remove capacitor plugin from CI/CD --- .changeset/pre.json | 1 - .github/workflows/build-desktop.yml | 8 +------- .github/workflows/build-mobile.yml | 6 ------ .github/workflows/build-web.yml | 6 ------ .github/workflows/release-desktop.yml | 6 ------ .github/workflows/release-mobile.yml | 6 ------ package-lock.json | 1 - package.json | 4 +--- web/components/providers/capacitor-provider.tsx | 3 ++- web/lib/hooks/use-auth.ts | 2 +- web/package.json | 2 +- 11 files changed, 6 insertions(+), 39 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 925bc9e3..9b57e8d9 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -2,7 +2,6 @@ "mode": "pre", "tag": "beta", "initialVersions": { - "@pulse-editor/capacitor-plugin": "0.0.1", "@pulse-editor/mobile": "0.0.1", "@pulse-editor/react-api": "0.1.1-alpha.54", "@pulse-editor/shared-utils": "0.1.1-alpha.54", diff --git a/.github/workflows/build-desktop.yml b/.github/workflows/build-desktop.yml index caa7ec77..3b842587 100644 --- a/.github/workflows/build-desktop.yml +++ b/.github/workflows/build-desktop.yml @@ -32,13 +32,7 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - + - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml index a4e063af..43dc14a0 100644 --- a/.github/workflows/build-mobile.yml +++ b/.github/workflows/build-mobile.yml @@ -55,12 +55,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml index ce6f5486..d53700ec 100644 --- a/.github/workflows/build-web.yml +++ b/.github/workflows/build-web.yml @@ -28,12 +28,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml index dd78a626..a5800311 100644 --- a/.github/workflows/release-desktop.yml +++ b/.github/workflows/release-desktop.yml @@ -30,12 +30,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - - name: Install web dependencies run: npm install --workspace=web diff --git a/.github/workflows/release-mobile.yml b/.github/workflows/release-mobile.yml index d330e806..7590004e 100644 --- a/.github/workflows/release-mobile.yml +++ b/.github/workflows/release-mobile.yml @@ -52,12 +52,6 @@ jobs: - name: Build shared-utils run: npm run shared-utils-build - - name: Install capacitor-plugin dependencies - run: npm install --workspace=capacitor-plugin - - - name: Build capacitor-plugin - run: npm run capacitor-plugin-build - - name: Install web dependencies run: npm install --workspace=web diff --git a/package-lock.json b/package-lock.json index fbac190a..900c1925 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "workspaces": [ "web", "mobile", - "capacitor-plugin", "vscode-extension", "npm-packages/react-api", "npm-packages/shared-utils" diff --git a/package.json b/package.json index 81640d67..5f23909d 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "workspaces": [ "web", "mobile", - "capacitor-plugin", "vscode-extension", "npm-packages/react-api", "npm-packages/shared-utils" @@ -21,8 +20,7 @@ "react-api-build": "npm run build --workspace=npm-packages/react-api", "shared-utils-build": "npm run build --workspace=npm-packages/shared-utils", "cli-dev": "npm run dev --workspace=cli", - "cli-build": "npm run build --workspace=cli", - "capacitor-plugin-build": "npm run build --workspace=capacitor-plugin" + "cli-build": "npm run build --workspace=cli" }, "devDependencies": { "@changesets/cli": "^2.29.4", diff --git a/web/components/providers/capacitor-provider.tsx b/web/components/providers/capacitor-provider.tsx index a250c004..4b3918ad 100644 --- a/web/components/providers/capacitor-provider.tsx +++ b/web/components/providers/capacitor-provider.tsx @@ -55,7 +55,8 @@ export default function CapacitorProvider({ if (token) { // Set token in cookie CapacitorCookies.setCookie({ - url: "https://192.168.2.103:3000", // must match your WebView origin + // must match your WebView origin + url: window.location.origin, key: "pulse-editor.session-token", value: token, path: "/", diff --git a/web/lib/hooks/use-auth.ts b/web/lib/hooks/use-auth.ts index e96dcf87..b868917b 100644 --- a/web/lib/hooks/use-auth.ts +++ b/web/lib/hooks/use-auth.ts @@ -120,7 +120,7 @@ export function useAuth() { await Browser.open({ url: url.toString() }); await CapacitorCookies.deleteCookie({ - url: "https://192.168.2.103:3000", + url: window.location.origin, key: "pulse-editor.session-token", }); } else { diff --git a/web/package.json b/web/package.json index 3c625f4f..0fdc2792 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "@pulse-editor/web", - "version": "0.1.1-alpha.13", + "version": "0.1.1-beta.0", "private": true, "type": "module", "scripts": { From 747110c9199b3d999effcf2b537a954aaf11dc7b Mon Sep 17 00:00:00 2001 From: Shellishack <40737228+Shellishack@users.noreply.github.com> Date: Wed, 29 Oct 2025 20:04:27 +0800 Subject: [PATCH 12/12] Fix capacitor prod build authentication issues. --- mobile/android/app/capacitor.build.gradle | 1 + mobile/android/capacitor.settings.gradle | 3 + mobile/capacitor.config.ts | 3 + mobile/package.json | 4 +- package-lock.json | 13 ++++- package.json | 2 +- .../providers/capacitor-provider.tsx | 37 +++++++----- web/lib/hooks/use-auth.ts | 36 +++++++++--- web/lib/pulse-editor-website/backend.ts | 58 +++++++++++++++++++ web/package.json | 1 + 10 files changed, 131 insertions(+), 27 deletions(-) diff --git a/mobile/android/app/capacitor.build.gradle b/mobile/android/app/capacitor.build.gradle index 554981fe..f80517b2 100644 --- a/mobile/android/app/capacitor.build.gradle +++ b/mobile/android/app/capacitor.build.gradle @@ -14,6 +14,7 @@ dependencies { implementation project(':capacitor-browser') implementation project(':capacitor-filesystem') implementation project(':capacitor-keyboard') + implementation project(':capacitor-preferences') implementation project(':capacitor-screen-orientation') implementation project(':capacitor-status-bar') implementation project(':capawesome-capacitor-file-picker') diff --git a/mobile/android/capacitor.settings.gradle b/mobile/android/capacitor.settings.gradle index 582ac845..d224628d 100644 --- a/mobile/android/capacitor.settings.gradle +++ b/mobile/android/capacitor.settings.gradle @@ -17,6 +17,9 @@ project(':capacitor-filesystem').projectDir = new File('../../node_modules/@capa include ':capacitor-keyboard' project(':capacitor-keyboard').projectDir = new File('../../node_modules/@capacitor/keyboard/android') +include ':capacitor-preferences' +project(':capacitor-preferences').projectDir = new File('../../node_modules/@capacitor/preferences/android') + include ':capacitor-screen-orientation' project(':capacitor-screen-orientation').projectDir = new File('../../node_modules/@capacitor/screen-orientation/android') diff --git a/mobile/capacitor.config.ts b/mobile/capacitor.config.ts index a017e10b..9110b9d4 100644 --- a/mobile/capacitor.config.ts +++ b/mobile/capacitor.config.ts @@ -17,6 +17,9 @@ const config: CapacitorConfig = { SafeArea: { enabled: true, }, + CapacitorHttp: { + enabled: true, + }, }, }; diff --git a/mobile/package.json b/mobile/package.json index 5d052b7b..48b95d96 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -5,8 +5,7 @@ "type": "module", "scripts": { "android-dev": "node dev.js", - "android-run": "npx cap run android", - "android-build": "npx cap build android" + "android-run": "npx cap run android" }, "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", @@ -17,6 +16,7 @@ "@capacitor/core": "^7.2.0", "@capacitor/filesystem": "^7.0.1", "@capacitor/keyboard": "^7.0.1", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.1", "@capacitor/status-bar": "^7.0.1", "@capawesome/capacitor-file-picker": "^7.0.1" diff --git a/package-lock.json b/package-lock.json index 900c1925..90bb5aa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "@capacitor/core": "^7.2.0", "@capacitor/filesystem": "^7.0.1", "@capacitor/keyboard": "^7.0.1", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.1", "@capacitor/status-bar": "^7.0.1", "@capawesome/capacitor-file-picker": "^7.0.1" @@ -1933,6 +1934,15 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/preferences": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.2.tgz", + "integrity": "sha512-JVCy0/oc6RsRencLOZ8rMqjNxAlHs7awPJU/MXqangsJ48oO2PnYGHfCvci6WgIJlqyC0QhvWZaO1BR1lVkHWQ==", + "license": "MIT", + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/screen-orientation": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@capacitor/screen-orientation/-/screen-orientation-7.0.2.tgz", @@ -26277,7 +26287,7 @@ }, "web": { "name": "@pulse-editor/web", - "version": "0.1.1-alpha.13", + "version": "0.1.1-beta.0", "dependencies": { "@capacitor-community/safe-area": "^7.0.0-alpha.1", "@capacitor/android": "^7.4.3", @@ -26287,6 +26297,7 @@ "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", "@capacitor/keyboard": "^7.0.3", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.2", "@capacitor/status-bar": "^7.0.3", "@capawesome/capacitor-file-picker": "^7.2.0", diff --git a/package.json b/package.json index 5f23909d..7195c3d1 100644 --- a/package.json +++ b/package.json @@ -28,4 +28,4 @@ "prettier-plugin-organize-imports": "^4.3.0", "prettier-plugin-tailwindcss": "^0.6.14" } -} +} \ No newline at end of file diff --git a/web/components/providers/capacitor-provider.tsx b/web/components/providers/capacitor-provider.tsx index 4b3918ad..7d5f1003 100644 --- a/web/components/providers/capacitor-provider.tsx +++ b/web/components/providers/capacitor-provider.tsx @@ -1,7 +1,8 @@ "use client"; import { App, URLOpenListenerEvent } from "@capacitor/app"; -import { Capacitor, CapacitorCookies } from "@capacitor/core"; +import { Capacitor } from "@capacitor/core"; +import { Preferences } from "@capacitor/preferences"; import { ScreenOrientation } from "@capacitor/screen-orientation"; import { StatusBar } from "@capacitor/status-bar"; import { useContext, useEffect } from "react"; @@ -39,7 +40,7 @@ export default function CapacitorProvider({ // Set deep linking listener useEffect(() => { - App.addListener("appUrlOpen", (event: URLOpenListenerEvent) => { + App.addListener("appUrlOpen", async (event: URLOpenListenerEvent) => { /* Custom Scheme */ @@ -51,24 +52,30 @@ export default function CapacitorProvider({ const token = params.get("token"); const exp = params.get("exp"); - // Navigate to the slug + // Set token in Preferences and refresh session. + // After refresh, the cookie will be set in the webview automatically. + // Hence, no need to set cookie manually here. if (token) { - // Set token in cookie - CapacitorCookies.setCookie({ - // must match your WebView origin - url: window.location.origin, + await Preferences.set({ key: "pulse-editor.session-token", value: token, - path: "/", - expires: exp ?? undefined, - }).then(() => { - editorContext?.setEditorStates((prev) => ({ - ...prev, - isRefreshSession: true, - isSigningIn: false, - })); + }); + if (exp) { + await Preferences.set({ + key: "pulse-editor.session-expiration", + value: exp, + }); + } + } else { + await Preferences.remove({ + key: "pulse-editor.session-token", }); } + editorContext?.setEditorStates((prev) => ({ + ...prev, + isRefreshSession: true, + isSigningIn: false, + })); } /* Google Verified Links diff --git a/web/lib/hooks/use-auth.ts b/web/lib/hooks/use-auth.ts index b868917b..007d0af7 100644 --- a/web/lib/hooks/use-auth.ts +++ b/web/lib/hooks/use-auth.ts @@ -3,6 +3,7 @@ import { EditorContext } from "@/components/providers/editor-context-provider"; import { Browser } from "@capacitor/browser"; import { CapacitorCookies } from "@capacitor/core"; +import { Preferences } from "@capacitor/preferences"; import { useContext, useEffect } from "react"; import useSWR from "swr"; import { PlatformEnum } from "../enums"; @@ -60,14 +61,38 @@ export function useAuth() { ); useEffect(() => { - if (editorContext?.editorStates.isRefreshSession) { - mutate().then(() => { + async function refreshSession() { + if (editorContext?.editorStates.isRefreshSession) { + const token = await Preferences.get({ + key: "pulse-editor.session-token", + }); + + /* + Sometimes other hooks using useSWR are fired right after retuning from deep linking + before session is refreshed (triggered by window re-focus), causing cookies to be + set again between when it is removed (if removed in deep link handler) and when + session is refreshed. + + So a better approach is to clear cookies right here before refreshing session, but + possibly after other hooks are fired. + */ + if (!token.value) { + // CapacitorCookies.clearAllCookies(); + CapacitorCookies.deleteCookie({ + key: "pulse-editor.session-token", + url: process.env.NEXT_PUBLIC_BACKEND_URL, + }); + } + + await mutate(); editorContext.setEditorStates((prev) => ({ ...prev, isRefreshSession: false, })); - }); + } } + + refreshSession(); }, [editorContext?.editorStates.isRefreshSession]); // Open a sign-in page if the user is not signed in. @@ -118,11 +143,6 @@ export function useAuth() { process.env.NEXT_PUBLIC_BACKEND_URL + "/api/mobile", ); await Browser.open({ url: url.toString() }); - - await CapacitorCookies.deleteCookie({ - url: window.location.origin, - key: "pulse-editor.session-token", - }); } else { const url = getAPIUrl(`/api/auth/signout`); url.searchParams.set("callbackUrl", window.location.href); diff --git a/web/lib/pulse-editor-website/backend.ts b/web/lib/pulse-editor-website/backend.ts index 6739f966..8e8fb167 100644 --- a/web/lib/pulse-editor-website/backend.ts +++ b/web/lib/pulse-editor-website/backend.ts @@ -1,3 +1,8 @@ +import { CapacitorHttp } from "@capacitor/core"; +import { Preferences } from "@capacitor/preferences"; +import { PlatformEnum } from "../enums"; +import { getPlatform } from "../platform-api/platform-checker"; + export async function fetchAPI( relativeUrl: string | URL, options?: RequestInit, @@ -6,6 +11,59 @@ export async function fetchAPI( const url = typeof relativeUrl === "string" ? getAPIUrl(relativeUrl) : relativeUrl; + /* + Use Capacitor Http plugin for native http requests. + The native fetch will include cookies, + and when response is processed by client, + the cookies are set in the webview automatically. + */ + if (getPlatform() === PlatformEnum.Capacitor) { + // attach cookie manually + const tokenPref = await Preferences.get({ + key: "pulse-editor.session-token", + }); + const expPref = await Preferences.get({ + key: "pulse-editor.session-expiration", + }); + const token = tokenPref.value; + const exp = expPref.value; + + const headers = new Headers(options?.headers ?? {}); + if (token) { + headers.append( + "Cookie", + `pulse-editor.session-token=${token}; Path=/; Expires=${exp}; SameSite=None; Secure; ${process.env.NEXT_PUBLIC_BACKEND_URL ? "Domain=" + new URL(process.env.NEXT_PUBLIC_BACKEND_URL).hostname : ""}`, + ); + options = { + ...options, + headers, + }; + } + + const headerObj = Object.fromEntries(headers.entries()); + + const nativeResponse = await CapacitorHttp.request({ + url: url.toString(), + method: options?.method ?? "GET", + headers: headerObj, + data: options?.body, + }); + + console.log( + `${url}. \n\nRequest header: ${JSON.stringify(headerObj)} \n\nNative response: ${JSON.stringify(nativeResponse)} \n\nCookie: ${document.cookie}`, + ); + + const data = JSON.stringify(nativeResponse.data); + + // Convert CapacitorHttpResponse to Fetch Response + const fetchResponse = new Response(data, { + status: nativeResponse.status, + headers: nativeResponse.headers, + }); + + return fetchResponse; + } + return await fetch(url, { credentials: "include", ...options, diff --git a/web/package.json b/web/package.json index 0fdc2792..c96b75c8 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "@capacitor/core": "^7.4.3", "@capacitor/filesystem": "7.1.4", "@capacitor/keyboard": "^7.0.3", + "@capacitor/preferences": "^7.0.2", "@capacitor/screen-orientation": "^7.0.2", "@capacitor/status-bar": "^7.0.3", "@capawesome/capacitor-file-picker": "^7.2.0",