Skip to content

Commit

Permalink
Implement electron standalone app and add external data directory
Browse files Browse the repository at this point in the history
  • Loading branch information
thera2002 committed Nov 17, 2024
1 parent 6f95733 commit 0a62397
Show file tree
Hide file tree
Showing 5 changed files with 4,875 additions and 557 deletions.
68 changes: 59 additions & 9 deletions dist/examples/electron-editor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The editor integrates OpenLIME structures to handle annotations that can include
- [Node.js](https://nodejs.org/) (v16 or later recommended)
- [npm](https://www.npmjs.com/) (comes with Node.js)

### Steps
### Development Setup
1. Clone or download this repository to your local machine.
2. Navigate to the project directory in your terminal:
```bash
Expand All @@ -28,19 +28,74 @@ The editor integrates OpenLIME structures to handle annotations that can include
npm install
```

### Building Standalone Application
To create standalone executables for different platforms:

1. For all platforms:
```bash
npm run build
```

2. For specific platforms:
```bash
# For Windows
npm run build:win
# For macOS
npm run build:mac
# For Linux
npm run build:linux
```

The built applications will be available in the `dist` directory.

## Usage

### Development Mode
1. Start the application:
```bash
npm start
```
This will launch the annotation editor in an Electron window.

2. To use the editor in a specific mode (e.g., editor mode), run:
```bash
npm run editor
```

3. Begin creating, editing, and managing your annotations within the application interface.
### Standalone Application

#### Running the Application
- **Windows**: Run the installed `.exe` file or use the Start Menu shortcut
- **macOS**: Open the `.app` from Applications folder
- **Linux**: Run the AppImage:
```bash
./OpenLIME\ Electron\ Editor-1.0.0.AppImage
```

To run in editor mode:
- **Windows**: Use the "Editor Mode" shortcut or run from command line:
```bash
"C:\Program Files\OpenLIME Electron Editor\OpenLIME Electron Editor.exe" --editor
```
- **macOS**: From terminal:
```bash
/Applications/OpenLIME\ Electron\ Editor.app/Contents/MacOS/OpenLIME\ Electron\ Editor --editor
```
- **Linux**:
```bash
./OpenLIME\ Electron\ Editor-1.0.0.AppImage --editor
```

#### Accessing Annotation Data
Annotations are stored in a user-specific data directory. You can access this directory through:
1. The application menu: `File > Open Data Directory`
2. Manual navigation to:
- **Windows**: `%APPDATA%\OpenLIME Electron Editor\data`
- **macOS**: `~/Library/Application Support/OpenLIME Electron Editor/data`
- **Linux**: `~/.config/OpenLIME Electron Editor/data`

The annotations are stored in `anno.json` within the data directory.

## How It Works

Expand Down Expand Up @@ -94,14 +149,9 @@ The `main.js` file is the entry point for the Electron application. It handles:
});
```
- **Frontend to Backend**:
The `Annotation.js` file provides a structure for an OpenLIME annotation, including features like a unique identifier, description, category, drawing style, and potentially embedded graphical elements. I'll use this structure as a reference in the "Communication Between Backend and Frontend" section.

The backend (`main.js`) and frontend (`index.js`) work together to handle the creation of OpenLIME annotations, including SVG graphical elements. Below is an example:

The user interacts with the UI to define an annotation and its associated SVG data. The structure matches the OpenLIME `Annotation` class.
The `Annotation.js` file provides a structure for an OpenLIME annotation, including features like a unique identifier, description, category, drawing style, and potentially embedded graphical elements.
Example in the frontend (`index.js`):
```javascript
Expand Down
5 changes: 5 additions & 0 deletions dist/examples/electron-editor/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ function formatPerformanceTime() {
// Check URL parameters
const urlParams = new URLSearchParams(window.location.search);
const editorEnable = urlParams.has('editor');
if (editorEnable) {
console.log("Running in editor mode");
} else {
console.log("Running in default mode");
}

// Create an OpenLIME canvas into openlime
const lime = new OpenLIME.Viewer('.openlime');
Expand Down
121 changes: 95 additions & 26 deletions dist/examples/electron-editor/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,40 @@ const path = require('path');
const fs = require('fs');
const url = require('url');

const annotationFile = path.join(__dirname, 'data', 'anno.json');
// Proper argument handling for both development and packaged modes
function getIsEditorMode() {
if (process.defaultApp) {
return process.argv.slice(2).some(arg => arg === '--editor');
} else {
return process.argv.slice(1).some(arg => arg === '--editor');
}
}

// Get the user data directory path
const userDataPath = app.getPath('userData');
const dataDir = path.join(userDataPath, 'data');
const annotationFile = path.join(dataDir, 'anno.json');

console.log("App launched with arguments:", process.argv);
console.log("Editor mode:", getIsEditorMode());
console.log("User data directory:", userDataPath);
console.log("Data directory:", dataDir);

let annotations = [];

// Initialize annotations
try {
// Check if data directory exists, if not create it
const dataDir = path.join(__dirname, 'data');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir);
fs.mkdirSync(dataDir, { recursive: true });
console.log('Created data directory:', dataDir);
}

// Check if file exists
if (!fs.existsSync(annotationFile)) {
// Create file with empty array
fs.writeFileSync(annotationFile, '[]', 'utf8');
console.log('Created annotations file:', annotationFile);
} else {
// Read existing file
const data = fs.readFileSync(annotationFile, 'utf8');
Expand All @@ -28,10 +47,10 @@ try {
annotations = [];
fs.writeFileSync(annotationFile, '[]', 'utf8');
}
console.log('Loaded existing annotations');
}
} catch (err) {
console.error('Error initializing annotations file:', err);
// Initialize with empty array in case of error
annotations = [];
try {
fs.writeFileSync(annotationFile, '[]', 'utf8');
Expand All @@ -42,20 +61,16 @@ try {

// Create the main window
function createWindow() {

const args = process.argv.slice(2);
const editorArgs = args.find(arg => arg.startsWith('--editor'));
const isEditorMode = editorArgs ? true : false;
const queryParams = isEditorMode ? new URLSearchParams({ editor: isEditorMode }).toString() : "";
const isEditorMode = getIsEditorMode();
const queryParams = isEditorMode ? new URLSearchParams({ editor: 'true' }).toString() : "";
const indexPath = `file://${path.join(__dirname, 'index.html')}?${queryParams}`;


const win = new BrowserWindow({
webPreferences: {
contextIsolation: true, // Keep this enabled for security
enableRemoteModule: false, // Disable unless absolutely necessary
nodeIntegration: false, // Use preload for secure Node.js access
preload: path.join(__dirname, 'preload.js'), // Use a preload script
contextIsolation: true,
enableRemoteModule: false,
nodeIntegration: false,
preload: path.join(__dirname, 'preload.js'),
webSecurity: true
}
});
Expand All @@ -69,28 +84,70 @@ function createWindow() {
event.preventDefault();
}
});

// Add menu item to open data directory
const { Menu, MenuItem } = require('electron');
const menu = Menu.getApplicationMenu() || Menu.buildFromTemplate([]);

const fileMenu = menu.items.find(item => item.label === 'File') ||
new MenuItem({ label: 'File', submenu: [] });

fileMenu.submenu.append(new MenuItem({
label: 'Open Data Directory',
click: () => {
require('electron').shell.openPath(dataDir);
}
}));

if (!menu.items.find(item => item.label === 'File')) {
menu.append(fileMenu);
}

Menu.setApplicationMenu(menu);
}

// Start the browser when app is ready
app.on('ready', createWindow);
// Handle the 'second-instance' event
app.on('second-instance', (event, commandLine, workingDirectory) => {
const isEditorMode = commandLine.slice(1).some(arg => arg === '--editor');
const existingWindows = BrowserWindow.getAllWindows();

if (existingWindows.length > 0) {
const win = existingWindows[0];
if (win.isMinimized()) win.restore();
win.focus();

// Reload the window with new mode if necessary
const currentURL = new URL(win.webContents.getURL());
const currentIsEditor = currentURL.searchParams.has('editor');

if (currentIsEditor !== isEditorMode) {
const queryParams = isEditorMode ? new URLSearchParams({ editor: 'true' }).toString() : "";
const indexPath = `file://${path.join(__dirname, 'index.html')}?${queryParams}`;
win.loadURL(indexPath);
}
}
});

// Ensure single instance
const gotTheLock = app.requestSingleInstanceLock();

if (!gotTheLock) {
app.quit();
} else {
app.whenReady().then(createWindow);
}

// Handler to read annotations
// IPC Handlers
ipcMain.handle('read-annotations', async () => {
try {
const data = fs.readFileSync(annotationFile, 'utf8');
// Handle empty file or whitespace-only content
if (!data.trim()) {
return [];
}
return JSON.parse(data);
return data.trim() ? JSON.parse(data) : [];
} catch (err) {
console.error('Error in reading JSON file:', err);
// If there's a JSON parsing error or any other error, return empty array
return [];
}
});

// Handler to create a new annotation
ipcMain.handle('create-annotation', async (event, newAnnotation) => {
try {
annotations.push(newAnnotation);
Expand All @@ -102,7 +159,6 @@ ipcMain.handle('create-annotation', async (event, newAnnotation) => {
}
});

// Handler to update an existing annotation
ipcMain.handle('update-annotation', async (event, updatedAnnotation) => {
try {
const index = annotations.findIndex(anno => anno.id === updatedAnnotation.id);
Expand All @@ -119,7 +175,6 @@ ipcMain.handle('update-annotation', async (event, updatedAnnotation) => {
}
});

// Handler to delete an annotation
ipcMain.handle('delete-annotation', async (event, annotationId) => {
try {
annotations = annotations.filter(anno => anno.id !== annotationId);
Expand All @@ -130,3 +185,17 @@ ipcMain.handle('delete-annotation', async (event, annotationId) => {
throw err;
}
});

// Handle app activation
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

// Quit when all windows are closed
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
Loading

0 comments on commit 0a62397

Please sign in to comment.