mirror of
https://github.com/NohamR/RMHook.git
synced 2026-04-08 07:59:58 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03b2b4c794 | ||
|
|
0db8a14ef7 |
@@ -114,24 +114,13 @@ endif()
|
||||
if(BUILD_MODE_QMLREBUILD)
|
||||
target_compile_definitions(reMarkable PRIVATE BUILD_MODE_QMLREBUILD=1)
|
||||
|
||||
# Enable Qt MOC for MessageBroker (HttpServer is pure Obj-C/Foundation, no MOC needed)
|
||||
# Enable Qt MOC for MessageBroker
|
||||
set_target_properties(reMarkable PROPERTIES AUTOMOC ON)
|
||||
|
||||
# Add MessageBroker (needs MOC) and HttpServer (native macOS)
|
||||
# Add MessageBroker source (needs MOC processing)
|
||||
target_sources(reMarkable PRIVATE
|
||||
${PROJECT_ROOT_DIR}/src/utils/MessageBroker.mm
|
||||
${PROJECT_ROOT_DIR}/src/utils/HttpServer.mm
|
||||
)
|
||||
|
||||
find_package(Qt6 COMPONENTS Qml QUIET)
|
||||
if(Qt6Qml_FOUND)
|
||||
target_link_libraries(reMarkable PRIVATE Qt6::Qml)
|
||||
else()
|
||||
find_package(Qt5 COMPONENTS Qml QUIET)
|
||||
if(Qt5Qml_FOUND)
|
||||
target_link_libraries(reMarkable PRIVATE Qt5::Qml)
|
||||
endif()
|
||||
endif()
|
||||
message(STATUS "Build mode: qmlrebuild (resource hooking)")
|
||||
endif()
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ RMHook hooks into the reMarkable Desktop app's network layer to redirect API cal
|
||||
## Compatibility
|
||||
|
||||
**Tested and working on:**
|
||||
- reMarkable Desktop v3.24.0 (released 2025-12-03)
|
||||
- reMarkable Desktop v3.26.0 (released 2026-23-03)
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/latest.png" width="40%" />
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
// Add this MessageBroker to a QML component that has access to PlatformHelpers
|
||||
// This listens for documentAccepted signals from the HTTP server
|
||||
// and calls PlatformHelpers.documentAccepted()
|
||||
|
||||
import net.noham.MessageBroker
|
||||
|
||||
MessageBroker {
|
||||
id: documentAcceptedBroker
|
||||
listeningFor: ["documentAccepted"]
|
||||
|
||||
onSignalReceived: (signal, message) => {
|
||||
console.log("[DocumentAccepted.MessageBroker] Received signal:", signal);
|
||||
console.log("[DocumentAccepted.MessageBroker] Message data:", message);
|
||||
|
||||
try {
|
||||
// Parse JSON message from HTTP server
|
||||
const data = JSON.parse(message);
|
||||
console.log("[DocumentAccepted.MessageBroker] Parsed request:", JSON.stringify(data));
|
||||
|
||||
// Extract parameters with defaults
|
||||
const url = data.url || "";
|
||||
const password = data.password || "";
|
||||
const directoryId = data.directoryId || "";
|
||||
const flag1 = data.flag1 !== undefined ? data.flag1 : false;
|
||||
const flag2 = data.flag2 !== undefined ? data.flag2 : false;
|
||||
|
||||
console.log("[DocumentAccepted.MessageBroker] Parameters:");
|
||||
console.log("[DocumentAccepted.MessageBroker] url:", url);
|
||||
console.log("[DocumentAccepted.MessageBroker] password:", password ? "(set)" : "(empty)");
|
||||
console.log("[DocumentAccepted.MessageBroker] directoryId:", directoryId);
|
||||
console.log("[DocumentAccepted.MessageBroker] flag1:", flag1);
|
||||
console.log("[DocumentAccepted.MessageBroker] flag2:", flag2);
|
||||
|
||||
// Validate required parameters
|
||||
if (!url) {
|
||||
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'url' parameter");
|
||||
return;
|
||||
}
|
||||
if (!directoryId) {
|
||||
console.error("[DocumentAccepted.MessageBroker] ERROR: Missing 'directoryId' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call PlatformHelpers.documentAccepted
|
||||
console.log("[DocumentAccepted.MessageBroker] Calling PlatformHelpers.documentAccepted...");
|
||||
PlatformHelpers.documentAccepted(url, password, directoryId, flag1, flag2);
|
||||
console.log("[DocumentAccepted.MessageBroker] Document accepted successfully");
|
||||
|
||||
} catch (error) {
|
||||
console.error("[DocumentAccepted.MessageBroker] ERROR parsing request:", error);
|
||||
console.error("[DocumentAccepted.MessageBroker] Message was:", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
// Add this MessageBroker to ExportDialog.qml after the PopupDialog definition
|
||||
// This should be added near the top of the component, after property definitions
|
||||
|
||||
import net.noham.MessageBroker
|
||||
|
||||
// ... existing properties ...
|
||||
|
||||
// MessageBroker for HTTP server export requests
|
||||
MessageBroker {
|
||||
id: exportBroker
|
||||
listeningFor: ["exportFile"]
|
||||
|
||||
onSignalReceived: (signal, message) => {
|
||||
console.log("[ExportDialog.MessageBroker] Received signal:", signal);
|
||||
console.log("[ExportDialog.MessageBroker] Message data:", message);
|
||||
|
||||
try {
|
||||
// Parse JSON message from HTTP server
|
||||
const data = JSON.parse(message);
|
||||
console.log("[ExportDialog.MessageBroker] Parsed export request:", JSON.stringify(data));
|
||||
|
||||
// Extract parameters
|
||||
const target = data.target || "";
|
||||
const documentId = data.id || data.documentId || "";
|
||||
const format = data.format !== undefined ? data.format : PlatformHelpers.ExportPdf;
|
||||
const password = data.password || "";
|
||||
const keepPassword = data.keepPassword !== undefined ? data.keepPassword : true;
|
||||
const grayscale = data.grayscale !== undefined ? data.grayscale : false;
|
||||
const pageSelection = data.pageSelection || [];
|
||||
|
||||
console.log("[ExportDialog.MessageBroker] Export parameters:");
|
||||
console.log("[ExportDialog.MessageBroker] target:", target);
|
||||
console.log("[ExportDialog.MessageBroker] documentId:", documentId);
|
||||
console.log("[ExportDialog.MessageBroker] format:", format);
|
||||
console.log("[ExportDialog.MessageBroker] keepPassword:", keepPassword);
|
||||
console.log("[ExportDialog.MessageBroker] grayscale:", grayscale);
|
||||
console.log("[ExportDialog.MessageBroker] pageSelection:", JSON.stringify(pageSelection));
|
||||
|
||||
// Validate required parameters
|
||||
if (!target) {
|
||||
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'target' parameter");
|
||||
return;
|
||||
}
|
||||
if (!documentId) {
|
||||
console.error("[ExportDialog.MessageBroker] ERROR: Missing 'id' or 'documentId' parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
// Call PlatformHelpers.exportFile
|
||||
console.log("[ExportDialog.MessageBroker] Calling PlatformHelpers.exportFile...");
|
||||
|
||||
if (pageSelection && pageSelection.length > 0) {
|
||||
console.log("[ExportDialog.MessageBroker] Exporting with page selection");
|
||||
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale, pageSelection);
|
||||
} else {
|
||||
console.log("[ExportDialog.MessageBroker] Exporting full document");
|
||||
PlatformHelpers.exportFile(target, documentId, format, password, keepPassword, grayscale);
|
||||
}
|
||||
|
||||
console.log("[ExportDialog.MessageBroker] Export completed successfully");
|
||||
|
||||
} catch (error) {
|
||||
console.error("[ExportDialog.MessageBroker] ERROR parsing export request:", error);
|
||||
console.error("[ExportDialog.MessageBroker] Message was:", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,293 +0,0 @@
|
||||
# HTTP Server for Export Requests
|
||||
|
||||
The RMHook dylib includes an HTTP server that accepts export requests and forwards them to the reMarkable application via MessageBroker.
|
||||
|
||||
## Server Details
|
||||
|
||||
- **Host**: `localhost`
|
||||
- **Port**: `8080`
|
||||
- **Base URL**: `http://localhost:8080`
|
||||
|
||||
## Endpoints
|
||||
|
||||
### `POST /exportFile`
|
||||
|
||||
Trigger a document export from the reMarkable application.
|
||||
|
||||
**Request Body** (JSON):
|
||||
```json
|
||||
{
|
||||
"target": "file:///Users/username/Desktop/output.pdf",
|
||||
"id": "document-uuid-here",
|
||||
"format": 0,
|
||||
"password": "",
|
||||
"keepPassword": true,
|
||||
"grayscale": false,
|
||||
"pageSelection": []
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `target` (string, required): File path or folder URL for the export. Use `file://` prefix for local paths.
|
||||
- `id` or `documentId` (string, required): The UUID of the document to export.
|
||||
- `format` (integer, optional): Export format. Default: `0` (PDF)
|
||||
- `0`: PDF
|
||||
- `1`: PNG
|
||||
- `2`: SVG
|
||||
- `3`: RmBundle
|
||||
- `4`: RmHtml
|
||||
- `password` (string, optional): Password for password-protected documents. Default: `""`
|
||||
- `keepPassword` (boolean, optional): Whether to keep password protection on PDF exports. Default: `true`
|
||||
- `grayscale` (boolean, optional): Export with grayscale pens. Default: `false`
|
||||
- `pageSelection` (array, optional): Array of page indices to export. If empty or omitted, exports all pages. Example: `[0, 1, 2]`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Export request sent to application"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
### `POST /documentAccepted`
|
||||
|
||||
Import/accept a document into the reMarkable application.
|
||||
|
||||
**Request Body** (JSON):
|
||||
```json
|
||||
{
|
||||
"url": "file:///Users/username/Desktop/test.pdf",
|
||||
"password": "",
|
||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
"flag1": false,
|
||||
"flag2": false
|
||||
}
|
||||
```
|
||||
|
||||
**Parameters**:
|
||||
- `url` (string, required): File URL to import. Use `file://` prefix for local paths.
|
||||
- `password` (string, optional): Password for password-protected documents. Default: `""`
|
||||
- `directoryId` (string, required): The UUID of the target directory/folder where the document should be imported.
|
||||
- `flag1` (boolean, optional): Purpose unclear. Default: `false`
|
||||
- `flag2` (boolean, optional): Purpose unclear. Default: `false`
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "success",
|
||||
"message": "Document accepted request sent to application"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Response**:
|
||||
```json
|
||||
{
|
||||
"error": "Error description"
|
||||
}
|
||||
```
|
||||
|
||||
### `GET /health`
|
||||
|
||||
Health check endpoint.
|
||||
|
||||
**Response**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"service": "RMHook HTTP Server"
|
||||
}
|
||||
```
|
||||
|
||||
## Example Requests
|
||||
|
||||
### Export a document to PDF
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/exportFile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "file:///Users/noham/Desktop/export.pdf",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 0,
|
||||
"grayscale": false,
|
||||
"keepPassword": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Export specific pages as PNG
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/exportFile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "file:///Users/noham/Desktop/pages",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 1,
|
||||
"pageSelection": [0, 1, 2],
|
||||
"grayscale": true
|
||||
}'
|
||||
```
|
||||
|
||||
### Export to RmBundle format
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/exportFile \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"target": "file:///Users/noham/Desktop/MyDocument",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 3
|
||||
}'
|
||||
```
|
||||
|
||||
### Import/Accept a document
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/documentAccepted \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "file:///Users/noham/Desktop/test.pdf",
|
||||
"password": "",
|
||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
"flag1": false,
|
||||
"flag2": false
|
||||
}'
|
||||
```
|
||||
|
||||
### Python Example - Export
|
||||
|
||||
```python
|
||||
import requests
|
||||
import json
|
||||
|
||||
# Export configuration
|
||||
export_data = {
|
||||
"target": "file:///Users/noham/Desktop/output.pdf",
|
||||
"id": "12345678-1234-1234-1234-123456789abc",
|
||||
"format": 0, # PDF
|
||||
"grayscale": False,
|
||||
"keepPassword": True
|
||||
}
|
||||
|
||||
# Send request
|
||||
response = requests.post(
|
||||
"http://localhost:8080/exportFile",
|
||||
json=export_data
|
||||
)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {response.json()}")
|
||||
```
|
||||
|
||||
### Python Example - Import Document
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
# Import configuration
|
||||
import_data = {
|
||||
"url": "file:///Users/noham/Desktop/test.pdf",
|
||||
"password": "",
|
||||
"directoryId": "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
"flag1": False,
|
||||
"flag2": False
|
||||
}
|
||||
|
||||
# Send request
|
||||
response = requests.post(
|
||||
"http://localhost:8080/documentAccepted",
|
||||
json=import_data
|
||||
)
|
||||
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {response.json()}")
|
||||
```
|
||||
|
||||
### JavaScript Example - Export
|
||||
|
||||
```javascript
|
||||
// Export configuration
|
||||
const exportData = {
|
||||
target: "file:///Users/noham/Desktop/output.pdf",
|
||||
id: "12345678-1234-1234-1234-123456789abc",
|
||||
format: 0, // PDF
|
||||
grayscale: false,
|
||||
keepPassword: true
|
||||
};
|
||||
|
||||
// Send request
|
||||
fetch("http://localhost:8080/exportFile", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(exportData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log("Success:", data))
|
||||
.catch(error => console.error("Error:", error));
|
||||
```
|
||||
|
||||
### JavaScript Example - Import Document
|
||||
|
||||
```javascript
|
||||
// Import configuration
|
||||
const importData = {
|
||||
url: "file:///Users/noham/Desktop/test.pdf",
|
||||
password: "",
|
||||
directoryId: "2166c19d-d2cc-456c-9f0e-49482031092a",
|
||||
flag1: false,
|
||||
flag2: false
|
||||
};
|
||||
|
||||
// Send request
|
||||
fetch("http://localhost:8080/documentAccepted", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify(importData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => console.log("Success:", data))
|
||||
.catch(error => console.error("Error:", error));
|
||||
```
|
||||
|
||||
## Integration with QML
|
||||
|
||||
### Export Dialog Integration
|
||||
|
||||
Add the MessageBroker snippet from `docs/ExportDialog_MessageBroker_snippet.qml` to your ExportDialog.qml replacement file. This will enable the QML side to receive export requests from the HTTP server.
|
||||
|
||||
The MessageBroker listens for "exportFile" signals and automatically calls `PlatformHelpers.exportFile()` with the provided parameters.
|
||||
|
||||
### Document Import Integration
|
||||
|
||||
Add the MessageBroker snippet from `docs/DocumentAccepted_MessageBroker_snippet.qml` to a QML component (such as GeneralSettings.qml) that has access to PlatformHelpers. This will enable the QML side to receive document import requests from the HTTP server.
|
||||
|
||||
The MessageBroker listens for "documentAccepted" signals and automatically calls `PlatformHelpers.documentAccepted()` with the provided parameters.
|
||||
|
||||
## Document ID and Directory ID Discovery
|
||||
|
||||
To find document and directory IDs, you can:
|
||||
|
||||
1. Check the reMarkable application logs when opening documents or folders
|
||||
2. Use the reMarkable Cloud API
|
||||
3. Access the local database at `~/Library/Application Support/remarkable/desktop-app/`
|
||||
4. For the root directory ID, check the logs when navigating to "My Files"
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Ensure the HTTP server started successfully by checking the logs: `2025-12-08 17:32:22.288 reMarkable[19574:1316287] [HttpServer] HTTP server started successfully on http://localhost:8080`
|
||||
- Test the health endpoint: `curl http://localhost:8080/health`
|
||||
- Check the Console.app for detailed logging from the MessageBroker and HttpServer
|
||||
- Verify the document ID and directory ID are correct UUIDs
|
||||
- Ensure the target/url path is accessible and uses the `file://` prefix
|
||||
- For imports, verify the target directory exists and is accessible
|
||||
BIN
docs/latest.png
BIN
docs/latest.png
Binary file not shown.
|
Before Width: | Height: | Size: 228 KiB After Width: | Height: | Size: 230 KiB |
@@ -1,200 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
import argparse
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
def export_document(document_id, target_path, format_type=0, grayscale=False,
|
||||
keep_password=True, password="", page_selection=None):
|
||||
"""
|
||||
Export a document via HTTP API
|
||||
|
||||
Args:
|
||||
document_id: UUID of the document to export
|
||||
target_path: Target path for the export (use file:// prefix)
|
||||
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
|
||||
grayscale: Export with grayscale pens
|
||||
keep_password: Keep password protection (for PDFs)
|
||||
password: Password for protected documents
|
||||
page_selection: List of page indices to export (None = all pages)
|
||||
"""
|
||||
print(f"\nExporting document {document_id}...")
|
||||
print(f"Target: {target_path}")
|
||||
print(f"Format: {format_type}")
|
||||
|
||||
data = {
|
||||
"target": target_path,
|
||||
"id": document_id,
|
||||
"format": format_type,
|
||||
"grayscale": grayscale,
|
||||
"keepPassword": keep_password,
|
||||
"password": password
|
||||
}
|
||||
|
||||
if page_selection:
|
||||
data["pageSelection"] = page_selection
|
||||
print(f"Pages: {page_selection}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/exportFile",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def import_document(file_url, directory_id, password="", flag1=False, flag2=False):
|
||||
"""
|
||||
Import a document via HTTP API
|
||||
|
||||
Args:
|
||||
file_url: File URL to import (use file:// prefix)
|
||||
directory_id: UUID of the target directory
|
||||
password: Password for protected documents (optional)
|
||||
flag1: Additional flag parameter
|
||||
flag2: Additional flag parameter
|
||||
"""
|
||||
print(f"\nImporting document from {file_url}...")
|
||||
print(f"Target directory: {directory_id}")
|
||||
|
||||
data = {
|
||||
"url": file_url,
|
||||
"password": password,
|
||||
"directoryId": directory_id,
|
||||
"flag1": flag1,
|
||||
"flag2": flag2
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/documentAccepted",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='reMarkable HTTP Server API Client',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog='''
|
||||
Examples:
|
||||
Export a document as PDF:
|
||||
%(prog)s export 12345678-1234-1234-1234-123456789abc file:///Users/noham/Desktop/test.pdf
|
||||
|
||||
Export as PNG with grayscale:
|
||||
%(prog)s export <doc-id> <target> --format 1 --grayscale
|
||||
|
||||
Export specific pages:
|
||||
%(prog)s export <doc-id> <target> --pages 0 1 2
|
||||
|
||||
Import a document:
|
||||
%(prog)s import file:///Users/noham/Desktop/test.pdf 2166c19d-d2cc-456c-9f0e-49482031092a
|
||||
|
||||
Import with password:
|
||||
%(prog)s import <file-url> <directory-id> --password mypassword
|
||||
'''
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(dest='command', help='Available commands')
|
||||
|
||||
# Export command
|
||||
export_parser = subparsers.add_parser('export', help='Export a document')
|
||||
export_parser.add_argument('document_id', help='UUID of the document to export')
|
||||
export_parser.add_argument('target_path', help='Target path for export (use file:// prefix)')
|
||||
export_parser.add_argument(
|
||||
'--format', '-f',
|
||||
type=int,
|
||||
default=0,
|
||||
choices=[0, 1, 2, 3, 4],
|
||||
help='Export format: 0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml (default: 0)'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--grayscale', '-g',
|
||||
action='store_true',
|
||||
help='Export with grayscale pens'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--no-keep-password',
|
||||
action='store_true',
|
||||
help='Do not keep password protection (for PDFs)'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--password', '-p',
|
||||
default='',
|
||||
help='Password for protected documents'
|
||||
)
|
||||
export_parser.add_argument(
|
||||
'--pages',
|
||||
type=int,
|
||||
nargs='+',
|
||||
help='List of page indices to export (default: all pages)'
|
||||
)
|
||||
|
||||
# Import command
|
||||
import_parser = subparsers.add_parser('import', help='Import a document')
|
||||
import_parser.add_argument('file_url', help='File URL to import (use file:// prefix)')
|
||||
import_parser.add_argument('directory_id', help='UUID of the target directory')
|
||||
import_parser.add_argument(
|
||||
'--password', '-p',
|
||||
default='',
|
||||
help='Password for protected documents'
|
||||
)
|
||||
import_parser.add_argument(
|
||||
'--flag1',
|
||||
action='store_true',
|
||||
help='Additional flag parameter'
|
||||
)
|
||||
import_parser.add_argument(
|
||||
'--flag2',
|
||||
action='store_true',
|
||||
help='Additional flag parameter'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == 'export':
|
||||
success = export_document(
|
||||
document_id=args.document_id,
|
||||
target_path=args.target_path,
|
||||
format_type=args.format,
|
||||
grayscale=args.grayscale,
|
||||
keep_password=not args.no_keep_password,
|
||||
password=args.password,
|
||||
page_selection=args.pages
|
||||
)
|
||||
if success:
|
||||
print("\n✅ Export request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Export request failed!")
|
||||
sys.exit(1)
|
||||
elif args.command == 'import':
|
||||
success = import_document(
|
||||
file_url=args.file_url,
|
||||
directory_id=args.directory_id,
|
||||
password=args.password,
|
||||
flag1=args.flag1,
|
||||
flag2=args.flag2
|
||||
)
|
||||
if success:
|
||||
print("\n✅ Import request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Import request failed!")
|
||||
sys.exit(1)
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,165 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script for RMHook HTTP Server
|
||||
Demonstrates how to trigger exports and imports via HTTP API
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import sys
|
||||
|
||||
BASE_URL = "http://localhost:8080"
|
||||
|
||||
def test_health():
|
||||
"""Test the health endpoint"""
|
||||
print("Testing /health endpoint...")
|
||||
try:
|
||||
response = requests.get(f"{BASE_URL}/health", timeout=5)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def export_document(document_id, target_path, format_type=0, grayscale=False,
|
||||
keep_password=True, password="", page_selection=None):
|
||||
"""
|
||||
Export a document via HTTP API
|
||||
|
||||
Args:
|
||||
document_id: UUID of the document to export
|
||||
target_path: Target path for the export (use file:// prefix)
|
||||
format_type: Export format (0=PDF, 1=PNG, 2=SVG, 3=RmBundle, 4=RmHtml)
|
||||
grayscale: Export with grayscale pens
|
||||
keep_password: Keep password protection (for PDFs)
|
||||
password: Password for protected documents
|
||||
page_selection: List of page indices to export (None = all pages)
|
||||
"""
|
||||
print(f"\nExporting document {document_id}...")
|
||||
print(f"Target: {target_path}")
|
||||
print(f"Format: {format_type}")
|
||||
|
||||
data = {
|
||||
"target": target_path,
|
||||
"id": document_id,
|
||||
"format": format_type,
|
||||
"grayscale": grayscale,
|
||||
"keepPassword": keep_password,
|
||||
"password": password
|
||||
}
|
||||
|
||||
if page_selection:
|
||||
data["pageSelection"] = page_selection
|
||||
print(f"Pages: {page_selection}")
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/exportFile",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def import_document(file_url, directory_id, password=""):
|
||||
"""
|
||||
Import a document via HTTP API
|
||||
|
||||
Args:
|
||||
file_url: File URL to import (use file:// prefix)
|
||||
directory_id: UUID of the target directory
|
||||
password: Password for protected documents (optional)
|
||||
"""
|
||||
print(f"\nImporting document from {file_url}...")
|
||||
print(f"Target directory: {directory_id}")
|
||||
|
||||
data = {
|
||||
"url": file_url,
|
||||
"password": password,
|
||||
"directoryId": directory_id,
|
||||
"flag1": False,
|
||||
"flag2": False
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{BASE_URL}/documentAccepted",
|
||||
json=data,
|
||||
timeout=10
|
||||
)
|
||||
print(f"Status: {response.status_code}")
|
||||
print(f"Response: {json.dumps(response.json(), indent=2)}")
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
print("=" * 60)
|
||||
print("RMHook HTTP Server Test Script")
|
||||
print("=" * 60)
|
||||
|
||||
# Test health endpoint
|
||||
if not test_health():
|
||||
print("\n❌ Health check failed. Is the server running?")
|
||||
print("Make sure reMarkable app is running with the dylib injected.")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n✅ Server is running!")
|
||||
|
||||
# Command line interface
|
||||
if len(sys.argv) < 2:
|
||||
print("\n" + "=" * 60)
|
||||
print("Usage Examples")
|
||||
print("=" * 60)
|
||||
print("\n1. Export a document:")
|
||||
print(' python3 test_http_server.py export <doc-id> <target-path> [format] [grayscale]')
|
||||
print("\n Example:")
|
||||
print(' python3 test_http_server.py export "abc-123" "file:///Users/noham/Desktop/test.pdf" 0 false')
|
||||
|
||||
print("\n2. Import a document:")
|
||||
print(' python3 test_http_server.py import <file-url> <directory-id>')
|
||||
print("\n Example:")
|
||||
print(' python3 test_http_server.py import "file:///Users/noham/Desktop/test.pdf" "2166c19d-d2cc-456c-9f0e-49482031092a"')
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
command = sys.argv[1].lower()
|
||||
|
||||
if command == "export" and len(sys.argv) >= 4:
|
||||
doc_id = sys.argv[2]
|
||||
target = sys.argv[3]
|
||||
format_type = int(sys.argv[4]) if len(sys.argv) > 4 else 0
|
||||
grayscale = sys.argv[5].lower() == "true" if len(sys.argv) > 5 else False
|
||||
|
||||
success = export_document(doc_id, target, format_type, grayscale)
|
||||
if success:
|
||||
print("\n✅ Export request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Export request failed!")
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "import" and len(sys.argv) >= 4:
|
||||
file_url = sys.argv[2]
|
||||
directory_id = sys.argv[3]
|
||||
password = sys.argv[4] if len(sys.argv) > 4 else ""
|
||||
|
||||
success = import_document(file_url, directory_id, password)
|
||||
if success:
|
||||
print("\n✅ Import request sent successfully!")
|
||||
else:
|
||||
print("\n❌ Import request failed!")
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(f"\n❌ Invalid command or missing arguments: {' '.join(sys.argv[1:])}")
|
||||
print("Run without arguments to see usage examples.")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -9,7 +9,6 @@
|
||||
#endif
|
||||
#ifdef BUILD_MODE_QMLREBUILD
|
||||
#import "MessageBroker.h"
|
||||
#import "HttpServer.h"
|
||||
#endif
|
||||
#import <objc/runtime.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@@ -315,13 +314,6 @@ static inline bool shouldPatchURL(const QString &host) {
|
||||
NSLogger(@"[reMarkable] Native callback received signal '%s' with value '%s'", signal, value);
|
||||
});
|
||||
|
||||
// Start HTTP server for export requests
|
||||
if (httpserver::start(8080)) {
|
||||
NSLogger(@"[reMarkable] HTTP server started on http://localhost:8080");
|
||||
} else {
|
||||
NSLogger(@"[reMarkable] Failed to start HTTP server");
|
||||
}
|
||||
|
||||
[MemoryUtils hookSymbol:@"QtCore"
|
||||
symbolName:@"__Z21qRegisterResourceDataiPKhS0_S0_"
|
||||
hookFunction:(void *)hooked_qRegisterResourceData
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// HTTP Server for RMHook - native macOS implementation
|
||||
#pragma once
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
namespace httpserver {
|
||||
// Start HTTP server on specified port
|
||||
bool start(uint16_t port = 8080);
|
||||
|
||||
// Stop HTTP server
|
||||
void stop();
|
||||
|
||||
// Check if server is running
|
||||
bool isRunning();
|
||||
}
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,305 +0,0 @@
|
||||
// HTTP Server for RMHook - native macOS implementation using CFSocket
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreFoundation/CoreFoundation.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <unistd.h>
|
||||
#include "HttpServer.h"
|
||||
#include "MessageBroker.h"
|
||||
#include "Logger.h"
|
||||
|
||||
static CFSocketRef g_serverSocket = NULL;
|
||||
static uint16_t g_serverPort = 0;
|
||||
static bool g_isRunning = false;
|
||||
|
||||
// Forward declarations
|
||||
static void handleClientConnection(int clientSocket);
|
||||
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType);
|
||||
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData);
|
||||
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData);
|
||||
|
||||
// Socket callback
|
||||
static void socketCallback(CFSocketRef socket, CFSocketCallBackType type,
|
||||
CFDataRef address, const void *data, void *info)
|
||||
{
|
||||
if (type == kCFSocketAcceptCallBack) {
|
||||
CFSocketNativeHandle clientSocket = *(CFSocketNativeHandle *)data;
|
||||
NSLogger(@"[HttpServer] New connection accepted, socket: %d", clientSocket);
|
||||
|
||||
// Handle client in background
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
handleClientConnection(clientSocket);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static void handleClientConnection(int clientSocket)
|
||||
{
|
||||
@autoreleasepool {
|
||||
// Read request
|
||||
char buffer[4096];
|
||||
ssize_t bytesRead = recv(clientSocket, buffer, sizeof(buffer) - 1, 0);
|
||||
|
||||
if (bytesRead <= 0) {
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
buffer[bytesRead] = '\0';
|
||||
NSString *request = [NSString stringWithUTF8String:buffer];
|
||||
|
||||
NSLogger(@"[HttpServer] Received request (%ld bytes)", (long)bytesRead);
|
||||
|
||||
// Parse request line
|
||||
NSArray *lines = [request componentsSeparatedByString:@"\r\n"];
|
||||
if (lines.count == 0) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray *requestLine = [lines[0] componentsSeparatedByString:@" "];
|
||||
if (requestLine.count < 3) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Invalid request line\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *method = requestLine[0];
|
||||
NSString *path = requestLine[1];
|
||||
|
||||
NSLogger(@"[HttpServer] %@ %@", method, path);
|
||||
|
||||
// Find body (after \r\n\r\n)
|
||||
NSRange bodyRange = [request rangeOfString:@"\r\n\r\n"];
|
||||
NSString *body = nil;
|
||||
if (bodyRange.location != NSNotFound) {
|
||||
body = [request substringFromIndex:bodyRange.location + 4];
|
||||
}
|
||||
|
||||
// Route requests
|
||||
if ([path isEqualToString:@"/exportFile"] && [method isEqualToString:@"POST"]) {
|
||||
if (!body || body.length == 0) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
|
||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
|
||||
error ? error.localizedDescription : @"Not an object"];
|
||||
sendResponse(clientSocket, 400, errorMsg, @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
handleExportFileRequest(clientSocket, (NSDictionary *)json);
|
||||
|
||||
} else if ([path isEqualToString:@"/documentAccepted"] && [method isEqualToString:@"POST"]) {
|
||||
if (!body || body.length == 0) {
|
||||
sendResponse(clientSocket, 400, @"{\"error\": \"Missing request body\"}", @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
NSData *jsonData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
id json = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
|
||||
if (error || ![json isKindOfClass:[NSDictionary class]]) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Invalid JSON: %@\"}",
|
||||
error ? error.localizedDescription : @"Not an object"];
|
||||
sendResponse(clientSocket, 400, errorMsg, @"application/json");
|
||||
close(clientSocket);
|
||||
return;
|
||||
}
|
||||
|
||||
handleDocumentAcceptedRequest(clientSocket, (NSDictionary *)json);
|
||||
|
||||
} else if ([path isEqualToString:@"/health"] || [path isEqualToString:@"/"]) {
|
||||
sendResponse(clientSocket, 200, @"{\"status\": \"ok\", \"service\": \"RMHook HTTP Server\"}", @"application/json");
|
||||
|
||||
} else {
|
||||
sendResponse(clientSocket, 404, @"{\"error\": \"Endpoint not found\"}", @"application/json");
|
||||
}
|
||||
|
||||
close(clientSocket);
|
||||
}
|
||||
}
|
||||
|
||||
static void handleExportFileRequest(int clientSocket, NSDictionary *jsonData)
|
||||
{
|
||||
NSLogger(@"[HttpServer] Processing /exportFile request");
|
||||
|
||||
// Convert to JSON string for MessageBroker
|
||||
NSError *error = nil;
|
||||
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
|
||||
options:0
|
||||
error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
|
||||
error.localizedDescription];
|
||||
sendResponse(clientSocket, 500, errorMsg, @"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
|
||||
NSLogger(@"[HttpServer] Broadcasting exportFile signal with data: %@", jsonStr);
|
||||
|
||||
// Broadcast to MessageBroker
|
||||
messagebroker::broadcast("exportFile", [jsonStr UTF8String]);
|
||||
|
||||
// Send success response
|
||||
sendResponse(clientSocket, 200,
|
||||
@"{\"status\": \"success\", \"message\": \"Export request sent to application\"}",
|
||||
@"application/json");
|
||||
}
|
||||
|
||||
static void handleDocumentAcceptedRequest(int clientSocket, NSDictionary *jsonData)
|
||||
{
|
||||
NSLogger(@"[HttpServer] Processing /documentAccepted request");
|
||||
|
||||
// Convert to JSON string for MessageBroker
|
||||
NSError *error = nil;
|
||||
NSData *jsonDataEncoded = [NSJSONSerialization dataWithJSONObject:jsonData
|
||||
options:0
|
||||
error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"{\"error\": \"Failed to encode JSON: %@\"}",
|
||||
error.localizedDescription];
|
||||
sendResponse(clientSocket, 500, errorMsg, @"application/json");
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *jsonStr = [[NSString alloc] initWithData:jsonDataEncoded encoding:NSUTF8StringEncoding];
|
||||
NSLogger(@"[HttpServer] Broadcasting documentAccepted signal with data: %@", jsonStr);
|
||||
|
||||
// Broadcast to MessageBroker
|
||||
messagebroker::broadcast("documentAccepted", [jsonStr UTF8String]);
|
||||
|
||||
// Send success response
|
||||
sendResponse(clientSocket, 200,
|
||||
@"{\"status\": \"success\", \"message\": \"Document accepted request sent to application\"}",
|
||||
@"application/json");
|
||||
}
|
||||
|
||||
static void sendResponse(int clientSocket, int statusCode, NSString *body, NSString *contentType)
|
||||
{
|
||||
NSString *statusText;
|
||||
switch (statusCode) {
|
||||
case 200: statusText = @"OK"; break;
|
||||
case 400: statusText = @"Bad Request"; break;
|
||||
case 404: statusText = @"Not Found"; break;
|
||||
case 500: statusText = @"Internal Server Error"; break;
|
||||
default: statusText = @"Unknown"; break;
|
||||
}
|
||||
|
||||
NSData *bodyData = [body dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
NSString *response = [NSString stringWithFormat:
|
||||
@"HTTP/1.1 %d %@\r\n"
|
||||
@"Content-Type: %@; charset=utf-8\r\n"
|
||||
@"Content-Length: %lu\r\n"
|
||||
@"Access-Control-Allow-Origin: *\r\n"
|
||||
@"Connection: close\r\n"
|
||||
@"\r\n"
|
||||
@"%@",
|
||||
statusCode, statusText, contentType, (unsigned long)bodyData.length, body
|
||||
];
|
||||
|
||||
NSData *responseData = [response dataUsingEncoding:NSUTF8StringEncoding];
|
||||
send(clientSocket, responseData.bytes, responseData.length, 0);
|
||||
}
|
||||
|
||||
namespace httpserver {
|
||||
|
||||
bool start(uint16_t port)
|
||||
{
|
||||
if (g_isRunning) {
|
||||
NSLogger(@"[HttpServer] Server already running on port %d", g_serverPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Create socket
|
||||
CFSocketContext context = {0, NULL, NULL, NULL, NULL};
|
||||
g_serverSocket = CFSocketCreate(kCFAllocatorDefault,
|
||||
PF_INET,
|
||||
SOCK_STREAM,
|
||||
IPPROTO_TCP,
|
||||
kCFSocketAcceptCallBack,
|
||||
socketCallback,
|
||||
&context);
|
||||
|
||||
if (!g_serverSocket) {
|
||||
NSLogger(@"[HttpServer] Failed to create socket");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set socket options
|
||||
int yes = 1;
|
||||
setsockopt(CFSocketGetNative(g_serverSocket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
|
||||
|
||||
// Bind to address
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_len = sizeof(addr);
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); // localhost only
|
||||
|
||||
CFDataRef addressData = CFDataCreate(kCFAllocatorDefault,
|
||||
(const UInt8 *)&addr,
|
||||
sizeof(addr));
|
||||
|
||||
CFSocketError error = CFSocketSetAddress(g_serverSocket, addressData);
|
||||
CFRelease(addressData);
|
||||
|
||||
if (error != kCFSocketSuccess) {
|
||||
NSLogger(@"[HttpServer] Failed to bind to port %d (error: %ld)", port, (long)error);
|
||||
CFRelease(g_serverSocket);
|
||||
g_serverSocket = NULL;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add to run loop
|
||||
CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, g_serverSocket, 0);
|
||||
CFRunLoopAddSource(CFRunLoopGetMain(), source, kCFRunLoopCommonModes);
|
||||
CFRelease(source);
|
||||
|
||||
g_serverPort = port;
|
||||
g_isRunning = true;
|
||||
|
||||
NSLogger(@"[HttpServer] HTTP server started successfully on http://localhost:%d", port);
|
||||
return true;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (!g_isRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (g_serverSocket) {
|
||||
CFSocketInvalidate(g_serverSocket);
|
||||
CFRelease(g_serverSocket);
|
||||
g_serverSocket = NULL;
|
||||
}
|
||||
|
||||
g_isRunning = false;
|
||||
NSLogger(@"[HttpServer] HTTP server stopped");
|
||||
}
|
||||
|
||||
bool isRunning()
|
||||
{
|
||||
return g_isRunning;
|
||||
}
|
||||
|
||||
} // namespace httpserver
|
||||
@@ -348,8 +348,6 @@ void ReMarkableDumpResourceFile(struct ResourceRoot *root, int node, const char
|
||||
static const char *kFilesToReplace[] = {
|
||||
"/qml/client/dialogs/ExportDialog.qml",
|
||||
"/qml/client/settings/GeneralSettings.qml",
|
||||
"/qml/client/dialogs/ExportUtils.js",
|
||||
"/qml/client/desktop/FileImportDialog.qml",
|
||||
NULL // Sentinel to mark end of list
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Example usage of MessageBroker from C++/Objective-C
|
||||
|
||||
#include <QObject>
|
||||
#include <QProcess>
|
||||
#include <QString>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include "MessageBroker.h"
|
||||
|
||||
// Example: Register MessageBroker QML type (called from dylib init)
|
||||
void initMessageBroker() {
|
||||
messagebroker::registerQmlType();
|
||||
}
|
||||
|
||||
// Example: Send a signal from C++ to QML
|
||||
void sendSignal() {
|
||||
messagebroker::broadcast("demoSignal", "Hello from C!");
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import net.noham.MessageBroker
|
||||
|
||||
MessageBroker {
|
||||
id: demoBroker
|
||||
listeningFor: ["demoSignal"]
|
||||
|
||||
onSignalReceived: (signal, message) => {
|
||||
console.log("Got message", signal, message);
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
onClicked: () => {
|
||||
demoBroker.sendSignal("mySignalName", "Hello from QML!");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user