Skip to content

Commit 9347209

Browse files
authored
Merge pull request #33 from MLH-Fellowship/desktop-helper-sample
Create Plugin/Desktop Helper Sample
2 parents 4cfcbe7 + 6f73ca5 commit 9347209

28 files changed

+17428
-0
lines changed

desktop-helper-sample/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules/
2+
dist/
3+
build/

desktop-helper-sample/README.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Desktop Helper Example
2+
3+
This sample has two components, a UXP plugin and an Electron application built in React.
4+
5+
For developers who have used `create-react-app` before, the folder structure and tools used in the Electron application will be familiar.
6+
7+
Both components must be running for the example to work correctly.
8+
9+
# Configuration
10+
11+
The following steps will help you load the plugin into Photoshop and get the Electron app up and running.
12+
13+
## UXP Plugin
14+
15+
### 1. Install Node.js dependencies
16+
17+
```
18+
$ cd uxp
19+
$ yarn install # or npm install
20+
```
21+
22+
### 2. Run plugin in watch or build mode
23+
24+
```
25+
$ cd uxp
26+
$ yarn watch # or npm run watch
27+
28+
# OR
29+
30+
$ yarn build # or npm run build
31+
```
32+
33+
- `yarn watch` or `npm run watch` will build a development version of the plugin, and recompile everytime you make a change to the source files. The result is placed in `uxp/dist`.
34+
- `yarn build` or `npm run build` will build a production version of the plugin and place it in `uxp/dist`. It will not update every time you make a change to the source files.
35+
36+
> You **must** run either `watch` or `build` prior to trying to use within Photoshop!
37+
38+
## Electron Helper
39+
40+
### 1. Install Node.js dependencies
41+
42+
```
43+
$ cd helper
44+
$ yarn install # or npm install
45+
```
46+
47+
### 2. Run helper app
48+
49+
```
50+
$ cd helper
51+
$ yarn start
52+
```
53+
54+
### 3. (Optional) Build the helper app
55+
56+
```
57+
$ cd helper
58+
$ yarn build
59+
```
60+
61+
**Warning:** This command will take quite some time to execute.
62+
63+
In Electron, production builds compile JavaScript into a binary executable on the target platform for distribution or personal use. It is recommended that you build for production when you finish development.
64+
65+
Once built, navigate to the `helper/dist` folder and double click the `UXP Helper App Setup 1.0.0` installer. Upon installation, the helper app can be run locally. Please note, the Electron project will have to be rebuilt and reinstalled for any future changes to take effect in production.
66+
67+
# Load the Plugin
68+
69+
You can use the UXP Developer Tools to load the plugin into Photoshop.
70+
71+
If the plugin hasn't already been added to your workspace in the UXP Developer Tools, you can add it by clicking "Add Plugin..." and selecting `uxp/dist/manifest.json`. **DO NOT** select the `manifest.json` file inside the `uxp/plugin` folder.
72+
73+
Once added, you can load it into Photoshop by clicking the ••• button on the corresponding row, and clicking "Load". Switch to Photoshop and you should see the sample panel.
74+
75+
# Using the Plugin
76+
77+
This plugin uses an embedded websocket server to communicate between the Electron helper app and UXP.
78+
79+
Files for the server and electron configuration are located in the `helper/public` folder and are served as entrypoints for Electron on build.
80+
81+
Once both components are launched, you should be greeted with `'Connected'` status' on both components. If one component disconnects, the other will automatically recognize this disconnect as well, and the changes should be reflected with a `'Disconnected'` status.
82+
83+
![Plugin Screenshot](./images/connection.png)
84+
85+
Since it's an embedded server, its worth noting that this only runs on `localhost` and only administers communication between a single instance of the helper app and the UXP plugin. By default, the websocket server runs on port `4040` and the Electron helper app is served from port `3000`.
86+
87+
Other than connecting with one another, the two components can also pass strings of text which get reflected in their `Received data from the helper` and `Received data from UXP` sections.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
{
2+
"name": "com.adobe.example.desktop-helper-app",
3+
"version": "1.0.0",
4+
"license": "none",
5+
"private": true,
6+
"main": "public/electron.js",
7+
"homepage": "./",
8+
"scripts": {
9+
"start": "concurrently -k \"npm:start:react\" \"npm:start:electron\"",
10+
"start:react": "cross-env BROWSER=none react-scripts start",
11+
"start:electron": "wait-on tcp:3000 && electron public/electron.js",
12+
"build": "npm run build:react && npm run build:electron",
13+
"build:react": "react-scripts build",
14+
"build:electron": "electron-builder",
15+
"eject": "react-scripts eject"
16+
},
17+
"devDependencies": {
18+
"@adobe/react-spectrum": "^3.11.2",
19+
"concurrently": "^6.2.0",
20+
"cross-env": "^7.0.3",
21+
"electron": "^13.1.6",
22+
"electron-builder": "^22.11.7",
23+
"wait-on": "^6.0.0"
24+
},
25+
"dependencies": {
26+
"electron-is-dev": "^2.0.0",
27+
"express": "^4.17.1",
28+
"react": "^17.0.2",
29+
"react-dom": "^17.0.2",
30+
"react-scripts": "4.0.3",
31+
"socket.io": "^4.1.2",
32+
"socket.io-client": "^4.1.3"
33+
},
34+
"build": {
35+
"appId": "com.electron.adobe.example.desktop-helper-sample",
36+
"productName": "UXP Helper App",
37+
"mac": {
38+
"category": "public.app-category.developer-tools",
39+
"target": [
40+
"dmg"
41+
]
42+
},
43+
"win": {
44+
"asar": false,
45+
"target": [
46+
"nsis"
47+
]
48+
},
49+
"directories": {
50+
"output": "dist"
51+
}
52+
},
53+
"browserslist": {
54+
"production": [
55+
">0.2%",
56+
"not dead",
57+
"not op_mini all"
58+
],
59+
"development": [
60+
"last 1 chrome version",
61+
"last 1 firefox version",
62+
"last 1 safari version"
63+
]
64+
}
65+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
const { app, BrowserWindow } = require('electron');
2+
const isDevelopment = require('electron-is-dev');
3+
const path = require('path');
4+
5+
require('./server.js');
6+
7+
const createWindow = () => {
8+
const window = new BrowserWindow({
9+
width: 800,
10+
height: 600,
11+
webPreferences: {
12+
nodeIntegration: true,
13+
},
14+
});
15+
16+
// Configure electron development environment
17+
window.loadURL(
18+
isDevelopment
19+
? 'http://localhost:3000'
20+
: `file://${path.join(__dirname, '../build/index.html')}`
21+
);
22+
23+
if (isDevelopment) {
24+
window.webContents.openDevTools({
25+
mode: 'detach',
26+
});
27+
}
28+
};
29+
30+
app.whenReady().then(createWindow);
31+
32+
app.on('window-all-closed', () => {
33+
if (process.platform !== 'darwin') {
34+
app.quit();
35+
}
36+
});
37+
38+
app.on('activate', () => {
39+
if (BrowserWindow.getAllWindows().length === 0) {
40+
createWindow();
41+
}
42+
});
15 KB
Binary file not shown.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1" />
7+
<meta name="description" content="UXP Helper App" />
8+
<title>UXP Helper</title>
9+
</head>
10+
<body>
11+
<noscript>You need to enable JavaScript to run this app.</noscript>
12+
<div id="root"></div>
13+
</body>
14+
</html>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const http = require('http');
2+
const express = require('express');
3+
const { Server } = require('socket.io');
4+
5+
const startServer = async () => {
6+
const port = 4040;
7+
8+
const app = express();
9+
10+
const server = http.createServer(app);
11+
12+
const io = new Server(server, {
13+
cors: {
14+
origin: '*',
15+
methods: ['GET'],
16+
transports: ['websocket'],
17+
},
18+
});
19+
20+
server.listen(port, () => {
21+
console.log(`[server] Server listening on port ${port}`);
22+
});
23+
24+
io.on('connection', (socket) => {
25+
io.emit('server-connection', true);
26+
27+
socket.on('uxp-connected', () => {
28+
io.emit('uxp-connected', true);
29+
});
30+
31+
socket.on('message', (message) => {
32+
io.emit('uxp-message', message);
33+
});
34+
35+
socket.on('helper-message', (message) => {
36+
io.emit('message', message);
37+
});
38+
39+
socket.on('disconnect', () => {
40+
io.emit('uxp-connected', false);
41+
});
42+
});
43+
44+
// Emit connect when uxp attempts to reconnect
45+
io.on('reconnect', () => {
46+
io.emit('server-connection', true);
47+
});
48+
49+
// Emit disconnect when helper app closes
50+
process.on('exit', () => {
51+
io.emit('server-connection', false);
52+
});
53+
};
54+
55+
startServer().catch((err) => {
56+
console.log(`[server] Error: ${err}`);
57+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Flex, Heading, Text } from '@adobe/react-spectrum';
2+
3+
import OptionsMenu from './components/OptionsMenu';
4+
5+
const App = () => {
6+
return (
7+
<Flex
8+
direction="column"
9+
gap="size-100"
10+
alignItems="center"
11+
justifyContent="center"
12+
height="100vh"
13+
>
14+
<Heading level={2} marginBottom={-2}>
15+
Welcome to the UXP Helper App
16+
</Heading>
17+
<Text marginBottom={8}>
18+
<i>To start, load the UXP plugin into Photoshop and send a message</i>
19+
</Text>
20+
<OptionsMenu />
21+
</Flex>
22+
);
23+
};
24+
25+
export default App;
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import React, { useState, useEffect, useContext } from 'react';
2+
import {
3+
Button,
4+
Flex,
5+
Item,
6+
StatusLight,
7+
Tabs,
8+
TabList,
9+
TabPanels,
10+
Text,
11+
TextField,
12+
} from '@adobe/react-spectrum';
13+
14+
import TextContainer from './TextContainer';
15+
import { SocketContext } from './SocketContext';
16+
17+
const OptionsMenu = () => {
18+
const socket = useContext(SocketContext);
19+
20+
const [connectionStatus, setConnectionStatus] = useState(false);
21+
const [helperData, setHelperData] = useState('');
22+
const [uxpData, setUXPData] = useState('');
23+
24+
useEffect(() => {
25+
socket.on('uxp-connected', (isUXPConnected) => {
26+
setConnectionStatus(isUXPConnected);
27+
});
28+
29+
socket.on('uxp-message', (receivedMessage) => {
30+
setUXPData(receivedMessage);
31+
});
32+
}, []);
33+
34+
const sendHelperData = () => {
35+
socket.emit('helper-message', helperData);
36+
};
37+
38+
let connectionStatusLight = (
39+
<StatusLight variant="negative">Disconnected</StatusLight>
40+
);
41+
42+
if (connectionStatus) {
43+
connectionStatusLight = (
44+
<StatusLight variant="positive">Connected</StatusLight>
45+
);
46+
}
47+
48+
return (
49+
<Flex width="size-6000" height="size-8000">
50+
<Tabs aria-label="UXP Helper Options">
51+
<TabList>
52+
<Item key="status">UXP Status</Item>
53+
<Item key="data">Send data to UXP</Item>
54+
<Item key="log">Received data from UXP</Item>
55+
</TabList>
56+
<TabPanels marginX={4} marginY={16}>
57+
<Item key="status">
58+
<TextContainer>{connectionStatusLight}</TextContainer>
59+
</Item>
60+
<Item key="data">
61+
<TextField
62+
aria-label="Send data to UXP"
63+
placeholder="Enter data to be sent"
64+
onChange={(s) => setHelperData(s)}
65+
></TextField>
66+
<Button variant="cta" marginX={16} onPress={() => sendHelperData()}>
67+
Send
68+
</Button>
69+
</Item>
70+
<Item key="log">
71+
<TextContainer>
72+
<Text>
73+
{uxpData === ''
74+
? 'Messages from UXP will appear here'
75+
: uxpData}
76+
</Text>
77+
</TextContainer>
78+
</Item>
79+
</TabPanels>
80+
</Tabs>
81+
</Flex>
82+
);
83+
};
84+
85+
export default OptionsMenu;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import React, { createContext } from 'react';
2+
import io from 'socket.io-client';
3+
4+
export const socket = io('http://localhost:4040', {
5+
transports: ['websocket'],
6+
});
7+
8+
export const SocketContext = createContext();

0 commit comments

Comments
 (0)