Compare commits
126 Commits
developmen
...
0ae0d644a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ae0d644a9 | ||
|
|
ce2832a584 | ||
|
|
b6b902ad4a | ||
|
|
3da3b187ec | ||
|
|
86ea73bfbd | ||
|
|
a14c7c7a3f | ||
|
|
9df9d884e5 | ||
|
|
2bf4d7c141 | ||
|
|
e37fd1ddbb | ||
|
|
a1e74829e8 | ||
|
|
2c7b609295 | ||
|
|
1008a55ec3 | ||
|
|
c8fdb8b1e7 | ||
|
|
bb51771dc7 | ||
|
|
53d200f10e | ||
|
|
7693b12aeb | ||
|
|
72d928f907 | ||
|
|
9b98b454fd | ||
|
|
ec29a576d5 | ||
|
|
1f90c819a0 | ||
|
|
9f76d6b7f4 | ||
|
|
ca4fddd57f | ||
|
|
c5a368aee5 | ||
|
|
b8199375a9 | ||
|
|
bab08cd8a7 | ||
|
|
36aed19169 | ||
|
|
5b59dbc2da | ||
|
|
002c27e73c | ||
|
|
3feaf1bbc3 | ||
|
|
b8b7b253bb | ||
|
|
b3f59b7bf5 | ||
|
|
01797f28aa | ||
|
|
2af6b3954b | ||
|
|
5a5814b2bc | ||
|
|
0ebfcd18bb | ||
|
|
2ad6bce8db | ||
|
|
7c0bb6719d | ||
|
|
fb8af70742 | ||
|
|
3d7aa1ab92 | ||
|
|
7784f7f2a3 | ||
|
|
083fc56337 | ||
|
|
e942b234c4 | ||
|
|
f6875acc72 | ||
|
|
280de213c1 | ||
|
|
30db989b2c | ||
|
|
0497f5767b | ||
|
|
fdbd12b64f | ||
|
|
54ae460da5 | ||
| 9a7b2b4089 | |||
| 5e04deab9c | |||
|
|
99984e7b6c | ||
|
|
fb02fc80a0 | ||
|
|
bc4bf6d294 | ||
|
|
29111e8dce | ||
|
|
a25978cf6f | ||
|
|
6c7a81f915 | ||
|
|
9f9ad8b1bb | ||
|
|
75c5d289cf | ||
|
|
4870a56dad | ||
|
|
86feb8517a | ||
| 30b7a00229 | |||
|
|
ff342fd9ea | ||
|
|
ced234fa08 | ||
|
|
c37c87f6d4 | ||
|
|
cdae86dd86 | ||
|
|
14cc3d1c3e | ||
|
|
a54d2c08bc | ||
|
|
6e7dacc1e0 | ||
|
|
f3558dc75b | ||
|
|
a83334aac7 | ||
|
|
987642886c | ||
|
|
fb75d450ac | ||
|
|
0dce6c89c0 | ||
|
|
0028df4604 | ||
|
|
040ab4aa4e | ||
|
|
2d27750d0e | ||
|
|
adca2e511b | ||
|
|
eb53e46ebc | ||
|
|
3a4812de4c | ||
| 31203648b4 | |||
|
|
e91907a884 | ||
|
|
7ac3f1fb16 | ||
|
|
556f4e66a5 | ||
|
|
9094b28376 | ||
| 9b1fffe024 | |||
| 7745ac1ef0 | |||
| ddda53d643 | |||
| 328cd1d51a | |||
|
|
5105f42479 | ||
|
|
ab6a071986 | ||
|
|
d762ec3b20 | ||
|
|
2533410c78 | ||
|
|
60df29243c | ||
|
|
d67d0085ef | ||
|
|
8283a4b0f7 | ||
|
|
c97e84b0ef | ||
|
|
bc1a3b2cc5 | ||
|
|
f706855b5b | ||
|
|
38f5b39060 | ||
|
|
143a760128 | ||
|
|
976ca7e0c1 | ||
|
|
491574a438 | ||
|
|
abd0165164 | ||
|
|
bf97d7acf1 | ||
|
|
fd279a9635 | ||
|
|
7b4f39760c | ||
|
|
31b278f238 | ||
|
|
01d2e214cb | ||
|
|
d2f0eaa1dc | ||
|
|
f565e0b421 | ||
|
|
b4d8dfa756 | ||
|
|
4bc1e8c4c9 | ||
|
|
2a56129336 | ||
|
|
f07aed2a02 | ||
|
|
f6fab17e00 | ||
|
|
4c65d9dc42 | ||
|
|
98bd835342 | ||
| 6802eac1d9 | |||
| 0906248fd1 | |||
| 26b48c4fc1 | |||
| 1860f9c09e | |||
| 0b7921f660 | |||
|
|
8ec3eb0b6a | ||
|
|
42a119cf34 | ||
|
|
6a788c11b4 | ||
|
|
88d166a8d8 |
14
.gitignore
vendored
@@ -8,7 +8,7 @@
|
||||
*.sock
|
||||
*.csv
|
||||
*.xlsx
|
||||
#*.pdf
|
||||
# *.pdf
|
||||
*.cfg
|
||||
*.ini
|
||||
*.bak
|
||||
@@ -20,7 +20,13 @@ backups/
|
||||
tmp/
|
||||
temp/
|
||||
development/
|
||||
.vscode
|
||||
*.code-workspace
|
||||
|
||||
# .vscode
|
||||
# *.code-workspace
|
||||
file_cache/
|
||||
aether_native_app_config.json
|
||||
# package-lock.json
|
||||
builds/
|
||||
resources/seed_config.json
|
||||
event.env
|
||||
.playwright-mcp/
|
||||
builds.tar.gz
|
||||
|
||||
BIN
AE_app_native_Electron_2026-04-20.tar.gz
Normal file
435
README.md
Normal file
@@ -0,0 +1,435 @@
|
||||
# Aether Native Launcher (Electron)
|
||||
|
||||
The Aether Native Launcher is a specialized Electron-based shell for the Aether Platform. It provides a secure bridge between the SvelteKit web UI and the local operating system, enabling features restricted by browser sandboxing.
|
||||
|
||||
## 🚀 Overview
|
||||
|
||||
This application serves as the "Native Mode" runtime for Aether podiums and devices. It handles:
|
||||
- **Local File Orchestration:** Managed cache for presentation files (PPTx, Keynote, PDF).
|
||||
- **Automation:** Specialized AppleScript handlers for PowerPoint and Keynote.
|
||||
- **Hardware Telemetry:** Direct access to CPU, RAM, and Network interface data.
|
||||
- **Remote Control:** Slide navigation and application control via WebSocket intents.
|
||||
|
||||
## Launcher Terminology
|
||||
|
||||
Use these terms consistently when working on the launcher bridge:
|
||||
|
||||
- **Launch Profiles**: the Svelte-side map keyed by file extension.
|
||||
- **Launch Profile**: one resolved config object selected from that map for a file.
|
||||
- **Native Template**: the single AppleScript or shell command string Electron executes after
|
||||
the file has been copied to temp. This is an implementation detail of the bridge.
|
||||
|
||||
In short, the profiles are the policy; the launch profile is the selected policy entry; the
|
||||
native template is the executable string produced by that policy.
|
||||
|
||||
Do not use `launch_scripts` as the public/config-facing term. If that wording appears in old
|
||||
comments or generated output, treat it as stale naming drift and update it to `launch_profiles`
|
||||
when referring to the Svelte-side map or `native_template` when referring to the resolved string.
|
||||
|
||||
## 🖥️ Onsite Deployment
|
||||
|
||||
**Current hardware:** MacBook Air 2018 — Intel x64. All current deployments use `aether_launcher-darwin-x64`.
|
||||
**Future hardware:** Apple Silicon Macs use `aether_launcher-darwin-arm64`. Windows and Linux builds are planned.
|
||||
|
||||
SSH user on all laptops: **`speaker ready`**
|
||||
IP pattern: `192.168.32.1XX` (XX = zero-padded laptop number, e.g. 03 → `.103`). Find/replace this prefix for other onsite environments.
|
||||
|
||||
Deploy files live in `deploy/`:
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `deploy/deploy.sh` | Deploy script — handles arch detection, scp, and seed.json |
|
||||
| `deploy/devices.conf` | Laptop list: number, IP, `event_device_id` |
|
||||
| `deploy/event.env` | **Gitignored** — per-event API key and URLs (create from example) |
|
||||
| `deploy/event.env.example` | Template for `event.env` |
|
||||
|
||||
### Step 1 — Build the app (workstation)
|
||||
|
||||
```bash
|
||||
cd ~/OSIT_dev/aether_app_native_electron
|
||||
npm run package:mac
|
||||
# Produces builds/aether_launcher-darwin-x64/aether_launcher.app ← the one to deploy
|
||||
```
|
||||
|
||||
Only rebuild if source code has changed. The `.app` bundle is identical for all Intel laptops —
|
||||
only `~/seed.json` differs per device.
|
||||
|
||||
> **Note:** The build will print `WARNING: Could not find icon "..." with extension ".icon"`. This
|
||||
> is cosmetic — `electron-packager` checks for several icon format variants and warns for each it
|
||||
> doesn't find. The `.icns` file is correctly embedded as `electron.icns` inside the app bundle.
|
||||
|
||||
### Step 2 — Create event.env
|
||||
|
||||
```bash
|
||||
cp deploy/event.env.example deploy/event.env
|
||||
# Edit deploy/event.env — fill in AETHER_API_KEY
|
||||
```
|
||||
|
||||
Create the API key in the Aether admin panel before the show (Core → Accounts or Events →
|
||||
Devices API key section). All laptops share one key per event. Delete it after the show.
|
||||
|
||||
### Step 3 — Run the deploy script
|
||||
|
||||
```bash
|
||||
# Deploy specific laptops:
|
||||
./deploy/deploy.sh 01 02 03
|
||||
|
||||
# Deploy all laptops in devices.conf:
|
||||
./deploy/deploy.sh all
|
||||
|
||||
# Update seed.json only (no .app copy — e.g. when rotating the API key):
|
||||
./deploy/deploy.sh --seed-only all
|
||||
|
||||
# Deploy and re-grant Accessibility permission in one pass:
|
||||
./deploy/deploy.sh --fix-accessibility all
|
||||
```
|
||||
|
||||
The script auto-detects each Mac's CPU architecture, copies the correct `.app` build, writes
|
||||
`seed.json`, and verifies. One SSH connection failure won't abort the batch — it logs and
|
||||
continues, then reports which laptops need a retry.
|
||||
|
||||
### Step 4 — Verify and launch
|
||||
|
||||
After the script completes, launch the app on each laptop and confirm it connects and shows
|
||||
the correct device name in the Launcher UI.
|
||||
|
||||
### macOS Accessibility Permission
|
||||
|
||||
The launcher sends keystrokes to PowerPoint and Keynote via AppleScript. macOS requires
|
||||
explicit **Accessibility** access for this. Every time a new `.app` binary is deployed, macOS
|
||||
invalidates the stored permission because the code signature changes — even when rsync
|
||||
updates in-place.
|
||||
|
||||
**Symptom:** Slide control silently fails; `osascript` eventually returns a permissions error.
|
||||
|
||||
**Manual fix** (GUI — always works):
|
||||
1. System Settings → Privacy & Security → Accessibility
|
||||
2. Find `aether_launcher` in the list → remove it (− button)
|
||||
3. Re-add it (+ button → `/Applications/aether_launcher.app`) and toggle it on
|
||||
|
||||
**Automated fix** — use the `--fix-accessibility` deploy flag:
|
||||
```bash
|
||||
./deploy/deploy.sh --fix-accessibility 01 02 03
|
||||
./deploy/deploy.sh --build --fix-accessibility all
|
||||
```
|
||||
This runs `tccutil reset` (no password required) then attempts a direct TCC database grant
|
||||
via `sudo sqlite3`. The sqlite3 step requires NOPASSWD sudo on each Mac — one-time setup:
|
||||
|
||||
```bash
|
||||
ssh "speaker ready"@192.168.32.1XX "sudo visudo -f /etc/sudoers.d/aether-tcc"
|
||||
```
|
||||
Add this line, then save:
|
||||
```text
|
||||
speaker ready ALL=(ALL) NOPASSWD: /usr/bin/sqlite3 /Library/Application\ Support/com.apple.TCC/TCC.db *
|
||||
```
|
||||
|
||||
If the sqlite3 grant fails (SIP enabled, sudoers not configured), the script logs a warning
|
||||
and falls back gracefully — a fresh permission prompt appears the first time the app uses
|
||||
accessibility. The `--fix-accessibility` flag can be combined with any other flag.
|
||||
|
||||
**Long-term fix:** Code-sign the app with an Apple Developer certificate. A stable signature
|
||||
means macOS never invalidates the permission on updates. Currently out of scope.
|
||||
|
||||
### Adding SSH key to a new laptop (first time only)
|
||||
|
||||
```bash
|
||||
ssh-copy-id "speaker ready"@192.168.32.1XX
|
||||
```
|
||||
|
||||
Run once per laptop before deploying.
|
||||
|
||||
---
|
||||
|
||||
### Manual deploy reference
|
||||
|
||||
The script covers the normal case. For one-off fixes or if the script isn't available:
|
||||
|
||||
```bash
|
||||
# Detect arch
|
||||
ssh "speaker ready"@192.168.32.103 "uname -m"
|
||||
# x86_64 → darwin-x64 | arm64 → darwin-arm64
|
||||
|
||||
# Copy .app (Intel example):
|
||||
scp -r builds/aether_launcher-darwin-x64/aether_launcher.app \
|
||||
"speaker ready"@192.168.32.103:/Applications/aether_launcher.app
|
||||
|
||||
# Write seed.json:
|
||||
ssh "speaker ready"@192.168.32.103 "cat > ~/seed.json" << 'EOF'
|
||||
{
|
||||
"event_device_id": "DEVICE_ID_FOR_THIS_LAPTOP",
|
||||
"aether_api_key": "YOUR_API_KEY",
|
||||
"primary_api_base_url": "https://api.oneskyit.com",
|
||||
"backup_api_base_url": "https://bak-api.oneskyit.com",
|
||||
"onsite_api_base_url": null
|
||||
}
|
||||
EOF
|
||||
|
||||
# Verify:
|
||||
ssh "speaker ready"@192.168.32.103 "cat ~/seed.json"
|
||||
```
|
||||
|
||||
`event_device_id` values by laptop — see the **Device Reference** table below.
|
||||
|
||||
---
|
||||
|
||||
## 📋 Device Reference
|
||||
|
||||
| Laptop | IP Address | event_device_id | Notes |
|
||||
|--------|------------------|-----------------|--------------------------|
|
||||
| 01 | 192.168.32.101 | tFLL1fLQfnk | |
|
||||
| 02 | 192.168.32.102 | rpbfunVPEzw | |
|
||||
| 03 | 192.168.32.103 | 1EPfPX8kfw8 | |
|
||||
| 04 | 192.168.32.104 | zvgyLM5yieU | |
|
||||
| 05 | 192.168.32.105 | QOc046GoeSc | |
|
||||
| 06 | 192.168.32.106 | 2o8j6eb0L6s | |
|
||||
| 07 | 192.168.32.107 | Oa1tlxPEVSQ | |
|
||||
| 08 | 192.168.32.108 | fY4yznpUZ48 | |
|
||||
| 09 | 192.168.32.109 | YlgGCyjo9bY | |
|
||||
| 10 | 192.168.32.110 | GcTnFsp1mHI | |
|
||||
| 11 | 192.168.32.111 | 6z88m9oEZio | |
|
||||
| 12 | 192.168.32.112 | EggJqL2kWkA | |
|
||||
| 13 | 192.168.32.113 | O11eckHFdVE | |
|
||||
| 14 | 192.168.32.114 | reI0SecUEhI | |
|
||||
| 15 | 192.168.32.115 | crozxT8mA44 | |
|
||||
| 16 | 192.168.32.116 | 0nP4VZsvr2Q | |
|
||||
| 17 | 192.168.32.117 | Gm2gNqPGzLA | |
|
||||
| 19 | 192.168.32.119 | 6tpukvRVugU | (no laptop 18) |
|
||||
| x20 | 192.168.32.120 | rwLYnKUNd1M | old 04, spare/retired |
|
||||
|
||||
`aether_api_key`: all laptops share a single key per event deployment. The key is created in
|
||||
the Aether admin panel before the show and deleted after. Check `builds/seed.json` for the
|
||||
current key, or create a new one in Aether (Core → Accounts or Events → Devices API key section)
|
||||
before each deployment.
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
The application requires a `seed.json` file to identify the device and connect to the Aether API.
|
||||
|
||||
### 1. Seed Configuration
|
||||
|
||||
**Location: `~/seed.json`** (user's home directory — external to the app bundle by design)
|
||||
|
||||
This file is intentionally kept outside the application bundle so it can be edited per-device
|
||||
without re-signing or repackaging the app. On macOS this is `/Users/speaker ready/seed.json`.
|
||||
|
||||
Seed file format:
|
||||
```json
|
||||
{
|
||||
"event_device_id": "tFLL1fLQfnk",
|
||||
"aether_api_key": "YOUR_API_KEY",
|
||||
"primary_api_base_url": "https://api.oneskyit.com",
|
||||
"backup_api_base_url": "https://bak-api.oneskyit.com",
|
||||
"onsite_api_base_url": null
|
||||
}
|
||||
```
|
||||
|
||||
`event_device_id` is the `id_random` from the Aether `event_device` record for that physical
|
||||
laptop — see the Device Reference table above. `aether_api_key` is a shared key created per
|
||||
event deployment and deleted after the show.
|
||||
|
||||
### 2. Development Setup
|
||||
```bash
|
||||
npm install
|
||||
npm start # Compiles TypeScript (tsc) then launches Electron
|
||||
```
|
||||
|
||||
### 3. File Cache Layout
|
||||
|
||||
Presentation files are cached locally under `hash_prefix_length`-char subdirectories (default: 2):
|
||||
```text
|
||||
[local_file_cache_path]/
|
||||
4a/
|
||||
4a228ef8ac1a...sha256hash...file
|
||||
1d/
|
||||
1d720916a831...sha256hash...file
|
||||
```
|
||||
|
||||
**Important:** `hash_prefix_length` must be consistent. If it changes, files in old directories
|
||||
become orphaned and will be re-downloaded. The default is `2` and should not be changed unless
|
||||
explicitly coordinated across all devices.
|
||||
|
||||
## 🌉 The Native Bridge (`aetherNative`)
|
||||
|
||||
The bridge is exposed to the renderer via `contextBridge`. It can be accessed in the web UI via `window.aetherNative`.
|
||||
|
||||
**Design principle:** The Electron app is a thin OS primitive layer. Business logic (which script
|
||||
runs for which file type, how to sequence operations, etc.) belongs in the SvelteKit/Svelte side
|
||||
where it can be changed without a rebuild and redeployment. Electron handlers should rarely need
|
||||
to change.
|
||||
|
||||
### File Cache
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `check_cache({cache_root, hash, hash_prefix_length?, verify_hash?})` | Checks if a file exists in the hashed cache. `verify_hash: true` re-hashes the file to confirm integrity. |
|
||||
| `download_to_cache({url, cache_root, hash, api_key, account_id, hash_prefix_length?})` | Streams a file from the API into the hashed cache. Verifies SHA-256 integrity before finalizing. |
|
||||
| `copy_from_cache_to_temp({cache_root, hash, temp_root, filename, hash_prefix_length?})` | **Preferred primitive.** Copies cached file to temp dir with original filename. Returns `{ success, path }`. The Svelte caller decides what to do next. |
|
||||
| `launch_from_cache({cache_root, hash, temp_root, filename, hash_prefix_length?, native_template?})` | Combines copy + launch. The Svelte side resolves the Launch Profile to a single `native_template` string (AppleScript or `shell:` prefixed command). If no template is supplied, it returns an error. |
|
||||
|
||||
### Shell & OS
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `run_cmd({cmd, timeout?, return_stdout?})` | Async shell command execution. |
|
||||
| `run_cmd_sync({cmd})` | Synchronous shell command execution. |
|
||||
| `run_osascript(script)` | **Hardened.** Runs AppleScript via temp `.scpt` file — handles multi-line scripts and paths with spaces/special characters correctly. macOS only. |
|
||||
| `open_folder(path)` | Opens a directory in Finder / system file manager. |
|
||||
| `open_local_file_v2(path)` | Opens a file with its default OS application. |
|
||||
| `open_external({url, app?})` | Opens a URL in Chrome, Firefox, or the default browser. |
|
||||
| `kill_processes({process_name_li})` | Terminates processes by name. macOS/Linux: `pkill -f`. Windows: `taskkill /F`. |
|
||||
|
||||
### Presentations (Phase 5 — legacy specialized handlers)
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `launch_presentation({path, app?, os?})` | Platform-aware launcher. Resolves `[home]`/`[tmp]` placeholders. Hardened (2026-05-11): AppleScript written to temp `.scpt` file, same as `run_osascript`. For new flows prefer `copy_from_cache_to_temp` + `run_osascript` for full control. |
|
||||
| `control_presentation({app, action})` | Slide navigation (`next`/`prev`/`start`/`stop`) for PowerPoint or Keynote via AppleScript. macOS only. |
|
||||
|
||||
### System Management (Phase 5)
|
||||
|
||||
| Method | Description |
|
||||
| --- | --- |
|
||||
| `get_device_config()` | Returns hydrated device config injected at startup from `seed.json` + API. |
|
||||
| `get_device_info()` | Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths. |
|
||||
| `window_control({action, value?})` | Electron window: maximize, minimize, restore, close, fullscreen, kiosk, devtools, reload. |
|
||||
| `set_wallpaper({path?, url?, url_external?, display?, api_key?, account_id?})` | Sets desktop wallpaper. Accepts a local `path` or downloads from `url` (cached to `~/Library/Caches/OSIT/wallpaper/`). `url_external` sets a separate image on the projector/second display. `display`: `'all'` (default) \| `'primary'` \| `'external'`. macOS only in production; Linux returns a dev-mode preview payload without applying. |
|
||||
| `power_control({action})` | Shutdown, reboot, or sleep. macOS + Linux. Requires sudo for shutdown/reboot. |
|
||||
| `set_display_layout({mode, configStr?})` | Mirror/extend displays. macOS only. **Primary path:** bundled `display_control` binary (native CoreGraphics, no Homebrew). **Fallback:** `displayplacer` — used when `display_control` binary is absent, or when a `configStr` override is set. `configStr` is an optional manual override (full `displayplacer` config string) stored in `event_device.data_json` for per-device tuning. Build `display_control` once via `scripts/build-display-control.sh` on a Mac and commit `resources/bin/display_control`. |
|
||||
| `manage_recording({action, options?})` | Screen recording via bundled `aperture` binary. macOS only. |
|
||||
| `update_app(args)` | **Stub.** Downloads update package but does not install. Not functional. |
|
||||
| `list_tools()` | Returns a self-describing manifest of all available bridge functions. |
|
||||
|
||||
### Example Usage (preferred composable pattern)
|
||||
|
||||
```typescript
|
||||
import * as native from '$lib/electron/electron_relay';
|
||||
|
||||
// Step 1: copy the cached file to temp and get the resolved path
|
||||
const copy = await native.copy_from_cache_to_temp({
|
||||
cache_root: $ae_loc.local_file_cache_path,
|
||||
hash: event_file_obj.hash_sha256,
|
||||
temp_root: $ae_loc.host_file_temp_path,
|
||||
filename: event_file_obj.filename
|
||||
});
|
||||
if (!copy.success) { /* handle error */ return; }
|
||||
|
||||
// Step 2: run whatever script/command you want with that path
|
||||
// Option A — AppleScript (macOS):
|
||||
await native.run_osascript(`
|
||||
tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
open (POSIX file "${copy.path}")
|
||||
delay 3
|
||||
end tell
|
||||
`);
|
||||
|
||||
// Option B — shell command:
|
||||
await native.run_cmd({ cmd: `open "${copy.path}"` });
|
||||
|
||||
// Option C — use a template from device config (data-driven, no rebuild needed):
|
||||
const template = $ae_loc.native_device?.launch_profiles?.pptx;
|
||||
if (template) {
|
||||
const script = template.replace(/\{\{path\}\}/g, copy.path);
|
||||
await native.run_osascript(script);
|
||||
}
|
||||
```
|
||||
|
||||
### Configurable Launch Profiles (no rebuild needed)
|
||||
|
||||
`launch_profiles` is the Svelte-side map stored in `event_device.data_json.launch_profiles`.
|
||||
`launch_from_cache` receives the resolved `native_template` string, not the profile map.
|
||||
Keys are lowercase extensions (`pptx`, `key`, `pdf`, etc.); `default` is a catch-all.
|
||||
Profiles use `{{path}}` as the file path placeholder.
|
||||
AppleScript strings run via `run_osascript`; prefix with `shell:` for shell commands.
|
||||
|
||||
See `documentation/PROJECT__AE_Events_Launcher_Native_integration.md` Section 8 for full details.
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
- **Preload:** Logic defined in `src/preload/index.ts`.
|
||||
- **Handlers:** OS-level logic in `src/main/shell_handlers.ts`, `src/main/file_handlers.ts`, and `src/main/system_handlers.ts`.
|
||||
- **Types:** Shared TypeScript interfaces in `src/shared/types.ts`.
|
||||
|
||||
### One-Time: Build the `display_control` Binary (macOS)
|
||||
|
||||
The `display_control` binary provides native CoreGraphics display mirror/extend without any Homebrew dependency. It must be compiled on a Mac (Xcode CLT required) and committed to the repo so it is bundled into every packaged app. Produces a universal binary (x86_64 + arm64) that runs on both Intel and Apple Silicon Macs.
|
||||
|
||||
Rebuild only if `scripts/display_control.m` changes.
|
||||
|
||||
#### Option A — From the Linux workstation (preferred)
|
||||
|
||||
Laptop 01 (`192.168.32.101`) is the designated build Mac — Xcode CLT is installed there.
|
||||
The remote build script copies the source over SSH, compiles on that Mac, and pulls the binary back.
|
||||
|
||||
```bash
|
||||
# From the repo root on the workstation:
|
||||
./scripts/remote-build-display-control.sh
|
||||
|
||||
# Override the build Mac IP if needed:
|
||||
./scripts/remote-build-display-control.sh 192.168.32.102
|
||||
```
|
||||
|
||||
The script prints `file resources/bin/display_control` at the end. Confirm both arches appear:
|
||||
```
|
||||
resources/bin/display_control: Mach-O universal binary with 2 architectures: [x86_64] [arm64]
|
||||
```
|
||||
|
||||
Then commit:
|
||||
```bash
|
||||
git add resources/bin/display_control
|
||||
git commit -m "build: update display_control binary (universal)"
|
||||
```
|
||||
|
||||
#### Option B — Directly on a Mac
|
||||
|
||||
If you have repo access on the Mac itself (Xcode CLT required — `xcode-select --install`):
|
||||
|
||||
```bash
|
||||
./scripts/build-display-control.sh
|
||||
# Expected last output line: x86_64 arm64
|
||||
|
||||
# Test with a second display connected:
|
||||
./resources/bin/display_control status
|
||||
./resources/bin/display_control extend
|
||||
./resources/bin/display_control mirror
|
||||
|
||||
git add resources/bin/display_control
|
||||
git commit -m "build: update display_control binary (universal)"
|
||||
```
|
||||
|
||||
Once committed, `brew install displayplacer` is no longer required on any venue Mac. The `displayplacer` fallback remains in the handler only for `configStr` per-device overrides.
|
||||
|
||||
---
|
||||
|
||||
### Build Requirements (System Dependencies)
|
||||
|
||||
`bsdtar` (libarchive) must be present on the build host. It is used by the `postinstall` patch
|
||||
to work around a hard hang in `@electron/packager` 20 on Node 26.
|
||||
|
||||
| OS | Install |
|
||||
| --- | --- |
|
||||
| Arch Linux | `sudo pacman -S libarchive` |
|
||||
| macOS | `brew install libarchive` (or use the system bsdtar included in Xcode CLT) |
|
||||
| Ubuntu/Debian | `sudo apt install libarchive-tools` |
|
||||
|
||||
### Why bsdtar — Node 26 Packaging Bug
|
||||
|
||||
**Symptom:** `npm run package:mac` or `npm run package:linux` prints
|
||||
`Packaging app for platform ... using electron v42.x` then hangs forever (or exits 0 with no output).
|
||||
|
||||
**Root cause:** `yauzl` 2.10.0 (used by `extract-zip` inside `@electron/packager` 20) creates
|
||||
read streams that never emit `data` events under Node 26. The zip extraction call blocks
|
||||
indefinitely.
|
||||
|
||||
**Fix:** `scripts/patch-packager-unzip.js` (run automatically via `postinstall`) replaces the
|
||||
`extractElectronZip` function in `node_modules/@electron/packager/dist/unzip.js` with a
|
||||
`child_process.execSync` call to `bsdtar -xf`. `bsdtar` was chosen over `7z` because `7z`
|
||||
refuses to extract macOS `.app` bundles that contain symlinks chained through other symlinks
|
||||
(e.g. `Electron Framework.framework/Libraries → Versions/Current/Libraries`, where
|
||||
`Versions/Current` is itself a symlink).
|
||||
|
||||
**This patch is re-applied automatically on every `npm install`** via the `postinstall` hook.
|
||||
If a future release of `@electron/packager` or `extract-zip` fixes Node 26 compatibility,
|
||||
remove the `postinstall` line from `package.json` and delete `scripts/patch-packager-unzip.js`.
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {}
|
||||
}
|
||||
16
aether_app_native_electron.code-workspace
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": "."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"git.autofetch": true,
|
||||
"markdownlint.config": {
|
||||
"MD007": false,
|
||||
"MD030": false,
|
||||
"MD004": false,
|
||||
"MD033": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
[Dolphin]
|
||||
Timestamp=2020,1,24,15,51,19
|
||||
Version=4
|
||||
ViewMode=2
|
||||
@@ -1,28 +0,0 @@
|
||||
body {
|
||||
/* min-height: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%; */
|
||||
|
||||
margin: .1em;
|
||||
padding: .1em;
|
||||
}
|
||||
|
||||
section#Main-Body {
|
||||
/* outline: solid thin red; */
|
||||
|
||||
/* min-height: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%; */
|
||||
}
|
||||
|
||||
section#Main-Nav-Menu {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
section#Main-Content {
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
@@ -1,497 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<title>One Sky IT</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="img/favicon.ico">
|
||||
|
||||
<!-- Cascading Style Sheets (CSS) start -->
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous"> -->
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- One Sky IT default custom Cascading Style Sheets (CSS) -->
|
||||
|
||||
<!-- One Sky IT site custom Cascading Style Sheets (CSS) -->
|
||||
<link rel="stylesheet" href="css/native_app.css">
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<!-- Cascading Style Sheets (CSS) end -->
|
||||
|
||||
<!-- JavaScript (JS) start -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.22.0/axios.min.js" integrity="sha512-m2ssMAtdCEYGWXQ8hXVG4Q39uKYtbfaJL5QMTbhl2kc6vYyubrKHhr6aLLXW4ITeXSywQLn1AhsAaqrJl8Acfg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.2.0/socket.io.js" integrity="sha512-WL6WGKMPBiM9PnHRYIn5YEtq0Z8XP4fkVb4qy7PP4vhmYQErJ/dySyXuFIMDf1eEYCXCrQrMJfkNwKc9gsjTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://cdn.rawgit.com/mozilla/localForage/master/dist/localforage.js"></script>
|
||||
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script> -->
|
||||
|
||||
<!-- JavaScript (JS) end -->
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<main id="main_content" class="svelte_target event_launcher_main">
|
||||
</main>
|
||||
|
||||
|
||||
<!-- JavaScript (JS) start -->
|
||||
<!--
|
||||
Using node.js may require this: npm install jquery, add script below.
|
||||
https://stackoverflow.com/questions/22658015/bootstrap-throws-uncaught-error-bootstraps-javascript-requires-jquery
|
||||
-->
|
||||
<!--
|
||||
<script>
|
||||
window.jQuery = window.$ = require('jquery');
|
||||
</script>
|
||||
-->
|
||||
|
||||
<!-- One Sky IT default custom JavaScript (JS) -->
|
||||
|
||||
<!-- One Sky IT site custom JavaScript (JS) -->
|
||||
<script>const app = require('./js/module_app');</script>
|
||||
<script src="js/app_api.js"></script>
|
||||
<script src="js/app_idb.js"></script>
|
||||
<script src="js/app_ui_misc.js"></script>
|
||||
<script src="js/app_ui_sessions.js"></script>
|
||||
<script src="js/app_ui_presentations.js"></script>
|
||||
<script src="js/app_ui_presenters.js"></script>
|
||||
<script src="js/app_ui_files.js"></script>
|
||||
|
||||
<script>
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
|
||||
let app_config = app.load_config();
|
||||
let account_id = app_config.account_id;
|
||||
let event_id = app_config.event_id;
|
||||
let event_location_id = app_config.event_location_id;
|
||||
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: Set display options
|
||||
|
||||
const display_menu_session_times = app_config.display_menu_session_times;
|
||||
const display_session_codes = app_config.display_session_codes;
|
||||
const display_session_badges = app_config.display_session_badges;
|
||||
const display_presentation_codes = app_config.display_presentation_codes; // NOTE: not currently used
|
||||
const display_presentation_badges = app_config.display_presentation_badges;
|
||||
const display_presenter_codes = app_config.display_presenter_codes; // NOTE: not currently used
|
||||
const display_presenter_badges = app_config.display_presenter_badges; // NOTE: not currently used
|
||||
|
||||
|
||||
// END: Set display options
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: API section
|
||||
|
||||
|
||||
const api_token_loop_interval = app_config.api_token_loop_interval;
|
||||
let api_update_period = app_config.idb_event_location_check_period;
|
||||
let api_update_datetime = Date.now();
|
||||
let waiting_on_api_token = false;
|
||||
|
||||
const api_secret_key = app_config.api_secret_key;
|
||||
let api_temporary_token = null;
|
||||
|
||||
let api_base_url = null; // 'http://api.localhost:5001' 'https://api.oneskyit.com'
|
||||
|
||||
if (app_config.use_local_api) {
|
||||
api_base_url = app_config.api_local_base_url; // 'http://api.localhost:5001'
|
||||
} else {
|
||||
api_base_url = app_config.api_remote_base_url; // 'https://api.oneskyit.com'
|
||||
}
|
||||
|
||||
axios.defaults.baseURL = api_base_url;
|
||||
axios.defaults.headers.post['Access-Control-Allow-Origin'] = app_config.access_control_allow_origin; // '*';
|
||||
|
||||
// Axios needs to authenticate first
|
||||
let axios_headers_set = false;
|
||||
|
||||
let app_online = false;
|
||||
//let app_use_cached_data = true;
|
||||
window.addEventListener('online', app.currently_online);
|
||||
window.addEventListener('offline', app.currently_offline);
|
||||
|
||||
var api_token_loop = setInterval(async function() {
|
||||
//console.log('****************** LOOP: API Token Loop ******************');
|
||||
|
||||
if (app_online && api_update_datetime < Date.now()) {
|
||||
//let api_temporary_token_result = api_token_request_async(axios, api_secret_key)
|
||||
api_temporary_token = await api_token_request_async(axios, api_secret_key)
|
||||
.then(function (result) {
|
||||
if (result) {
|
||||
console.log('Setting temporary API token to axios Authorization header: '+result);
|
||||
axios.defaults.headers.common['Authorization'] = `Token ${result}`;
|
||||
//api_temporary_token = result;
|
||||
|
||||
console.log('Axios headers have been set.');
|
||||
axios_headers_set = true;
|
||||
api_update_datetime = Date.now() + api_update_period;
|
||||
return result;
|
||||
} else {
|
||||
console.log('The temporary API token appears to have failed. Are we actually online? Setting the app to offline mode.');
|
||||
console.log(navigator.onLine);
|
||||
app.currently_offline();
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log('Something went wrong while trying to request a temporary API token.');
|
||||
console.log(error);
|
||||
return false;
|
||||
});
|
||||
} else if (app_online) {
|
||||
//console.log('Currently online, but not asking for a new temporary token yet.');
|
||||
} else {
|
||||
console.log('Currently offline. Can not request a new temporary API token.');
|
||||
}
|
||||
}, api_token_loop_interval);
|
||||
|
||||
|
||||
// Placing this online status check after the api_token_loop just in case?
|
||||
if (navigator.onLine) {
|
||||
app.currently_online();
|
||||
} else {
|
||||
app.currently_offline();
|
||||
}
|
||||
|
||||
|
||||
// END: API section
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: Load IDB section
|
||||
|
||||
|
||||
let idb_name = app_config.idb_name;
|
||||
|
||||
var tbl_event = null;
|
||||
var tbl_event_location = null;
|
||||
var tbl_event_session = null;
|
||||
var tbl_event_presentation = null;
|
||||
var tbl_event_presenter = null;
|
||||
var tbl_event_file = null;
|
||||
var tbl_hosted_file = null;
|
||||
|
||||
var tbl_event_key_count = null;
|
||||
var tbl_event_location_key_count = null;
|
||||
var tbl_event_session_key_count = null;
|
||||
var tbl_event_presentation_key_count = null;
|
||||
var tbl_event_presenter_key_count = null;
|
||||
var tbl_event_file_key_count = null;
|
||||
var tbl_hosted_file_key_count = null;
|
||||
|
||||
//const open_tables_loop_interval = 10; // only runs briefly on start up
|
||||
|
||||
const update_idb_loop_interval = app_config.update_idb_loop_interval; // loop forever
|
||||
//const update_render_loop_interval = 10000; // loop forever; only run when the launcher object is safe
|
||||
|
||||
// IDB tables need to be opened
|
||||
let idb_tables_opened = false;
|
||||
|
||||
// check periods and datetimes for update_idb_loop
|
||||
let idb_event_check_period = app_config.idb_event_check_period;
|
||||
let idb_event_check_datetime = Date.now();
|
||||
let idb_event_location_check_period = app_config.idb_event_location_check_period;
|
||||
let idb_event_location_check_datetime = Date.now();
|
||||
let idb_event_session_check_period = app_config.idb_event_session_check_period;
|
||||
let idb_event_session_check_datetime = Date.now();
|
||||
let idb_event_presentation_check_period = app_config.idb_event_presentation_check_period;
|
||||
let idb_event_presentation_check_datetime = Date.now();
|
||||
let idb_event_presenter_check_period = app_config.idb_event_presenter_check_period;
|
||||
let idb_event_presenter_check_datetime = Date.now();
|
||||
let idb_event_file_check_period = app_config.idb_event_file_check_period;
|
||||
let idb_event_file_check_datetime = Date.now();
|
||||
|
||||
|
||||
//let run_update_idb = false;
|
||||
let run_check_file_cache = false;
|
||||
|
||||
let looping_tbl_event = false;
|
||||
let looping_tbl_event_location = false;
|
||||
let looping_tbl_event_session = false;
|
||||
let looping_tbl_event_presentation = false;
|
||||
let looping_tbl_event_presenter = false;
|
||||
let looping_tbl_event_file = false;
|
||||
|
||||
//let idb_tables_have_records = false;
|
||||
|
||||
if (window.indexedDB) {
|
||||
console.log('IndexedDB is supported');
|
||||
|
||||
let load_idb_tables_result = load_idb_tables()
|
||||
.then(async function (result) {
|
||||
console.log('IDB tables have been opened');
|
||||
idb_tables_opened = load_idb_tables_result;
|
||||
|
||||
console.log('Attempting to use cached IDB data...');
|
||||
|
||||
if (tbl_event_key_count
|
||||
&& tbl_event_location_key_count
|
||||
&& tbl_event_session_key_count
|
||||
&& tbl_event_presentation_key_count
|
||||
&& tbl_event_presenter_key_count
|
||||
&& tbl_event_file_key_count) {
|
||||
let render_event_records_result = render_event_records();
|
||||
let render_event_location_records_result = render_event_location_records();
|
||||
let render_event_session_records_result = render_event_session_records()
|
||||
.then(async function (result) {
|
||||
console.log('Rendering session records has finished. Moving to other records.');
|
||||
console.log(result);
|
||||
let render_event_presentation_records_result = await render_event_presentation_records();
|
||||
let render_event_presenter_records_result = await render_event_presenter_records();
|
||||
let render_event_file_records_result = await render_event_file_records();
|
||||
|
||||
return true;
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log('Something went wrong rendering session records.');
|
||||
console.log(error);
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
console.log('One or more of the IDB tables has 0 records. The IDB probably needs to be updated.');
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log('Something went wrong opening the IDB tables.');
|
||||
console.log(error);
|
||||
return false;
|
||||
});
|
||||
|
||||
} else {
|
||||
console.log('IndexedDB is NOT supported');
|
||||
close();
|
||||
}
|
||||
|
||||
|
||||
// END: Load IDB section
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: Update IDB section
|
||||
|
||||
|
||||
var update_idb_loop = setInterval(async function() {
|
||||
//console.log('****************** LOOP: Update IDB ******************');
|
||||
|
||||
if (app_online && axios_headers_set && idb_tables_opened) {
|
||||
if (idb_event_check_datetime < Date.now()) {
|
||||
console.log('**** *** ** * Time to check events * ** *** ****');
|
||||
let v2_account_events = await v2_get_account_events(axios, account_id)
|
||||
.then(function (response) {
|
||||
for (var i in response) {
|
||||
tbl_event.setItem(response[i].id.toString(), response[i]);
|
||||
}
|
||||
idb_event_check_datetime = Date.now() + idb_event_check_period;
|
||||
let render_event_records_result = render_event_records(); // NOTE: v2 idb to ui
|
||||
//update_event_ui = true;
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log('Trying to check events. Something went wrong. Expired token?');
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
if (idb_event_location_check_datetime < Date.now()) {
|
||||
console.log('**** *** ** * Time to check event locations * ** *** ****');
|
||||
let v2_event_locations = await v2_get_event_locations(axios, event_id);
|
||||
for (var i in v2_event_locations) {
|
||||
tbl_event_location.setItem(v2_event_locations[i].id.toString(), v2_event_locations[i]);
|
||||
}
|
||||
idb_event_location_check_datetime = Date.now() + idb_event_location_check_period;
|
||||
let render_event_location_records_result = render_event_location_records(); // NOTE: v2 idb to ui
|
||||
//update_event_location_ui = true;
|
||||
}
|
||||
|
||||
if (idb_event_session_check_datetime < Date.now()) {
|
||||
console.log('**** *** ** * Time to check event sessions * ** *** ****'); // All event sessions, not just location specific.
|
||||
let v2_event_sessions = await v2_get_event_sessions(axios, event_id); // Was v2_get_event_location_sessions
|
||||
//tbl_event_session.clear();
|
||||
for (var i in v2_event_sessions) {
|
||||
tbl_event_session.setItem(v2_event_sessions[i].id.toString(), v2_event_sessions[i]);
|
||||
}
|
||||
idb_event_session_check_datetime = Date.now() + idb_event_session_check_period;
|
||||
let render_event_session_records_result = render_event_session_records(); // NOTE: v2 idb to ui
|
||||
//update_event_session_ui = true;
|
||||
}
|
||||
|
||||
if (idb_event_presentation_check_datetime < Date.now()) {
|
||||
console.log('**** *** ** * Time to check event location presentations * ** *** ****');
|
||||
let v2_event_presentations = await v2_get_event_location_presentations(axios, event_location_id);
|
||||
for (var i in v2_event_presentations) {
|
||||
tbl_event_presentation.setItem(v2_event_presentations[i].id.toString(), v2_event_presentations[i]);
|
||||
}
|
||||
idb_event_presentation_check_datetime = Date.now() + idb_event_presentation_check_period;
|
||||
let render_event_presentation_records_result = await render_event_presentation_records(); // NOTE: v2 idb to ui
|
||||
//update_event_presentation_ui = true;
|
||||
}
|
||||
|
||||
if (idb_event_presenter_check_datetime < Date.now()) {
|
||||
console.log('**** *** ** * Time to check event location presenters * ** *** ****');
|
||||
let v2_event_presenters = await v2_get_event_location_presenters(axios, event_location_id);
|
||||
for (var i in v2_event_presenters) {
|
||||
tbl_event_presenter.setItem(v2_event_presenters[i].id.toString(), v2_event_presenters[i]);
|
||||
}
|
||||
idb_event_presenter_check_datetime = Date.now() + idb_event_presenter_check_period;
|
||||
|
||||
let render_event_presenter_records_result = await render_event_presenter_records(); // NOTE: v2 idb to ui
|
||||
//update_event_presenter_ui = true;
|
||||
}
|
||||
|
||||
if (idb_event_file_check_datetime < Date.now()) {
|
||||
console.log('**** *** ** * Time to check event location files * ** *** ****');
|
||||
|
||||
let v2_event_files = await v2_get_event_files(axios, event_id);
|
||||
for (var i in v2_event_files) {
|
||||
tbl_event_file.setItem(v2_event_files[i].id.toString(), v2_event_files[i]);
|
||||
}
|
||||
|
||||
let v2_event_location_files = await v2_get_event_location_files(axios, event_location_id);
|
||||
for (var i in v2_event_location_files) {
|
||||
tbl_event_file.setItem(v2_event_location_files[i].id.toString(), v2_event_location_files[i]);
|
||||
}
|
||||
|
||||
let v2_event_location_files_sessions = await v2_get_event_location_files_sessions(axios, event_location_id);
|
||||
for (var i in v2_event_location_files_sessions) {
|
||||
tbl_event_file.setItem(v2_event_location_files_sessions[i].id.toString(), v2_event_location_files_sessions[i]);
|
||||
}
|
||||
|
||||
let v2_event_location_files_presentations = await v2_get_event_location_files_presentations(axios, event_location_id);
|
||||
for (var i in v2_event_location_files_presentations) {
|
||||
tbl_event_file.setItem(v2_event_location_files_presentations[i].id.toString(), v2_event_location_files_presentations[i]);
|
||||
}
|
||||
|
||||
let v2_event_location_files_presenters = await v2_get_event_location_files_presenters(axios, event_location_id);
|
||||
for (var i in v2_event_location_files_presenters) {
|
||||
tbl_event_file.setItem(v2_event_location_files_presenters[i].id.toString(), v2_event_location_files_presenters[i]);
|
||||
}
|
||||
|
||||
//run_check_file_cache = true; // Set to true so that the local file cache will be checked against the updated event files
|
||||
idb_event_file_check_datetime = Date.now() + idb_event_file_check_period;
|
||||
|
||||
let render_event_file_records_result = await render_event_file_records(); // NOTE: v2 idb to ui
|
||||
|
||||
let check_file_cache_result = app.check_file_cache()
|
||||
.then(function (result) {
|
||||
if (result) {
|
||||
console.log('Cached files have now been updated.');
|
||||
} else {
|
||||
console.log('Cached files were not updated.');
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
//update_event_file_ui = true;
|
||||
}
|
||||
|
||||
} else if (app_online && idb_tables_opened && !axios_headers_set) {
|
||||
console.log('Online and waiting for the Axios headers to be set.');
|
||||
} else if (app_online && axios_headers_set && !idb_tables_opened) {
|
||||
console.log('Online and Axios headers are set. Waiting for the IDB tables to be opened');
|
||||
} else if (!app_online) {
|
||||
console.log('Waiting to update IDB once online.');
|
||||
} else {
|
||||
console.log('This should not happen.');
|
||||
}
|
||||
}, update_idb_loop_interval);
|
||||
|
||||
|
||||
// END: IDB section
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: Cached file section
|
||||
|
||||
|
||||
const check_file_cache_loop_interval = app_config.check_file_cache_loop_interval; // loop forever; only run after the event_file table has updated
|
||||
//const update_render_loop_interval = app_config.update_render_loop_interval; // loop forever
|
||||
const host_file_cache_path = app_config.host_file_cache_path; // 'file_cache/'
|
||||
|
||||
var check_file_cache_loop = setInterval(async function() {
|
||||
//console.log('****************** LOOP: Check File Cache ******************');
|
||||
|
||||
if (run_check_file_cache) {
|
||||
console.log('FLAG: check_file_cache='+run_check_file_cache);
|
||||
check_file_cache_result = app.check_file_cache();
|
||||
|
||||
check_file_cache_result.then(function (response) {
|
||||
console.log('Cached files have now been updated.');
|
||||
});
|
||||
run_check_file_cache = false;
|
||||
} else {
|
||||
//console.log('Flag to run check_file_cache is set to false');
|
||||
}
|
||||
}, check_file_cache_loop_interval);
|
||||
|
||||
|
||||
// END: Cached file section
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: Check online status section
|
||||
|
||||
|
||||
// NOTE: This might be needed because the navigator.onLine status is not always correct.
|
||||
var check_initial_online_status = setInterval(async function() {
|
||||
//console.log('****************** LOOP: Check Online Status ******************');
|
||||
//console.log('Secondary online status check?');
|
||||
//console.log(navigator.onLine);
|
||||
//clearInterval(check_initial_online_status);
|
||||
/*
|
||||
if (navigator.onLine) {
|
||||
//app.currently_online();
|
||||
} else {
|
||||
//app.currently_offline();
|
||||
}
|
||||
*/
|
||||
}, 3000);
|
||||
|
||||
|
||||
// END: Check online status section
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
// BEGIN: Status clock section
|
||||
|
||||
|
||||
let status_clock = setInterval(function() {
|
||||
//console.log('****************** LOOP: Status Clock ******************');
|
||||
//https://date-fns.org/v1.30.1/docs/format
|
||||
let datetime_string = dateFns.format(new Date(), 'dddd, Do h:mm:ss A'); //dddd, Do hh:mm:ss A
|
||||
document.getElementById('app_datetime').innerHTML = datetime_string;
|
||||
}, 1000);
|
||||
|
||||
|
||||
// END: Status clock section
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<!-- <script src="js/test.js"></script>-->
|
||||
<!-- JavaScript (JS) end -->
|
||||
|
||||
</body>
|
||||
</html>
|
||||
209
app/index.html
@@ -1,209 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<title>One Sky IT's Aether App</title>
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="img/favicon.ico">
|
||||
|
||||
<!-- Cascading Style Sheets (CSS) start -->
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
||||
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<!-- One Sky IT default custom Cascading Style Sheets (CSS) -->
|
||||
|
||||
<!-- One Sky IT site custom Cascading Style Sheets (CSS) -->
|
||||
<link rel="stylesheet" href="http://dev.oneskyit.local:5000/static/css/aether_variables.css">
|
||||
<link rel="stylesheet" href="http://dev.oneskyit.local:5000/static/svelte/build/aether_utilities.css">
|
||||
<link rel="stylesheet" href="http://dev.oneskyit.local:5000/static/css/aether_system.css">
|
||||
<!-- <link rel="stylesheet" href="svelte/build/aether_layout.css"> -->
|
||||
<!-- <link rel="stylesheet" href="css/aether_layout_flow.css"> -->
|
||||
<!-- <link rel="stylesheet" href="css/aether_layout_grid.css"> -->
|
||||
<link rel="stylesheet" href="http://dev.oneskyit.local:5000/static/svelte/build/aether_modules_core.css">
|
||||
<!-- <link rel="stylesheet" href="http://dev.oneskyit.local:5000/static/svelte/build/aether_modules_other.css"> -->
|
||||
|
||||
<link href="http://dev.oneskyit.local:5000/static/svelte/build/bundle.css" rel="stylesheet">
|
||||
<link href="http://dev.oneskyit.local:5000/static/svelte/build/reloading.css" rel="stylesheet">
|
||||
|
||||
<link href="http://dev.oneskyit.local:5000/static/css/base_style_grid_layout_v3.css" rel="stylesheet">
|
||||
<link href="http://dev.oneskyit.local:5000/static/css/base_style_grid_theme_v3.css" rel="stylesheet">
|
||||
|
||||
<link href="http://dev.oneskyit.local:5000/static/svelte/build/event_launcher.css" rel="stylesheet">
|
||||
|
||||
<link rel="stylesheet" href="css/aether_native_app_v3.css">
|
||||
|
||||
<style>
|
||||
</style>
|
||||
<!-- Cascading Style Sheets (CSS) end -->
|
||||
|
||||
<!-- JavaScript (JS) start -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.22.0/axios.min.js" integrity="sha512-m2ssMAtdCEYGWXQ8hXVG4Q39uKYtbfaJL5QMTbhl2kc6vYyubrKHhr6aLLXW4ITeXSywQLn1AhsAaqrJl8Acfg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.2.0/socket.io.js" integrity="sha512-WL6WGKMPBiM9PnHRYIn5YEtq0Z8XP4fkVb4qy7PP4vhmYQErJ/dySyXuFIMDf1eEYCXCrQrMJfkNwKc9gsjTjA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dayjs/1.10.7/dayjs.min.js" integrity="sha512-bwD3VD/j6ypSSnyjuaURidZksoVx3L1RPvTkleC48SbHCZsemT3VKMD39KknPnH728LLXVMTisESIBOAb5/W0Q==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/localforage/1.10.0/localforage.min.js" integrity="sha512-+BMamP0e7wn39JGL8nKAZ3yAQT2dL5oaXWr4ZYlTGkKOaoXM/Yj7c4oy50Ngz5yoUutAG17flueD4F6QpTlPng==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
|
||||
<script src="https://static.oneskyit.com/js/utilities.js"></script>
|
||||
<!-- <script src="js/app_idb.js"></script> -->
|
||||
|
||||
<script>
|
||||
const app = require('./js/aether_native_app_v3');
|
||||
let app_config = app.load_config();
|
||||
console.log(app_config);
|
||||
|
||||
const flask_env = 'development'; // 'development', 'production'
|
||||
const env = 'development'; // 'development', 'production'
|
||||
const app_env = 'development_local'; // 'development_local', 'development_remote'
|
||||
const api_env = 'development_local'; // 'development_local', 'development_remote'
|
||||
const app_mode = 'app'; // null, 'default', 'onsite', 'app'
|
||||
|
||||
let client_account_id = app_config.account_id;
|
||||
console.log(client_account_id);
|
||||
let event_id = app_config.event_id;
|
||||
let event_device_id = app_config.event_device_id;
|
||||
let event_location_id = app_config.event_location_id;
|
||||
|
||||
const page_for = { 'event': event_id, 'event_device': event_device_id, 'event_location': event_location_id }; // Simple key value like object
|
||||
console.log(page_for);
|
||||
|
||||
const host_file_cache_path = app_config.host_file_cache_path; // 'file_cache/'
|
||||
// console.log(host_file_cache_path);
|
||||
const host_file_temp_path = app_config.host_file_temp_path; // 'temp/'
|
||||
// console.log(host_file_temp_path);
|
||||
|
||||
let idb_name = app_config.idb_name;
|
||||
|
||||
// BEGIN: API section
|
||||
|
||||
const access_control_allow_origin = app_config.access_control_allow_origin;
|
||||
const api_secret_key = app_config.api_secret_key;
|
||||
console.log(api_secret_key);
|
||||
let api_temporary_token = null;
|
||||
|
||||
let api_base_url = null;
|
||||
|
||||
if (app_config.use_local_api) {
|
||||
api_base_url = app_config.api_local_base_url; // 'http://api.localhost:5001'
|
||||
} else {
|
||||
api_base_url = app_config.api_remote_base_url; // 'https://api.oneskyit.com'
|
||||
}
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
let app_online = false;
|
||||
//let app_use_cached_data = true;
|
||||
window.addEventListener('online', app.currently_online);
|
||||
window.addEventListener('offline', app.currently_offline);
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
</script>
|
||||
|
||||
<!-- <script defer src="svelte/build/bundle.js" crossorigin></script> -->
|
||||
<script defer src="http://dev.oneskyit.local:5000/static/svelte/build/bundle.js" crossorigin></script>
|
||||
<!-- JavaScript (JS) end -->
|
||||
|
||||
</head>
|
||||
|
||||
<body class="body_container">
|
||||
|
||||
|
||||
<section id="System-Nav-Menu">Site-Nav-Menu</section>
|
||||
<section id="Site-Header">Site-Header</section>
|
||||
<section id="Site-Nav-Menu">Site-Nav-Menu</section>
|
||||
|
||||
<section id="System-Notifications">System-Notifications (and Site-Notifications)</section><!-- and what would be Site-Notifications-->
|
||||
|
||||
<section id="Main-Body" class="main_template_content svelte_target event_launcher_main">
|
||||
</section>
|
||||
|
||||
<section id="Site-Footer">Site-Footer</section>
|
||||
<section id="System-Footer">Site-Footer</section>
|
||||
|
||||
<section id="System-Debug">System-Debug</section>
|
||||
|
||||
|
||||
</body>
|
||||
<!-- JavaScript (JS) start -->
|
||||
|
||||
<!-- One Sky IT default custom JavaScript (JS) -->
|
||||
|
||||
<!-- One Sky IT site custom JavaScript (JS) -->
|
||||
<!-- <script>const app = require('./js/app_v3');</script> -->
|
||||
<!-- <script src="js/app_v3.js"></script> -->
|
||||
<script>
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// BEGIN: Load IDB section
|
||||
// let idb_name = app_config.idb_name;
|
||||
|
||||
// let tbl_event = null;
|
||||
// let tbl_event_location = null;
|
||||
// let tbl_event_session = null;
|
||||
// let tbl_event_presentation = null;
|
||||
// let tbl_event_presenter = null;
|
||||
// let tbl_event_file = null;
|
||||
// let tbl_hosted_file = null;
|
||||
|
||||
// let tbl_event_key_count = null;
|
||||
// let tbl_event_location_key_count = null;
|
||||
// let tbl_event_session_key_count = null;
|
||||
// let tbl_event_presentation_key_count = null;
|
||||
// let tbl_event_presenter_key_count = null;
|
||||
// let tbl_event_file_key_count = null;
|
||||
// let tbl_hosted_file_key_count = null;
|
||||
|
||||
// let load_idb_tables_result = load_idb_tables()
|
||||
// .then(async function (result) {
|
||||
// console.log('IDB tables have been opened');
|
||||
// idb_tables_opened = load_idb_tables_result;
|
||||
|
||||
// console.log('Attempting to use cached IDB data...');
|
||||
|
||||
// if (tbl_event_key_count
|
||||
// && tbl_event_location_key_count
|
||||
// && tbl_event_session_key_count
|
||||
// && tbl_event_presentation_key_count
|
||||
// && tbl_event_presenter_key_count
|
||||
// && tbl_event_file_key_count) {
|
||||
// console.log('********* Not yet sure how this works... ');
|
||||
// } else {
|
||||
// console.log('One or more of the IDB tables has 0 records. The IDB probably needs to be updated.');
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// console.log(tbl_event);
|
||||
// console.log(tbl_event_location);
|
||||
// console.log(tbl_event_session);
|
||||
// console.log(tbl_event_presentation);
|
||||
// console.log(tbl_event_presenter);
|
||||
// console.log(tbl_event_file);
|
||||
// console.log(tbl_hosted_file);
|
||||
// })
|
||||
// .catch(function (error) {
|
||||
// console.log('Something went wrong opening the IDB tables.');
|
||||
// console.log(error);
|
||||
// return false;
|
||||
// });
|
||||
|
||||
/* ***** **** *** ** * ### * ** *** **** ***** */
|
||||
</script>
|
||||
|
||||
|
||||
|
||||
</html>
|
||||
@@ -1,390 +0,0 @@
|
||||
'use strict';
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { ipcRenderer } = require('electron');
|
||||
|
||||
let home_directory = require('os').homedir();
|
||||
console.log('Home: '+home_directory);
|
||||
|
||||
let tmp_directory = require('os').tmpdir();
|
||||
console.log('Temporary: '+tmp_directory);
|
||||
|
||||
let config = null;
|
||||
|
||||
exports.load_config = function () {
|
||||
console.log('*** Electron framework: load_config() ***');
|
||||
console.log('CWD: '+process.cwd());
|
||||
|
||||
// let home_directory = require('os').homedir();
|
||||
// console.log('Home: '+home_directory);
|
||||
|
||||
// let tmp_directory = require('os').tmpdir();
|
||||
// console.log('Temporary: '+tmp_directory);
|
||||
|
||||
// let config = null;
|
||||
let config_directory = null;
|
||||
let default_config_path = path.join(process.cwd(),'config.json.default');
|
||||
let config_path = null;
|
||||
|
||||
// Set the config path for macOS or Linux
|
||||
if (os.platform == 'darwin') {
|
||||
config_directory = path.join(home_directory, 'Library/Application Support/OSIT');
|
||||
console.log('macOS config directory: '+config_directory);
|
||||
} else if (os.platform == 'linux') {
|
||||
config_directory = path.join(home_directory, '.config/OSIT');
|
||||
console.log('Linux config directory: '+config_directory);
|
||||
}
|
||||
|
||||
// Look for the config file and copy the default if not found.
|
||||
if (fs.existsSync(config_directory)) {
|
||||
console.log('Config: '+config_directory);
|
||||
config_path = path.join(config_directory, 'config.json');
|
||||
} else {
|
||||
fs.mkdirSync(config_directory);
|
||||
console.log('Config directory created: '+config_directory);
|
||||
|
||||
//default_config_path = path.join(process.cwd(),'config.json.default');
|
||||
config_path = path.join(config_directory, 'config.json');
|
||||
fs.copyFileSync(default_config_path, config_path);
|
||||
console.log('Default config file copied: '+config_directory);
|
||||
}
|
||||
|
||||
// Attempt to open the config file. The preferred location is based on the OS's config directory.
|
||||
if (fs.existsSync(config_path)) {
|
||||
console.log('Config path: '+config_path);
|
||||
console.log('Config file (config.json) found under '+config_directory+'.');
|
||||
|
||||
config = JSON.parse(fs.readFileSync(config_path));
|
||||
console.log('Config file read.');
|
||||
} else if (!fs.existsSync(config_path)) {
|
||||
fs.copyFileSync(default_config_path, config_path);
|
||||
console.log('Default config file copied: '+config_directory);
|
||||
|
||||
config = JSON.parse(fs.readFileSync(config_path));
|
||||
console.log('Config file read.');
|
||||
} else if (fs.existsSync('config.json')) {
|
||||
//fs.copyFileSync(default_config_path, config_path);
|
||||
//console.log('Default config file copied: '+config_directory);
|
||||
|
||||
config = JSON.parse(fs.readFileSync('config.json'));
|
||||
console.log('Config file (config.json) not found under '+config_directory+'. Using config in CWD.');
|
||||
console.log('Config file read.');
|
||||
|
||||
//console.log('Config file (config.json) not found under '+config_directory+'. Using config in CWD.');
|
||||
//config = JSON.parse(fs.readFileSync('config.json'));
|
||||
} else {
|
||||
//close();
|
||||
}
|
||||
|
||||
config.host_file_cache_path = config.host_file_cache_path.replace('[home]', home_directory);
|
||||
config.host_file_cache_path = config.host_file_cache_path.replace('[tmp]', tmp_directory);
|
||||
console.log(config.host_file_cache_path);
|
||||
|
||||
config.host_file_temp_path = config.host_file_temp_path.replace('[home]', home_directory);
|
||||
config.host_file_temp_path = config.host_file_temp_path.replace('[tmp]', tmp_directory);
|
||||
console.log(config.host_file_temp_path);
|
||||
|
||||
let import_config_to_ipc_result = ipcRenderer.invoke('import_config', config).then((result) => {
|
||||
console.log('IPC import config finished');
|
||||
console.log(result);
|
||||
return true;
|
||||
})
|
||||
|
||||
//console.log(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
// Check for local file
|
||||
// Updated 2022-03-10
|
||||
exports.check_local_file = async function ({local_file_path, filename}) {
|
||||
console.log('*** Electron framework export: check_local_file() ***');
|
||||
// console.log('Check for local file');
|
||||
console.log(`Local File Path: ${local_file_path}; Filename: ${filename}`);
|
||||
|
||||
let full_local_file_path = path.join(local_file_path, filename);
|
||||
console.log(full_local_file_path);
|
||||
|
||||
if (fs.existsSync(full_local_file_path)) {
|
||||
console.log(`Local file exists: ${full_local_file_path}`);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Check local hash file cache
|
||||
// Updated 2022-03-09
|
||||
exports.check_hash_file_cache = async function ({host_file_cache_path, hash}) {
|
||||
console.log('*** Electron framework export: check_hash_file_cache() ***');
|
||||
// console.log('Check local hash file cache');
|
||||
console.log(`Host File Cache Path: ${host_file_cache_path}; Hash: ${hash}`);
|
||||
|
||||
let hash_filename = `${hash}.file`;
|
||||
|
||||
let hash_file_cache_path = path.join(host_file_cache_path, hash_filename);
|
||||
// console.log(hash_file_cache_path);
|
||||
|
||||
if (fs.existsSync(hash_file_cache_path)) {
|
||||
console.log(`Hashed file exists in cache: ${hash_file_cache_path}`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`Hashed file not found in cache: ${hash_file_cache_path}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Download hash file to cache
|
||||
// Updated 2022-03-09
|
||||
exports.download_hash_file_to_cache = async function ({host_file_cache_path, event_file_id=null, hash=null}) {
|
||||
console.log('*** Electron framework export: download_hash_file_to_cache() ***');
|
||||
// console.log('Download hash file to cache');
|
||||
console.log(`Host File Cache Path: ${host_file_cache_path}; Event File ID: ${event_file_id}; Hash: ${hash}`);
|
||||
|
||||
let endpoint = `/event/file/${event_file_id}/download`;
|
||||
|
||||
let hash_filename = `${hash}.file`;
|
||||
let hash_file_cache_path = path.join(host_file_cache_path, hash_filename);
|
||||
// console.log(hash_file_cache_path);
|
||||
|
||||
let download_file_result = await ipcRenderer.invoke('download_file', api_base_url, endpoint, hash_file_cache_path).then((result) => {
|
||||
console.log('IPC download file process finished');
|
||||
// console.log(result);
|
||||
return true;
|
||||
});
|
||||
|
||||
// console.log(download_file_result);
|
||||
// console.log('End: download_hash_file_to_cache()');
|
||||
if (download_file_result) {
|
||||
console.log('File downloaded successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('File was not downloaded successfully');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Open cached hash file after copying to temp directory
|
||||
// Updated 2022-03-09
|
||||
exports.open_hash_file_to_temp = async function ({host_file_cache_path, hash, host_file_temp_path, filename}) {
|
||||
console.log('*** Electron framework export: open_hash_file_to_temp() ***');
|
||||
// console.log('Open cached hash file after copying to temp directory');
|
||||
console.log(`Host File Cache Path: ${host_file_cache_path}; Hash: ${hash}; Host File Temp Path: ${host_file_temp_path}; Filename: ${filename}`);
|
||||
|
||||
let open_hash_file_to_temp_result = await ipcRenderer.invoke('open_hash_file_to_temp', host_file_cache_path, hash, host_file_temp_path, filename).then((result) => {
|
||||
console.log('IPC open hash file to temp finished');
|
||||
console.log(result);
|
||||
return true;
|
||||
})
|
||||
|
||||
// let result = await ipcRenderer.send('open_local_file', host_file_cache_path, hash, host_file_temp_path, filename);
|
||||
// console.log(result);
|
||||
|
||||
console.log(open_hash_file_to_temp_result);
|
||||
console.log('End: open_hash_file_to_temp()');
|
||||
if (open_hash_file_to_temp_result) {
|
||||
console.log('File opened successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('File was not opened successfully');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Open local file
|
||||
// Updated 2022-03-10
|
||||
exports.open_local_file = async function ({local_file_path, filename}) {
|
||||
console.log('*** Electron framework export: open_local_file() ***');
|
||||
// console.log('Open local file');
|
||||
console.log(`Local File Path: ${local_file_path}; Filename: ${filename}`);
|
||||
|
||||
// let full_local_file_path = path.join(local_file_path, filename);
|
||||
// console.log(full_local_file_path);
|
||||
|
||||
// if (fs.existsSync(full_local_file_path)) {
|
||||
// console.log(`Local file exists: ${full_local_file_path}`);
|
||||
// // return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
let open_local_file_result = await ipcRenderer.invoke('open_local_file', local_file_path, filename).then((result) => {
|
||||
console.log('IPC open local file finished');
|
||||
console.log(result);
|
||||
return true;
|
||||
})
|
||||
|
||||
console.log(open_local_file_result);
|
||||
console.log('End: open_local_file()');
|
||||
if (open_local_file_result) {
|
||||
console.log('File opened successfully');
|
||||
return true;
|
||||
} else {
|
||||
console.log('File was not opened successfully');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Check local file cache and download from server if needed.
|
||||
// Updated 2022-03-09
|
||||
// exports.check_file_cache = async function ({host_file_cache_path, event_file_id, hash}) {
|
||||
exports.check_file_cache = async function ({host_file_cache_path, event_file_id, hash}) {
|
||||
console.log('*** Electron framework export: check_file_cache() ***');
|
||||
// console.log('Check local file cache and download from server if needed.');
|
||||
console.log(`Host File Cache Path: ${host_file_cache_path}; Event File ID: ${event_file_id}; Hash: ${hash}`);
|
||||
|
||||
// NOTE: event_file_id is the event_file.id_random or event_file.event_file_id_random
|
||||
let hash_filename = hash+'.file';
|
||||
|
||||
let save_path = path.join(host_file_cache_path, hash_filename);
|
||||
console.log(save_path);
|
||||
|
||||
if (fs.existsSync(save_path)) {
|
||||
console.log('Hashed file cache already exists: '+save_path);
|
||||
return true;
|
||||
} else {
|
||||
console.log('Hashed file not found in local cache. Downloading file: '+save_path);
|
||||
let endpoint = `/event/file/${event_file_id}/download`;
|
||||
let result = await ipcRenderer.send('download_file', api_base_url, endpoint, save_path); // Must download file using main node.js thread.
|
||||
console.log(result);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.once('download_file_reply', function(event, response){
|
||||
console.log(response);
|
||||
return response;
|
||||
})
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// await ipcRenderer.once('download_file_reply', function(event, response){
|
||||
// console.log(response);
|
||||
// return response;
|
||||
// });
|
||||
|
||||
// result.then(function (response) {
|
||||
// console.log('Downloaded!!!???');
|
||||
// return true;
|
||||
// }).catch(function (error) {
|
||||
// console.log(error);
|
||||
// return false;
|
||||
// });
|
||||
|
||||
// return result;
|
||||
|
||||
// console.log(result);
|
||||
// if (result) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Check local file cache and download from server if needed. Must use IPC to Main to download file. Set a Promise to wait for download_file_reply.
|
||||
// Updated 2022-03-09
|
||||
async function check_file_cache({host_file_cache_path, event_file_id, hash}) {
|
||||
console.log('*** Electron framework: check_file_cache() ***');
|
||||
// console.log('Check local file cache and download from server if needed.');
|
||||
console.log(`Host File Cache Path: ${host_file_cache_path}; Event File ID: ${event_file_id}; Hash: ${hash}`);
|
||||
|
||||
// NOTE: event_file_id is the event_file.id_random or event_file.event_file_id_random
|
||||
let hash_filename = hash+'.file';
|
||||
|
||||
let save_path = path.join(host_file_cache_path, hash_filename);
|
||||
console.log(save_path);
|
||||
|
||||
if (fs.existsSync(save_path)) {
|
||||
console.log('Hashed file cache already exists: '+save_path);
|
||||
return true;
|
||||
} else {
|
||||
console.log('Hashed file not found in local cache. Downloading file: '+save_path);
|
||||
let endpoint = `/event/file/${event_file_id}/download`;
|
||||
let result = await ipcRenderer.send('download_file', api_base_url, endpoint, save_path); // Must download file using main node.js thread.
|
||||
console.log(result);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
ipcRenderer.once('download_file_reply', function(event, response){
|
||||
console.log(response);
|
||||
return response;
|
||||
})
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
// await ipcRenderer.once('download_file_reply', function(event, response){
|
||||
// console.log(response);
|
||||
// return response;
|
||||
// });
|
||||
|
||||
// result.then(function (response) {
|
||||
// console.log('Downloaded!!!???');
|
||||
// return true;
|
||||
// }).catch(function (error) {
|
||||
// console.log(error);
|
||||
// return false;
|
||||
// });
|
||||
|
||||
// return result;
|
||||
|
||||
// console.log(result);
|
||||
// if (result) {
|
||||
// return true;
|
||||
// } else {
|
||||
// return false;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// IPC to Main: Open local file cache if available. Copy to temp directory with given filename first.
|
||||
// Updated 2022-03-09
|
||||
async function open_local_file({host_file_cache_path, hash, host_file_temp_path, filename}) {
|
||||
console.log('*** Electron framework: open_local_file() ***');
|
||||
// console.log('Open local file cache if available. Copy to temp directory with given filename first.');
|
||||
console.log(`Host File Cache Path: ${host_file_cache_path}; Hash: ${hash}; Host File Temp Path: ${host_file_temp_path}; Filename: ${filename}`);
|
||||
|
||||
console.log(host_file_cache_path);
|
||||
console.log(hash);
|
||||
console.log(filename);
|
||||
|
||||
let result = await ipcRenderer.send('open_local_file', host_file_cache_path, hash, host_file_temp_path, filename);
|
||||
console.log(result);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
exports.check_file_cache_and_open_local_file = async function ({host_file_cache_path, event_file_id, hash, host_file_temp_path, filename}) {
|
||||
console.log('*** Electron framework: check_file_cache_and_open_local_file() ***');
|
||||
console.log('Checking the local file cache against the remote server and then opening the local file.');
|
||||
|
||||
let check_file_cache_result = check_file_cache({host_file_cache_path: host_file_cache_path, event_file_id: event_file_id, hash: hash});
|
||||
console.log(check_file_cache_result);
|
||||
|
||||
if (check_file_cache_result) {
|
||||
let open_local_file_result = open_local_file({host_file_cache_path: host_file_cache_path, hash: hash, host_file_temp_path: host_file_temp_path, filename: filename});
|
||||
console.log(open_local_file_result);
|
||||
|
||||
return open_local_file_result;
|
||||
}
|
||||
|
||||
ipcRenderer.once('download_file_reply', function(event, response){
|
||||
console.log(response);
|
||||
|
||||
let open_local_file_result = open_local_file({host_file_cache_path: host_file_cache_path, hash: hash, host_file_temp_path: host_file_temp_path, filename: filename});
|
||||
console.log(open_local_file_result);
|
||||
|
||||
return open_local_file_result;
|
||||
})
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 17 KiB |
@@ -1 +0,0 @@
|
||||
launcher reset
|
||||
@@ -1 +0,0 @@
|
||||
test txt
|
||||
@@ -1,878 +0,0 @@
|
||||
html {
|
||||
/* Parent Layout */
|
||||
/* Box Model */
|
||||
/* Positioning */
|
||||
/*height: 100%;*/
|
||||
/* Display */
|
||||
}
|
||||
|
||||
body {
|
||||
/* Parent Layout */
|
||||
/* Box Model */
|
||||
/* Positioning */
|
||||
margin-bottom: 2.25rem; /* full height */
|
||||
/* margin: 0; */
|
||||
/* padding: 0; */
|
||||
/* Display */
|
||||
}
|
||||
|
||||
nav {
|
||||
}
|
||||
|
||||
|
||||
/* *** BEGIN *** Layout *** System Menu (root menu) *** */
|
||||
|
||||
|
||||
/* *** END *** Layout *** System Menu (root menu) *** */
|
||||
|
||||
|
||||
|
||||
|
||||
/* Aether System Styles */
|
||||
/* Most of this should not be overridden by the client unless using something like an iframe for specific content. */
|
||||
|
||||
|
||||
/* *** BEGIN *** System *** General *** */
|
||||
/* *** END *** System *** General *** */
|
||||
|
||||
|
||||
/* *** BEGIN *** System *** System Menu (root menu) *** */
|
||||
.system_menu {
|
||||
z-index: 1040; /* Bootstrap's modal background is also z-index: 1040 */
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
|
||||
/*height: .1rem;*/
|
||||
height: 2.68rem;
|
||||
/*height: 6.12rem;*/
|
||||
width: 100vw;
|
||||
max-width: 1440px; /*100vw*/
|
||||
|
||||
transform: translateX(-50%);
|
||||
|
||||
margin: 0 0;
|
||||
margin-bottom: .1rem;
|
||||
|
||||
padding-top: .03rem;
|
||||
padding-left: .03rem;
|
||||
padding-right: .03rem;
|
||||
padding-bottom: .03rem;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
/*background: linear-gradient(to right, rgba(220,220,220,.9) 25%, rgba(128,128,128,.8) 50%, rgba(220,220,220,.9) 75%);*/
|
||||
|
||||
border: solid thick transparent;
|
||||
|
||||
border-top: none;
|
||||
border-left: solid thick hsl(var(--hue-green),var(--saturation-least),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-green),var(--saturation-least),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-green),var(--saturation-least),var(--lum-darkest));
|
||||
border-bottom: none;
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--hue-green),var(--saturation-least),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
/* NOTE: transition when no longer hovering */
|
||||
transition-property: height;
|
||||
transition-delay: 4s;
|
||||
transition-duration: .1s;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
.system_menu:hover {
|
||||
z-index: 1051; /* Bootstrap's modal background is z-index: 1040 */
|
||||
|
||||
border-left: solid thick hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image: linear-gradient(to right, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest).1) 0%, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest),.9) 3%, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest),.9) 97%, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest),.1) 100%);
|
||||
|
||||
|
||||
height: 5.33rem;
|
||||
|
||||
/* NOTE: transition when hover starts */
|
||||
transition-property: height;
|
||||
transition-delay: 1.5s;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
.system_menu_clicked {
|
||||
z-index: 1051; /* Bootstrap's modal background is z-index: 1040 */
|
||||
|
||||
border-left: solid thick hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image: linear-gradient(to right, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest).1) 0%, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest),.9) 3%, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest),.9) 97%, hsla(var(--hue-yellow), var(--saturation-less), var(--lum-lightest),.1) 100%);
|
||||
|
||||
|
||||
height: 5.33rem;
|
||||
|
||||
/* NOTE: transition when hover starts */
|
||||
transition-property: height;
|
||||
transition-delay: 0s;
|
||||
transition-duration: .15s;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
|
||||
.system_menu_begin {
|
||||
/*border: solid thin red;*/
|
||||
align-self: flex-end;
|
||||
|
||||
display: flex;
|
||||
/*align-content: center;*/
|
||||
align-items: center;
|
||||
/*justify-content: center;*/
|
||||
|
||||
height: 100%;
|
||||
padding: 0 .25rem;
|
||||
|
||||
text-shadow: 0 0 .25rem hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),1), 0 0 .75rem hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),1), 0 0 1rem hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),1);
|
||||
|
||||
color: black;
|
||||
|
||||
/*background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),.9) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),.9) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),0) 100%);*/
|
||||
}
|
||||
.system_menu_begin:hover {
|
||||
}
|
||||
|
||||
.system_menu_center {
|
||||
/*border: solid thin blue;*/
|
||||
align-self: flex-end;
|
||||
flex: auto;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/*background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-mid),.1) 25%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-mid),.8) 50%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-mid),.9) 75%);*/
|
||||
}
|
||||
|
||||
.system_menu_end {
|
||||
/*border: solid thin red;*/
|
||||
align-self: flex-end;
|
||||
|
||||
display: flex;
|
||||
/*align-content: center;*/
|
||||
align-items: center;
|
||||
/*justify-content: center;*/
|
||||
|
||||
height: 100%;
|
||||
padding: 0 .25rem;
|
||||
|
||||
text-shadow: 0 0 .25rem white, 0 0 .75rem hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),1), 0 0 1rem hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),1);
|
||||
|
||||
color: black;
|
||||
|
||||
/*background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.6) 20%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.6) 80%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 100%);*/
|
||||
}
|
||||
.system_menu_end:hover {
|
||||
}
|
||||
|
||||
.system_menu_hidden {
|
||||
border-bottom: none;
|
||||
align-self: flex-end;
|
||||
flex: auto;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
margin: 0;
|
||||
padding: .1rem .75rem;
|
||||
|
||||
/*background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.9) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.9) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 100%);*/
|
||||
}
|
||||
.system_menu_hidden:hover {
|
||||
/*border-bottom: solid thin gray;*/
|
||||
}
|
||||
|
||||
.system_menu_visible {
|
||||
border-bottom: none;
|
||||
/*border-bottom: solid thin gray;*/
|
||||
align-self: flex-end;
|
||||
flex: auto;
|
||||
width: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
margin: 0;
|
||||
padding: .1rem .75rem;
|
||||
|
||||
/*background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.9) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.9) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 100%);*/
|
||||
}
|
||||
.system_menu_visible:hover {
|
||||
/*border-bottom: solid thin gray;*/
|
||||
}
|
||||
|
||||
.system_menu_name {
|
||||
/*border: dashed thin blue;*/
|
||||
/*flex: auto;*/
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-begin;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
padding: 0rem .5rem;
|
||||
|
||||
background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.8) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.8) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 100%);
|
||||
}
|
||||
|
||||
.system_menu_name img {
|
||||
max-height: 1rem;
|
||||
}
|
||||
|
||||
.system_menu_information {
|
||||
/*border: dashed thin green;*/
|
||||
/*flex: auto;*/
|
||||
align-self: flex-end;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
margin: 0;
|
||||
padding: 0rem .5rem;
|
||||
|
||||
background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.8) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.8) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),0) 100%);
|
||||
}
|
||||
.system_menu_information:hover {
|
||||
/*background-color: gray;*/
|
||||
|
||||
/*background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),.9) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),.9) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lighter),0) 100%);*/
|
||||
|
||||
background-image: linear-gradient(to right, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.0) 0%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.97) 3%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.97) 97%, hsla(var(--hue-blue), var(--saturation-least), var(--lum-lightest),.0) 100%);
|
||||
|
||||
}
|
||||
|
||||
|
||||
.system_menu_float {
|
||||
z-index: 1040;
|
||||
position: fixed;
|
||||
top: 2.75rem;
|
||||
right: .1rem;
|
||||
|
||||
background-color: rgba(220,175,175,.8);
|
||||
|
||||
padding: .1rem .5rem;
|
||||
|
||||
/*position: fixed; top: 0; right: 0; */
|
||||
}
|
||||
|
||||
/* Set style based on the user's permissions */
|
||||
.system_menu.user_super {
|
||||
border-left: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.system_menu.user_manager {
|
||||
border-left: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--hue-orange),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.system_menu.user_administrator {
|
||||
border-left: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--hue-yellow),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.system_menu.user_verified {
|
||||
border-left: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--hue-yellow),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.system_menu.user_authenticated {
|
||||
border-left: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-right: solid thick hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-bottom: solid thin hsl(var(--hue-red),var(--saturation-most),var(--lum-darkest));
|
||||
border-image:
|
||||
linear-gradient(
|
||||
to top,
|
||||
hsl(var(--hue-green),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
) 1;
|
||||
|
||||
background-image:
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsl(var(--hue-cyan),var(--saturation-most),var(--lum-darkest)),
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.system_menu .user_status_options {
|
||||
font-size: .8rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
/* *** END *** System *** System Menu (root menu) *** */
|
||||
|
||||
|
||||
/* *** BEGIN *** System *** User Login *** */
|
||||
.user_login .account__name--container {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.user_login .account__none--container {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: darkred;
|
||||
}
|
||||
|
||||
.user_login .show_password, .user_change_password .show_password {
|
||||
font-size: 1.1rem;
|
||||
line-height: 3.0rem;
|
||||
color: darkred;
|
||||
cursor: zoom-in; /*progress*/
|
||||
}
|
||||
|
||||
.user_login .show_password:hover, .user_change_password .show_password:hover {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
/* *** END *** System *** User Login *** */
|
||||
|
||||
|
||||
/* *** BEGIN *** System *** System Footer *** */
|
||||
/* system_footer or system_footer or system_status is the global (root) footer or status bar */
|
||||
.system_footer {
|
||||
position: fixed;
|
||||
/*width: 100vw;*/
|
||||
bottom: 0;
|
||||
|
||||
margin: 0 0;
|
||||
padding: 0 .5rem;
|
||||
|
||||
color: gray;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.system_footer .footer_left {
|
||||
float: left;
|
||||
}
|
||||
.system_footer .footer_right {
|
||||
float: right;
|
||||
}
|
||||
/* *** END *** System *** System Footer *** */
|
||||
|
||||
|
||||
/* *** BEGIN *** System *** Debug *** */
|
||||
.view_debug {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.hidden_debug {
|
||||
display: none;
|
||||
|
||||
position: fixed;
|
||||
bottom: 1.5rem;
|
||||
right: 0;
|
||||
background: pink;
|
||||
border: dashed thin pink;
|
||||
margin: .25rem .25rem .25rem;
|
||||
padding: .25rem;
|
||||
opacity: .5;
|
||||
|
||||
font-size: .7rem;
|
||||
|
||||
overflow-y: scroll;
|
||||
|
||||
max-height: 96.75vh;
|
||||
|
||||
z-index: 1050; /* Bootstrap 4 sticky-top class has a z-index of 1020 */
|
||||
}
|
||||
|
||||
.hidden_debug:hover {
|
||||
z-index: 1051;
|
||||
border: solid thin pink;
|
||||
opacity: .95;
|
||||
}
|
||||
/* *** END *** System *** Debug *** */
|
||||
|
||||
|
||||
/* BEGIN: Global System Classes */
|
||||
|
||||
|
||||
/* BEGIN: Main section of the page layout */
|
||||
|
||||
.site_header {
|
||||
padding-top: .5rem;
|
||||
padding-bottom: .5rem;
|
||||
margin-bottom: .25rem;
|
||||
|
||||
max-height: 15rem; /* Just in case something is added that is very high. */
|
||||
|
||||
width: 95%;
|
||||
max-width: 1400px;
|
||||
}
|
||||
|
||||
.site_header img {
|
||||
max-width: 100%;
|
||||
max-height: 8rem; /* Just in case something is added that is very high. */
|
||||
/*border: solid thin #aaa;*/
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
/* primary_menu is generally the site_menu */
|
||||
.primary_menu {
|
||||
z-index: 1040; /* Bootstrap's modal background is also z-index: 1040 */
|
||||
/*padding-top: 2rem;*/
|
||||
|
||||
width: 95%;
|
||||
max-width: 1400px; /* 2048px or 2560px is 2K */
|
||||
margin-bottom: .25rem;
|
||||
|
||||
padding-right: 1.75rem;
|
||||
padding-left: 1.75rem;
|
||||
|
||||
background-image: linear-gradient(to right, rgba(255,255,255,.7) 0%, rgba(255,255,255,.95) 3%, rgba(255,255,255,.95) 97%, rgba(255,255,255,.7) 100%);
|
||||
}
|
||||
.primary_menu:hover {
|
||||
z-index: 1041; /* Bootstrap's modal background is also z-index: 1040 */
|
||||
}
|
||||
|
||||
|
||||
.flash_messages {
|
||||
width: 95%;
|
||||
max-width: 1400px; /* 2048px or 2560px is 2K */
|
||||
margin-bottom: .25rem;
|
||||
|
||||
padding-top: .25rem;
|
||||
padding-right: 1.75rem;
|
||||
padding-bottom: .25rem;
|
||||
padding-left: 1.75rem;
|
||||
|
||||
background-image: linear-gradient(to right, #ffff80 0%, #ffffe1 2%, #ffffe1 98%, #ffff80 100%);
|
||||
/*border: solid thin #ffff80;*/
|
||||
}
|
||||
|
||||
.flash_messages ul {
|
||||
margin: .25rem .25rem .25rem .25rem;
|
||||
padding: .25rem .25rem .25rem .25rem;
|
||||
}
|
||||
|
||||
.flash_messages li {
|
||||
list-style-type: circle;
|
||||
}
|
||||
|
||||
.flash_messages li.info {
|
||||
/*background-color: green;*/
|
||||
}
|
||||
|
||||
.flash_messages .flashes {
|
||||
}
|
||||
|
||||
.flash_messages.show_dev .debug {
|
||||
}
|
||||
|
||||
/* BEGIN: main_template_content section of the page layout */
|
||||
|
||||
.main_template_container {
|
||||
/*border: dashed medium darkred;
|
||||
* background-color: red;*/
|
||||
|
||||
position: relative;
|
||||
/*display: flex;*/
|
||||
align-items: stretch;
|
||||
|
||||
height: auto;
|
||||
min-height: 60vh;
|
||||
width: 95%;
|
||||
max-width: 1400px; /* 2048px or 2560px is 2K */
|
||||
|
||||
margin: .5rem auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
|
||||
/* secondary_menu is generally the page_menu or object_type_menu */
|
||||
.secondary_menu {
|
||||
/*border: dashed medium darkgreen;
|
||||
* background-color: green;*/
|
||||
|
||||
/*position: relative;*/
|
||||
|
||||
margin-top: .25rem;
|
||||
margin-right: 0rem;
|
||||
margin-bottom: .25rem;
|
||||
margin-left: 0rem;
|
||||
|
||||
padding-top: .25rem;
|
||||
padding-right: 1.75rem;
|
||||
padding-bottom: .25rem;
|
||||
padding-left: 1.75rem;
|
||||
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
background-image: linear-gradient(to right, rgba(255,255,255,.7) 0%, rgba(255,255,255,.95) 3%, rgba(255,255,255,.95) 97%, rgba(255,255,255,.7) 100%);
|
||||
}
|
||||
|
||||
.secondary_menu>.btn-toolbar>.btn-group {
|
||||
margin-left: .1em;
|
||||
margin-right: .1em;
|
||||
}
|
||||
|
||||
.secondary_menu>.btn-toolbar>.btn-group>.btn {
|
||||
margin-left: .05em;
|
||||
margin-right: .05em;
|
||||
/*
|
||||
* border-left: solid thin #aaa;
|
||||
* border-right: solid thin #aaa;*/
|
||||
}
|
||||
|
||||
|
||||
.main_template_content {
|
||||
/*border: dashed medium darkgreen;
|
||||
* background-color: green;*/
|
||||
|
||||
position: relative;
|
||||
/*display: flex;
|
||||
* align-items: stretch;*/
|
||||
|
||||
margin-top: .25rem;
|
||||
margin-right: 0rem; /* Keep 0 to avoid horizontal scroll */
|
||||
margin-bottom: .25rem;
|
||||
margin-left: 0rem; /* Keep 0 to avoid horizontal scroll */
|
||||
padding-top: .5rem;
|
||||
padding-right: 1.75rem;
|
||||
padding-bottom: .5rem;
|
||||
padding-left: 1.75rem;
|
||||
|
||||
height: 100%;
|
||||
min-height: 50vh;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
max-width: 100%;
|
||||
|
||||
/*background: white;*/
|
||||
/*background-image: linear-gradient(to right, #eee 0%, #fff 2%, #fff 98%, #eee 100%);*/
|
||||
background-image: linear-gradient(to right, rgba(255,255,255,.7) 0%, rgba(255,255,255,.95) 3%, rgba(255,255,255,.95) 97%, rgba(255,255,255,.7) 100%);
|
||||
}
|
||||
|
||||
/* END: main_template_content section of the page layout */
|
||||
|
||||
|
||||
/* END: Global System Classes */
|
||||
|
||||
|
||||
/*X-Small devices (portrait phones, less than 576px)*/
|
||||
/*No media query for `xs` since this is the default in Bootstrap*/
|
||||
|
||||
/*x-Small devices (portrait phones, less than 576px)*/
|
||||
@media (max-width: 575px) {
|
||||
.viewport_width {
|
||||
padding-left: .5rem;
|
||||
padding-right: .5rem;
|
||||
background-color: blue;
|
||||
color: white;
|
||||
}
|
||||
.viewport_width::after {
|
||||
content: 'xs';
|
||||
padding-left: .5rem;
|
||||
}
|
||||
|
||||
header.site_header, nav.primary_menu.site_menu, section.flash_messages, div.main_template_container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main_template_container {
|
||||
width: 100%;
|
||||
margin: 0rem auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main_template_content {
|
||||
margin-top: .05rem;
|
||||
margin-right: 0rem; /* Keep 0 to avoid horizontal scroll */
|
||||
margin-bottom: .05rem;
|
||||
margin-left: 0rem; /* Keep 0 to avoid horizontal scroll */
|
||||
padding-top: .0rem;
|
||||
padding-right: .05rem;
|
||||
padding-bottom: .0rem;
|
||||
padding-left: .05rem;
|
||||
}
|
||||
|
||||
footer.system_footer {
|
||||
font-size: .8em;
|
||||
}
|
||||
}
|
||||
|
||||
/*Small devices (landscape phones, 576px and up)*/
|
||||
@media (min-width: 576px) {
|
||||
.viewport_width {
|
||||
padding-left: .5rem;
|
||||
padding-right: .5rem;
|
||||
background-color: green;
|
||||
color: black;
|
||||
}
|
||||
.viewport_width::after {
|
||||
content: 'sm';
|
||||
padding-left: .5rem;
|
||||
}
|
||||
|
||||
header.site_header, nav.primary_menu.site_menu, section.flash_messages, div.main_template_container {
|
||||
width: 97%;
|
||||
}
|
||||
|
||||
.main_template_container {
|
||||
width: 97%;
|
||||
margin: .25rem auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main_template_content {
|
||||
margin-top: .1rem;
|
||||
margin-right: 0rem;
|
||||
margin-bottom: .1rem;
|
||||
margin-left: 0rem;
|
||||
padding-top: .25rem;
|
||||
padding-right: 1.00rem;
|
||||
padding-bottom: .25rem;
|
||||
padding-left: 1.00rem;
|
||||
}
|
||||
|
||||
|
||||
footer.system_footer {
|
||||
font-size: .85em;
|
||||
}
|
||||
}
|
||||
|
||||
/*Medium devices (tablets, 768px and up)*/
|
||||
@media (min-width: 768px) {
|
||||
.viewport_width {
|
||||
padding-left: .5rem;
|
||||
padding-right: .5rem;
|
||||
background-color: yellow;
|
||||
color: black;
|
||||
}
|
||||
.viewport_width::after {
|
||||
content: 'md';
|
||||
padding-left: .5rem;
|
||||
}
|
||||
|
||||
footer.system_footer {
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
/*Large devices (desktops, 992px and up)*/
|
||||
@media (min-width: 992px) {
|
||||
.viewport_width {
|
||||
padding-left: .5rem;
|
||||
padding-right: .5rem;
|
||||
background-color: orange;
|
||||
color: black;
|
||||
}
|
||||
.viewport_width::after {
|
||||
content: 'lg';
|
||||
padding-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*X-Large devices (large desktops, 1200px and up)*/
|
||||
@media (min-width: 1200px) {
|
||||
.viewport_width {
|
||||
padding-left: .5rem;
|
||||
padding-right: .5rem;
|
||||
background-color: red;
|
||||
color: white;
|
||||
}
|
||||
.viewport_width::after {
|
||||
content: 'xl';
|
||||
padding-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/*XX-Large devices (larger desktops, 1400px and up)*/
|
||||
@media (min-width: 1400px) {
|
||||
.viewport_width {
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
.viewport_width::after {
|
||||
content: 'xxl';
|
||||
padding-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
@page {
|
||||
size: Letter; /*Legal*/
|
||||
margin: .5in !important; /* top and bottom margin is set to .41 because of Google Chrome */
|
||||
padding: 0in;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white !important;
|
||||
background-image: none !important;
|
||||
color: black !important;
|
||||
|
||||
margin: .25in !important;
|
||||
padding: .1in !important;
|
||||
|
||||
border: solid thin #eeeeee;
|
||||
}
|
||||
|
||||
.system_menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.system_menu_float {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site_header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.site_menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.flash_messages {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main_template_container {
|
||||
/*border: dashed thin red;*/
|
||||
|
||||
background-color: white !important;
|
||||
background-image: none !important;
|
||||
color: black !important;
|
||||
|
||||
margin: .01in !important;
|
||||
padding: .0in !important;
|
||||
|
||||
min-width: 0 !important;
|
||||
/*width: 0 !important;*/
|
||||
max-width: 100% !important;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.secondary_menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main_template_content {
|
||||
/*border: dotted thin red;*/
|
||||
|
||||
background-color: white !important;
|
||||
background-image: none !important;
|
||||
color: black !important;
|
||||
|
||||
margin: .01in !important;
|
||||
padding: .01in !important;
|
||||
|
||||
min-width: 0 !important;
|
||||
/*width: 0 !important;*/
|
||||
max-width: 100% !important;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.system_footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden_debug {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.page_help {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.page_links {
|
||||
float: right;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
/* This file will trigger the browser to reload quickly */
|
||||
|
||||
/* Component types */
|
||||
/* A section is a main component and should be outlined. It may be displayed as block. */
|
||||
section.component {
|
||||
outline: dashed thin;
|
||||
outline-color: var(--default-color-lighter);
|
||||
}
|
||||
section.component:hover {
|
||||
outline: dashed thin;
|
||||
outline-color: var(--default-color-light);
|
||||
}
|
||||
/* A div is a shared component, block, and should not be styled internally */
|
||||
div.component {
|
||||
outline: dotted thin;
|
||||
outline-color: var(--default-color-lighter);
|
||||
}
|
||||
div.component:hover {
|
||||
outline-color: var(--default-color-light);
|
||||
}
|
||||
/* A span is a shared component, inline, and should not be styled internally */
|
||||
span.component {
|
||||
outline: dotted thin;
|
||||
outline-color: var(--default-color-lighter);
|
||||
}
|
||||
span.component:hover {
|
||||
outline-color: var(--default-color-light);
|
||||
}
|
||||
|
||||
|
||||
.main_template_container {
|
||||
/*outline: dashed medium green;*/
|
||||
}
|
||||
|
||||
|
||||
.main_template_content {
|
||||
/*outline: dashed thin red;*/
|
||||
|
||||
}
|
||||
|
||||
.qr_scanner_form {
|
||||
/*outline: solid thin green;*/
|
||||
}
|
||||
|
||||
.qr_scanner {
|
||||
/*outline: solid thin pink;*/
|
||||
}
|
||||
|
||||
|
||||
.badge_body {
|
||||
/*outline: dotted thin red;*/
|
||||
}
|
||||
|
||||
.badge_person_name {
|
||||
/*outline: dashed thin pink;*/
|
||||
}
|
||||
|
||||
.badge_affiliations_location {
|
||||
/*outline: dashed thin pink;*/
|
||||
}
|
||||
79
config.json
@@ -1,79 +0,0 @@
|
||||
{
|
||||
"account_id": "_XY7DXtc9MY",
|
||||
"event_id": "pjrcghqwert",
|
||||
"event_device_id": "dbgMWS3KEHE",
|
||||
"event_location_id": null,
|
||||
"event_session_id": null,
|
||||
"event_presentation_id": null,
|
||||
"event_presenter_id": null,
|
||||
"event_file_id": null,
|
||||
|
||||
"api_secret_key": "dFP6J9DVj9hUgIMn-fNIqg",
|
||||
"api_remote_base_url": "https://dev-fastapi.oneskyit.com",
|
||||
"api_local_base_url": "http://dev-fastapi.oneskyit.local:5005",
|
||||
"access_control_allow_origin": "*",
|
||||
|
||||
"use_local_api": true,
|
||||
"use_local_db": false,
|
||||
|
||||
"idb_name": "osit",
|
||||
"idb_event_check_period": 120000,
|
||||
"idb_event_location_check_period": 90000,
|
||||
"idb_event_session_check_period": 90000,
|
||||
"idb_event_presentation_check_period": 90000,
|
||||
"idb_event_presenter_check_period": 60000,
|
||||
"idb_event_file_check_period": 30000,
|
||||
|
||||
"host_file_cache_path": "[home]/tmp/OSIT/file_cache",
|
||||
"host_file_cache_check_period": 30000,
|
||||
"host_file_temp_path": "[home]/tmp/OSIT/temp",
|
||||
|
||||
"main_loop_interval": 2000,
|
||||
"api_token_loop_interval": 1000,
|
||||
"api_update_period": 90000,
|
||||
"api_auth_loop_interval": 100,
|
||||
"api_token_update_loop_interval": 120000,
|
||||
|
||||
"open_tables_loop_interval": 10,
|
||||
"update_idb_loop_interval": 10,
|
||||
"idb_to_launcher_loop_interval": 250,
|
||||
"update_render_loop_interval": 1000,
|
||||
"check_file_cache_loop_interval": 250,
|
||||
|
||||
"display_menu_session_times": true,
|
||||
"display_session_codes": true,
|
||||
"display_session_badges": true,
|
||||
"display_presentation_codes": true,
|
||||
"display_presentation_badges": true,
|
||||
"display_presenter_codes": true,
|
||||
"display_presenter_badges": true,
|
||||
|
||||
"display_arrangement": "mirror_and_extend",
|
||||
"display_builtin_resolution": "",
|
||||
"display_builtin_refresh": "",
|
||||
"display_builtin_rotation": "",
|
||||
"display_external_resolution": "",
|
||||
"display_external_refresh": "",
|
||||
"display_external_rotation": "",
|
||||
|
||||
"audio_out_volume": null,
|
||||
"audio_in_volume": null,
|
||||
|
||||
"recording_fps": 30,
|
||||
"recording_show_cursor": true,
|
||||
"recording_highlight_clicks": false,
|
||||
"recording_screen_id": null,
|
||||
"recording_audio_device_id": null,
|
||||
"known_builtin_screen_ids": [69732032, 69733952, 69733248],
|
||||
"known_builtin_audio_device_ids": [ "AppleHDAEngineInput:1B,0,1,0:1", "BuiltInMicrophoneDevice" ],
|
||||
"recording_video_codec": "h264",
|
||||
"recording_path": "[home]/recordings",
|
||||
"recording_base_filename": "recording",
|
||||
"aperture_bin_path": null,
|
||||
"recording_start_datetime": "2019-10-12 01:01:01Z",
|
||||
"recording_stop_datetime": "2019-10-31 23:59:59Z",
|
||||
"recordings_datetime": [
|
||||
{ "start": "2019-10-11T09:50:00.00", "stop": "2019-10-11T10:15:00.00" },
|
||||
{ "start": "2019-10-11T10:50:00.00", "stop": "2019-10-11T11:15:00.00" }
|
||||
]
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"account_id": "",
|
||||
"event_id": "",
|
||||
"event_device_id": "",
|
||||
"event_location_id": "",
|
||||
"event_session_id": "",
|
||||
|
||||
"api_secret_key": "XXXXXX",
|
||||
"api_remote_base_url": "https://dev-fastapi.oneskyit.com",
|
||||
"api_local_base_url": "http://dev-fastapi.oneskyit.local:5005",
|
||||
"access_control_allow_origin": "*",
|
||||
|
||||
"use_local_api": true,
|
||||
"use_local_db": false,
|
||||
|
||||
"idb_name": "osit",
|
||||
"idb_event_check_period": 120000,
|
||||
"idb_event_location_check_period": 90000,
|
||||
"idb_event_session_check_period": 90000,
|
||||
"idb_event_presentation_check_period": 90000,
|
||||
"idb_event_presenter_check_period": 60000,
|
||||
"idb_event_file_check_period": 30000,
|
||||
|
||||
"host_file_cache_path": "[home]/OSIT/file_cache",
|
||||
"host_file_cache_check_period": 30000,
|
||||
"host_file_temp_path": "[home]/OSIT/temp",
|
||||
|
||||
"main_loop_interval": 2000,
|
||||
"api_token_loop_interval": 1000,
|
||||
"api_update_period": 90000,
|
||||
"api_auth_loop_interval": 100,
|
||||
"api_token_update_loop_interval": 120000,
|
||||
|
||||
"open_tables_loop_interval": 10,
|
||||
"update_idb_loop_interval": 10,
|
||||
"idb_to_launcher_loop_interval": 250,
|
||||
"update_render_loop_interval": 1000,
|
||||
"check_file_cache_loop_interval": 250,
|
||||
|
||||
"display_menu_session_times": true,
|
||||
"display_session_codes": true,
|
||||
"display_session_badges": true,
|
||||
"display_presentation_codes": true,
|
||||
"display_presentation_badges": true,
|
||||
"display_presenter_codes": true,
|
||||
"display_presenter_badges": true,
|
||||
|
||||
"display_arrangement": "mirror_and_extend",
|
||||
"display_builtin_resolution": "",
|
||||
"display_builtin_refresh": "",
|
||||
"display_builtin_rotation": "",
|
||||
"display_external_resolution": "",
|
||||
"display_external_refresh": "",
|
||||
"display_external_rotation": "",
|
||||
|
||||
"audio_out_volume": null,
|
||||
"audio_in_volume": null,
|
||||
|
||||
"recording_fps": 30,
|
||||
"recording_show_cursor": true,
|
||||
"recording_highlight_clicks": false,
|
||||
"recording_screen_id": null,
|
||||
"recording_audio_device_id": null,
|
||||
"known_builtin_screen_ids": [69732032, 69733952, 69733248],
|
||||
"known_builtin_audio_device_ids": [ "AppleHDAEngineInput:1B,0,1,0:1", "BuiltInMicrophoneDevice" ],
|
||||
"recording_video_codec": "h264",
|
||||
"recording_path": "[home]/recordings",
|
||||
"recording_base_filename": "recording",
|
||||
"aperture_bin_path": null,
|
||||
"recording_start_datetime": "2019-10-12 01:01:01Z",
|
||||
"recording_stop_datetime": "2019-10-31 23:59:59Z",
|
||||
"recordings_datetime": [
|
||||
{ "start": "2019-10-11T09:50:00.00", "stop": "2019-10-11T10:15:00.00" },
|
||||
{ "start": "2019-10-11T10:50:00.00", "stop": "2019-10-11T11:15:00.00" }
|
||||
]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"api_secret_key": "YWAAk39H2qH0edK6lPH0yg",
|
||||
"api_remote_base_url": "https://api.oneskyit.com",
|
||||
"api_local_base_url": "http://api.localhost:5001",
|
||||
"remote_db_server": "db.oneskyit.com",
|
||||
"remote_db_port": "3306",
|
||||
"remote_db_name": "onesky_ams_test",
|
||||
"remote_db_username": "username_here",
|
||||
"remote_db_password": "password_here",
|
||||
"local_db_server": "db.localhost",
|
||||
"local_db_port": "3306",
|
||||
"local_db_name": "onesky_ams_test",
|
||||
"local_db_username": "username_here",
|
||||
"local_db_password": "password_here",
|
||||
"local_file_cache":"file_cache",
|
||||
"display_arrangement": "mirror_and_extend",
|
||||
"display_builtin_resolution": "",
|
||||
"display_builtin_refresh": "",
|
||||
"display_builtin_rotation": "",
|
||||
"display_external_resolution": "",
|
||||
"display_external_refresh": "",
|
||||
"display_external_rotation": "",
|
||||
"audio_out_volume": "",
|
||||
"audio_in_volume": "",
|
||||
"fps": 30,
|
||||
"show_cursor": true,
|
||||
"highlight_clicks": false,
|
||||
"screen_id": null,
|
||||
"audio_device_id": null,
|
||||
"known_builtin_screen_ids": [69732032, 69733952, 69733248],
|
||||
"known_builtin_audio_device_ids": [ "AppleHDAEngineInput:1B,0,1,0:1", "BuiltInMicrophoneDevice" ],
|
||||
"video_codec": "h264",
|
||||
"recordings_path": "[home]/recordings",
|
||||
"base_filename": "recording",
|
||||
"aperture_bin_path": null,
|
||||
"recording_start_datetime": "2019-10-12 01:01:01Z",
|
||||
"recording_stop_datetime": "2019-10-31 23:59:59Z",
|
||||
"recordings_datetime": [
|
||||
{ "start": "2019-10-11T09:50:00.00", "stop": "2019-10-11T10:15:00.00" },
|
||||
{ "start": "2019-10-11T10:50:00.00", "stop": "2019-10-11T11:15:00.00" }
|
||||
]
|
||||
}
|
||||
246
deploy/deploy.sh
Executable file
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.sh — Deploy Aether Native Launcher to onsite Mac laptops
|
||||
#
|
||||
# USAGE:
|
||||
# ./deploy.sh <num> [num ...] Deploy to one or more laptops (e.g. 03 04 05)
|
||||
# ./deploy.sh all Deploy to all laptops in devices.conf
|
||||
# ./deploy.sh --seed-only <num> Update seed.json only — skip .app copy
|
||||
# ./deploy.sh --seed-only all
|
||||
# ./deploy.sh --build <num> [num ...] Build first (npm run package:mac), then deploy
|
||||
# ./deploy.sh --build all
|
||||
# ./deploy.sh --fix-accessibility <num> [num ...] Re-grant macOS Accessibility permission after .app update
|
||||
# ./deploy.sh --fix-accessibility all (requires NOPASSWD sudo for sqlite3 — see README)
|
||||
#
|
||||
# REQUIRES:
|
||||
# event.env — copy from event.env.example and fill in AETHER_API_KEY
|
||||
# builds/ — pre-built, or use --build to build before deploying
|
||||
#
|
||||
# SSH keys must already be installed on each target (run ssh-copy-id once per laptop).
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
DEVICES_FILE="$SCRIPT_DIR/devices.conf"
|
||||
EVENT_ENV="$SCRIPT_DIR/event.env"
|
||||
SSH_USER="speaker ready"
|
||||
BUILD_DIR="$SCRIPT_DIR/../builds"
|
||||
|
||||
# ── Argument parsing ──────────────────────────────────────────────────────────
|
||||
|
||||
SEED_ONLY=false
|
||||
BUILD_FIRST=false
|
||||
FIX_ACCESSIBILITY=false
|
||||
TARGETS=()
|
||||
# Bundle ID as embedded in Info.plist by electron-packager (no --app-bundle-id override)
|
||||
BUNDLE_ID="com.electron.aetherlauncher"
|
||||
|
||||
usage() {
|
||||
grep '^#' "$0" | grep -v '^#!/' | sed 's/^# \{0,1\}//'
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ $# -eq 0 ]]; then usage; fi
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--seed-only) SEED_ONLY=true ;;
|
||||
--build) BUILD_FIRST=true ;;
|
||||
--fix-accessibility) FIX_ACCESSIBILITY=true ;;
|
||||
--help|-h) usage ;;
|
||||
all) TARGETS+=("all") ;;
|
||||
*) TARGETS+=("$1") ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
if [[ ${#TARGETS[@]} -eq 0 ]]; then
|
||||
echo "ERROR: No targets specified."
|
||||
usage
|
||||
fi
|
||||
|
||||
# ── Load event config ─────────────────────────────────────────────────────────
|
||||
|
||||
if [[ ! -f "$EVENT_ENV" ]]; then
|
||||
echo "ERROR: $EVENT_ENV not found."
|
||||
echo " Copy event.env.example → event.env and fill in AETHER_API_KEY."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# shellcheck source=/dev/null
|
||||
source "$EVENT_ENV"
|
||||
|
||||
: "${AETHER_API_KEY:?event.env must set AETHER_API_KEY}"
|
||||
: "${PRIMARY_API_BASE_URL:?event.env must set PRIMARY_API_BASE_URL}"
|
||||
: "${BACKUP_API_BASE_URL:?event.env must set BACKUP_API_BASE_URL}"
|
||||
|
||||
# Render onsite URL as JSON null or quoted string
|
||||
if [[ -n "${ONSITE_API_BASE_URL:-}" ]]; then
|
||||
ONSITE_JSON="\"$ONSITE_API_BASE_URL\""
|
||||
else
|
||||
ONSITE_JSON="null"
|
||||
fi
|
||||
|
||||
# ── Device list helpers ───────────────────────────────────────────────────────
|
||||
|
||||
# Returns "ip device_id" for a laptop number, or empty if not found
|
||||
lookup_device() {
|
||||
local num="$1"
|
||||
grep -v '^[[:space:]]*#' "$DEVICES_FILE" \
|
||||
| grep -E "^[[:space:]]*${num}[[:space:]]" \
|
||||
| awk '{print $2, $3}' \
|
||||
| head -1
|
||||
}
|
||||
|
||||
# Returns all laptop numbers from devices.conf (first column, non-comment lines)
|
||||
all_device_nums() {
|
||||
grep -v '^[[:space:]]*#' "$DEVICES_FILE" \
|
||||
| grep -v '^[[:space:]]*$' \
|
||||
| awk '{print $1}'
|
||||
}
|
||||
|
||||
# ── Deploy one laptop ─────────────────────────────────────────────────────────
|
||||
|
||||
deploy_laptop() {
|
||||
local num="$1"
|
||||
|
||||
local info
|
||||
info=$(lookup_device "$num")
|
||||
if [[ -z "$info" ]]; then
|
||||
echo " ERROR: Laptop $num not found in devices.conf"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local ip device_id
|
||||
ip=$(echo "$info" | awk '{print $1}')
|
||||
device_id=$(echo "$info" | awk '{print $2}')
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════"
|
||||
echo " Laptop $num · $ip · $device_id"
|
||||
echo "══════════════════════════════════════════════"
|
||||
|
||||
# ── Copy .app ──────────────────────────────────────────────────────────
|
||||
if [[ "$SEED_ONLY" != "true" ]]; then
|
||||
echo " Detecting architecture..."
|
||||
local arch
|
||||
arch=$(ssh "$SSH_USER@$ip" "uname -m" 2>/dev/null) || {
|
||||
echo " ERROR: SSH failed for $ip — is the laptop on the network?"
|
||||
return 1
|
||||
}
|
||||
|
||||
local bundle
|
||||
case "$arch" in
|
||||
x86_64) bundle="$BUILD_DIR/aether_launcher-darwin-x64/aether_launcher.app" ;;
|
||||
arm64) bundle="$BUILD_DIR/aether_launcher-darwin-arm64/aether_launcher.app" ;;
|
||||
*)
|
||||
echo " ERROR: Unknown arch '$arch' on $ip"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$bundle" ]]; then
|
||||
echo " ERROR: Build not found: $bundle"
|
||||
echo " Run: npm run package:mac"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo " Arch: $arch → syncing $(basename "$bundle")..."
|
||||
# rsync --delete syncs contents in-place without removing the top-level .app dir.
|
||||
# This preserves the inode so macOS Aliases and Desktop shortcuts keep working.
|
||||
rsync -a --delete -e ssh "$bundle/" "$SSH_USER@$ip:/Applications/aether_launcher.app/" || {
|
||||
echo " ERROR: rsync failed."
|
||||
return 1
|
||||
}
|
||||
echo " .app synced."
|
||||
else
|
||||
echo " (--seed-only: skipping .app copy)"
|
||||
fi
|
||||
|
||||
# ── Write seed.json ────────────────────────────────────────────────────
|
||||
echo " Writing seed.json..."
|
||||
ssh "$SSH_USER@$ip" "cat > ~/seed.json" <<EOF
|
||||
{
|
||||
"event_device_id": "$device_id",
|
||||
"aether_api_key": "$AETHER_API_KEY",
|
||||
"primary_api_base_url": "$PRIMARY_API_BASE_URL",
|
||||
"backup_api_base_url": "$BACKUP_API_BASE_URL",
|
||||
"onsite_api_base_url": $ONSITE_JSON
|
||||
}
|
||||
EOF
|
||||
|
||||
# ── Accessibility permission ───────────────────────────────────────────
|
||||
if [[ "$FIX_ACCESSIBILITY" == "true" ]]; then
|
||||
echo " Resetting Accessibility permission (tccutil)..."
|
||||
if ssh "$SSH_USER@$ip" "tccutil reset Accessibility $BUNDLE_ID" 2>/dev/null; then
|
||||
echo " tccutil reset OK."
|
||||
else
|
||||
echo " WARNING: tccutil reset failed (non-fatal)."
|
||||
fi
|
||||
|
||||
echo " Granting Accessibility via TCC database (requires NOPASSWD sudo)..."
|
||||
# shellcheck disable=SC2016
|
||||
TCC_SQL="INSERT OR REPLACE INTO access(service,client,client_type,auth_value,auth_reason,auth_version) VALUES('kTCCServiceAccessibility','$BUNDLE_ID',0,2,4,1);"
|
||||
if ssh "$SSH_USER@$ip" "sudo sqlite3 '/Library/Application Support/com.apple.TCC/TCC.db' \"$TCC_SQL\"" 2>/dev/null; then
|
||||
echo " ✓ Accessibility granted."
|
||||
else
|
||||
echo " WARNING: TCC grant failed — manual re-authorization required."
|
||||
echo " See README: macOS Accessibility Permission."
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Verify ─────────────────────────────────────────────────────────────
|
||||
echo " Verifying..."
|
||||
ssh "$SSH_USER@$ip" "cat ~/seed.json"
|
||||
if [[ "$SEED_ONLY" != "true" ]]; then
|
||||
ssh "$SSH_USER@$ip" \
|
||||
"test -d /Applications/aether_launcher.app && echo ' ✓ .app present' || echo ' ✗ .app NOT found'"
|
||||
fi
|
||||
|
||||
echo " ✓ Laptop $num done."
|
||||
return 0
|
||||
}
|
||||
|
||||
# ── Build if requested ───────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$BUILD_FIRST" == "true" ]]; then
|
||||
echo "══════════════════════════════════════════════"
|
||||
echo " Building: npm run package:mac"
|
||||
echo "══════════════════════════════════════════════"
|
||||
(cd "$SCRIPT_DIR/.." && npm run package:mac) || {
|
||||
echo "ERROR: Build failed. Aborting deploy."
|
||||
exit 1
|
||||
}
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# ── Expand "all" target ───────────────────────────────────────────────────────
|
||||
|
||||
EXPANDED_TARGETS=()
|
||||
for t in "${TARGETS[@]}"; do
|
||||
if [[ "$t" == "all" ]]; then
|
||||
while IFS= read -r num; do
|
||||
EXPANDED_TARGETS+=("$num")
|
||||
done < <(all_device_nums)
|
||||
else
|
||||
EXPANDED_TARGETS+=("$t")
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Run deploys ───────────────────────────────────────────────────────────────
|
||||
|
||||
FAILED=()
|
||||
for num in "${EXPANDED_TARGETS[@]}"; do
|
||||
if ! deploy_laptop "$num"; then
|
||||
FAILED+=("$num")
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "══════════════════════════════════════════════"
|
||||
if [[ ${#FAILED[@]} -eq 0 ]]; then
|
||||
echo " All done. ✓"
|
||||
else
|
||||
echo " FAILED: ${FAILED[*]}"
|
||||
echo " Re-run with just those numbers to retry."
|
||||
exit 1
|
||||
fi
|
||||
28
deploy/devices.conf
Normal file
@@ -0,0 +1,28 @@
|
||||
# Aether Native Launcher — Device List
|
||||
# Fields: laptop_num ip_address event_device_id [notes]
|
||||
# Blank lines and lines starting with # are ignored.
|
||||
#
|
||||
# IP pattern: 192.168.32.1XX (XX = zero-padded laptop number)
|
||||
# SSH user: "speaker ready" on all laptops
|
||||
# Find/replace 192.168.32 for other venue network prefixes.
|
||||
#
|
||||
# num ip event_device_id notes
|
||||
01 192.168.32.101 tFLL1fLQfnk
|
||||
02 192.168.32.102 rpbfunVPEzw
|
||||
03 192.168.192.203 1EPfPX8kfw8
|
||||
04 192.168.192.204 zvgyLM5yieU
|
||||
05 192.168.192.205 QOc046GoeSc
|
||||
06 192.168.192.206 2o8j6eb0L6s
|
||||
07 192.168.32.107 Oa1tlxPEVSQ
|
||||
08 192.168.32.108 fY4yznpUZ48
|
||||
09 192.168.32.109 YlgGCyjo9bY
|
||||
10 192.168.32.110 GcTnFsp1mHI
|
||||
# 11 192.168.32.111 6z88m9oEZio
|
||||
# 12 192.168.32.112 EggJqL2kWkA
|
||||
# 13 192.168.32.113 O11eckHFdVE
|
||||
# 14 192.168.32.114 reI0SecUEhI
|
||||
# 15 192.168.32.115 crozxT8mA44
|
||||
# 16 192.168.32.116 0nP4VZsvr2Q
|
||||
# 17 192.168.32.117 Gm2gNqPGzLA
|
||||
# 19 192.168.32.119 6tpukvRVugU
|
||||
# x20 192.168.32.120 rwLYnKUNd1M old 04, spare/retired
|
||||
15
deploy/event.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# event.env — Per-event deployment config for deploy.sh
|
||||
# Copy this file to event.env and fill in the values before deploying.
|
||||
# event.env is gitignored — never commit it (contains the API key).
|
||||
#
|
||||
# AETHER_API_KEY: shared across all laptops for this event deployment.
|
||||
# Create in Aether admin (Core → Accounts or Events → Devices API key section)
|
||||
# before the show. Delete after the show.
|
||||
#
|
||||
# ONSITE_API_BASE_URL: set to the local onsite API if running one (e.g.
|
||||
# http://192.168.32.1/api). Leave blank to use null (cloud-only mode).
|
||||
|
||||
AETHER_API_KEY="your_api_key_here"
|
||||
PRIMARY_API_BASE_URL="https://api.oneskyit.com"
|
||||
BACKUP_API_BASE_URL="https://bak-api.oneskyit.com"
|
||||
ONSITE_API_BASE_URL=""
|
||||
65
dist/main/api_client.js
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.fetchFullConfig = fetchFullConfig;
|
||||
async function fetchFullConfig(seed) {
|
||||
const apiUrls = [
|
||||
seed.onsite_api_base_url,
|
||||
seed.primary_api_base_url,
|
||||
seed.backup_api_base_url
|
||||
].filter(url => url !== null && url !== undefined);
|
||||
let lastError = null;
|
||||
for (const baseUrl of apiUrls) {
|
||||
try {
|
||||
console.log(`Bootstrap: Attempting connection to ${baseUrl}...`);
|
||||
// --- STEP 1: Get Device Config ---
|
||||
const deviceUrl = `${baseUrl}/v3/crud/event_device/${seed.event_device_id}`;
|
||||
const deviceResponse = await fetch(deviceUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-aether-api-key': seed.aether_api_key,
|
||||
'x-no-account-id': 'bypass'
|
||||
},
|
||||
});
|
||||
if (!deviceResponse.ok) {
|
||||
throw new Error(`Device lookup failed (${deviceResponse.status})`);
|
||||
}
|
||||
const deviceResult = await deviceResponse.json();
|
||||
const deviceData = deviceResult.data || deviceResult;
|
||||
// Use 'app_base_url' as the FQDN for the site lookup
|
||||
const fqdn = deviceData.app_base_url || 'native-demo.oneskyit.com';
|
||||
// --- STEP 2: Get Site Context ---
|
||||
const searchUrl = `${baseUrl}/v3/crud/site_domain/search?limit=1`;
|
||||
const siteResponse = await fetch(searchUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-aether-api-key': seed.aether_api_key,
|
||||
'x-account-id': deviceData.account_id_random || deviceData.account_id || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
|
||||
})
|
||||
});
|
||||
if (!siteResponse.ok) {
|
||||
throw new Error(`Site context lookup failed (${siteResponse.status})`);
|
||||
}
|
||||
const siteResult = await siteResponse.json();
|
||||
const siteDomain = (siteResult.data && siteResult.data.length > 0) ? siteResult.data[0] : null;
|
||||
console.log(`Bootstrap Success using ${baseUrl}`);
|
||||
return {
|
||||
...siteDomain,
|
||||
native_device: deviceData,
|
||||
aether_api_key: seed.aether_api_key // Include the key for frontend use
|
||||
};
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Bootstrap failed for ${baseUrl}: `, error);
|
||||
lastError = error;
|
||||
continue; // Try next URL
|
||||
}
|
||||
}
|
||||
console.error('Bootstrap Critical Failure: All API endpoints exhausted.', lastError);
|
||||
return null;
|
||||
}
|
||||
//# sourceMappingURL=api_client.js.map
|
||||
1
dist/main/api_client.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"api_client.js","sourceRoot":"","sources":["../../src/main/api_client.ts"],"names":[],"mappings":";;AAEA,0CAwEC;AAxEM,KAAK,UAAU,eAAe,CAAC,IAAgB;IACpD,MAAM,OAAO,GAAG;QACd,IAAI,CAAC,mBAAmB;QACxB,IAAI,CAAC,oBAAoB;QACzB,IAAI,CAAC,mBAAmB;KACzB,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAa,CAAC;IAE/D,IAAI,SAAS,GAAQ,IAAI,CAAC;IAE1B,KAAK,MAAM,OAAO,IAAI,OAAO,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,OAAO,CAAC,GAAG,CAAC,uCAAuC,OAAO,KAAK,CAAC,CAAC;YAEjE,oCAAoC;YACpC,MAAM,SAAS,GAAG,GAAG,OAAO,yBAAyB,IAAI,CAAC,eAAe,EAAE,CAAC;YAC5E,MAAM,cAAc,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBAC5C,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,kBAAkB,EAAE,IAAI,CAAC,cAAc;oBACvC,iBAAiB,EAAE,QAAQ;iBAC5B;aACF,CAAC,CAAC;YAEH,IAAI,CAAC,cAAc,CAAC,EAAE,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,yBAAyB,cAAc,CAAC,MAAM,GAAG,CAAC,CAAC;YACrE,CAAC;YAED,MAAM,YAAY,GAAG,MAAM,cAAc,CAAC,IAAI,EAAE,CAAC;YACjD,MAAM,UAAU,GAAG,YAAY,CAAC,IAAI,IAAI,YAAY,CAAC;YAErD,qDAAqD;YACrD,MAAM,IAAI,GAAG,UAAU,CAAC,YAAY,IAAI,0BAA0B,CAAC;YAEnE,mCAAmC;YACnC,MAAM,SAAS,GAAG,GAAG,OAAO,qCAAqC,CAAC;YAClE,MAAM,YAAY,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE;gBAC1C,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,kBAAkB,EAAE,IAAI,CAAC,cAAc;oBACvC,cAAc,EAAE,UAAU,CAAC,iBAAiB,IAAI,UAAU,CAAC,UAAU,IAAI,EAAE;iBAC5E;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,GAAG,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;iBAChD,CAAC;aACH,CAAC,CAAC;YAEH,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC;gBACrB,MAAM,IAAI,KAAK,CAAC,+BAA+B,YAAY,CAAC,MAAM,GAAG,CAAC,CAAC;YACzE,CAAC;YAED,MAAM,UAAU,GAAG,MAAM,YAAY,CAAC,IAAI,EAAE,CAAC;YAC7C,MAAM,UAAU,GAAG,CAAC,UAAU,CAAC,IAAI,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;YAE/F,OAAO,CAAC,GAAG,CAAC,2BAA2B,OAAO,EAAE,CAAC,CAAC;YAElD,OAAO;gBACL,GAAG,UAAU;gBACb,aAAa,EAAE,UAAU;gBACzB,cAAc,EAAE,IAAI,CAAC,cAAc,CAAC,mCAAmC;aACxE,CAAC;QAEJ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,IAAI,CAAC,wBAAwB,OAAO,IAAI,EAAE,KAAK,CAAC,CAAC;YACzD,SAAS,GAAG,KAAK,CAAC;YAClB,SAAS,CAAC,eAAe;QAC3B,CAAC;IACH,CAAC;IAED,OAAO,CAAC,KAAK,CAAC,0DAA0D,EAAE,SAAS,CAAC,CAAC;IACrF,OAAO,IAAI,CAAC;AACd,CAAC"}
|
||||
62
dist/main/config_loader.js
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.loadSeedConfig = loadSeedConfig;
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const os = __importStar(require("os"));
|
||||
async function loadSeedConfig() {
|
||||
// For development, we look in the home directory
|
||||
const configPath = path.join(os.homedir(), 'seed.json');
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.log(`Seed config not found at: ${configPath}`);
|
||||
return null;
|
||||
}
|
||||
const data = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = JSON.parse(data);
|
||||
// Basic validation
|
||||
if (!config.event_device_id) {
|
||||
console.error('Invalid seed config: missing event_device_id');
|
||||
return null;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error loading seed config:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=config_loader.js.map
|
||||
1
dist/main/config_loader.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"config_loader.js","sourceRoot":"","sources":["../../src/main/config_loader.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAKA,wCAwBC;AA7BD,uCAAyB;AACzB,2CAA6B;AAC7B,uCAAyB;AAGlB,KAAK,UAAU,cAAc;IAClC,iDAAiD;IACjD,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,CAAC,CAAC;IAExD,IAAI,CAAC;QACH,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,GAAG,CAAC,6BAA6B,UAAU,EAAE,CAAC,CAAC;YACvD,OAAO,IAAI,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QAClD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC;QAE9C,mBAAmB;QACnB,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC;YAC5B,OAAO,CAAC,KAAK,CAAC,8CAA8C,CAAC,CAAC;YAC9D,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,4BAA4B,EAAE,KAAK,CAAC,CAAC;QACnD,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
|
||||
217
dist/main/file_handlers.js
vendored
Normal file
@@ -0,0 +1,217 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.registerFileHandlers = registerFileHandlers;
|
||||
const electron_1 = require("electron");
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const os = __importStar(require("os"));
|
||||
const crypto = __importStar(require("crypto"));
|
||||
const child_process_1 = require("child_process");
|
||||
const axios_1 = __importDefault(require("axios"));
|
||||
const file_utils_1 = require("./file_utils");
|
||||
let endpoints_in_progress = [];
|
||||
function registerFileHandlers() {
|
||||
// Flexible organization: [root]/[prefix_len-char-prefix]/[hash].file
|
||||
function get_organized_hashed_path(root, hash, prefix_len = 2) {
|
||||
const expanded_root = (0, file_utils_1.expandPath)(root);
|
||||
const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8)));
|
||||
const dir = path.join(expanded_root, prefix);
|
||||
if (!fs.existsSync(dir))
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
return path.join(dir, `${hash}.file`);
|
||||
}
|
||||
electron_1.ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
|
||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
if (!fs.existsSync(full_path))
|
||||
return false;
|
||||
if (verify_hash) {
|
||||
try {
|
||||
const file_buffer = fs.readFileSync(full_path);
|
||||
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||
return actual_hash === hash;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
electron_1.ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
|
||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
const tmp_path = `${full_path}.tmp`;
|
||||
if (endpoints_in_progress.includes(url))
|
||||
return { success: true, status: 'in_progress' };
|
||||
// 1. If final file exists, skip
|
||||
if (fs.existsSync(full_path))
|
||||
return { success: true, path: full_path, status: 'exists' };
|
||||
// 2. Handle stale .tmp files (Legacy "Trust No One" pattern)
|
||||
if (fs.existsSync(tmp_path)) {
|
||||
const stats = fs.statSync(tmp_path);
|
||||
const age_ms = Date.now() - stats.mtimeMs;
|
||||
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
|
||||
if (age_ms > 5 * 60 * 1000) {
|
||||
console.log(`Native: Deleting stale temp file (${Math.round(age_ms / 1000)}s old)`);
|
||||
fs.unlinkSync(tmp_path);
|
||||
}
|
||||
else {
|
||||
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
|
||||
}
|
||||
}
|
||||
console.log(`Native: Hardened Download -> ${full_path}`);
|
||||
try {
|
||||
endpoints_in_progress.push(url);
|
||||
const response = await (0, axios_1.default)({
|
||||
method: 'get', url, responseType: 'stream',
|
||||
headers: {
|
||||
'x-aether-api-key': api_key,
|
||||
'x-account-id': account_id || ''
|
||||
}
|
||||
});
|
||||
const writer = fs.createWriteStream(tmp_path);
|
||||
response.data.pipe(writer);
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve());
|
||||
writer.on('error', reject);
|
||||
});
|
||||
// 3. Verify Integrity before renaming (The "Trust No One" Check)
|
||||
const file_buffer = fs.readFileSync(tmp_path);
|
||||
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||
if (actual_hash !== hash) {
|
||||
console.error(`Native: Hash Mismatch! Expected ${hash}, got ${actual_hash}`);
|
||||
fs.unlinkSync(tmp_path);
|
||||
return { success: false, error: 'Integrity check failed: Hash mismatch' };
|
||||
}
|
||||
fs.renameSync(tmp_path, full_path);
|
||||
console.log(`Native: Cache Integrity Verified. File moved to final destination.`);
|
||||
return { success: true, path: full_path };
|
||||
}
|
||||
catch (error) {
|
||||
if (fs.existsSync(tmp_path))
|
||||
fs.unlinkSync(tmp_path);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
finally {
|
||||
endpoints_in_progress = endpoints_in_progress.filter(e => e !== url);
|
||||
}
|
||||
});
|
||||
electron_1.ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, native_template = null }) => {
|
||||
try {
|
||||
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
const expanded_temp = (0, file_utils_1.expandPath)(temp_root);
|
||||
const target = path.join(expanded_temp, filename);
|
||||
console.log(`Native: Launching from Cache -> ${filename}`);
|
||||
if (!fs.existsSync(source)) {
|
||||
return { success: false, error: `File not in cache: ${hash}` };
|
||||
}
|
||||
if (!fs.existsSync(expanded_temp))
|
||||
fs.mkdirSync(expanded_temp, { recursive: true });
|
||||
// 1. Copy the file to temp folder with original name
|
||||
fs.copyFileSync(source, target);
|
||||
// 2a. Data-driven launcher template (no rebuild needed for config changes).
|
||||
// Svelte resolves a Launch Profile to a single native_template string.
|
||||
// Format: AppleScript string with {{path}} placeholder, OR "shell:<cmd> {{path}}"
|
||||
if (!native_template) {
|
||||
return { success: false, error: 'No native template configured for this file' };
|
||||
}
|
||||
const resolved = native_template.replace(/\{\{path\}\}/g, target);
|
||||
if (resolved.startsWith('shell:')) {
|
||||
const cmd = resolved.slice(6).trim();
|
||||
console.log(`Native: Running custom shell template for ${filename}`);
|
||||
return new Promise((resolve_fn) => {
|
||||
(0, child_process_1.exec)(cmd, (err, stdout, stderr) => {
|
||||
if (err)
|
||||
resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
|
||||
else
|
||||
resolve_fn({ success: true, stdout: stdout.trim() });
|
||||
});
|
||||
});
|
||||
}
|
||||
console.log(`Native: Running custom AppleScript template for ${filename}`);
|
||||
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
|
||||
return new Promise((resolve_fn) => {
|
||||
try {
|
||||
fs.writeFileSync(tmp_script_path, resolved.trim());
|
||||
}
|
||||
catch (e) {
|
||||
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (err) => {
|
||||
try {
|
||||
fs.unlinkSync(tmp_script_path);
|
||||
}
|
||||
catch { }
|
||||
if (err)
|
||||
resolve_fn({ success: false, error: err.message });
|
||||
else
|
||||
resolve_fn({ success: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
// Thin primitive: copy a cached file to the temp directory with its original filename,
|
||||
// then return the resolved path. The caller (Svelte side) decides what to do next —
|
||||
// run_osascript, run_cmd, open_local_file, etc.
|
||||
//
|
||||
// This is the preferred building block for custom launch flows. Use launch_from_cache
|
||||
// when Svelte has already resolved a native template; use copy_from_cache_to_temp when
|
||||
// you want full control over what happens after the file lands in temp.
|
||||
electron_1.ipcMain.handle('native:copy-from-cache-to-temp', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2 }) => {
|
||||
try {
|
||||
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
if (!fs.existsSync(source)) {
|
||||
return { success: false, error: `File not in cache: ${hash}` };
|
||||
}
|
||||
const expanded_temp = (0, file_utils_1.expandPath)(temp_root);
|
||||
const target = path.join(expanded_temp, filename);
|
||||
if (!fs.existsSync(expanded_temp))
|
||||
fs.mkdirSync(expanded_temp, { recursive: true });
|
||||
fs.copyFileSync(source, target);
|
||||
console.log(`Native: Copied from cache to temp -> ${target}`);
|
||||
return { success: true, path: target };
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=file_handlers.js.map
|
||||
1
dist/main/file_handlers.js.map
vendored
Normal file
53
dist/main/file_utils.js
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.expandPath = expandPath;
|
||||
exports.getHashedPath = getHashedPath;
|
||||
const os = __importStar(require("os"));
|
||||
const path = __importStar(require("path"));
|
||||
function expandPath(filePath) {
|
||||
if (!filePath)
|
||||
return filePath;
|
||||
// Resolve all instances of [home] and [tmp] using global regex
|
||||
return filePath
|
||||
.replace(/\[home\]/g, os.homedir())
|
||||
.replace(/\[tmp\]/g, os.tmpdir());
|
||||
}
|
||||
function getHashedPath(cacheRoot, hash) {
|
||||
const expandedRoot = expandPath(cacheRoot);
|
||||
const subdirectory = hash.substring(0, 2);
|
||||
return path.join(expandedRoot, subdirectory, `${hash}.file`);
|
||||
}
|
||||
//# sourceMappingURL=file_utils.js.map
|
||||
1
dist/main/file_utils.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"file_utils.js","sourceRoot":"","sources":["../../src/main/file_utils.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAGA,gCAMC;AAED,sCAIC;AAfD,uCAAyB;AACzB,2CAA6B;AAE7B,SAAgB,UAAU,CAAC,QAAgB;IACzC,IAAI,CAAC,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC/B,+DAA+D;IAC/D,OAAO,QAAQ;SACZ,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,OAAO,EAAE,CAAC;SAClC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AACtC,CAAC;AAED,SAAgB,aAAa,CAAC,SAAiB,EAAE,IAAY;IAC3D,MAAM,YAAY,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC;IAC3C,MAAM,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAC1C,OAAO,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,CAAC;AAC/D,CAAC"}
|
||||
120
dist/main/index.js
vendored
Normal file
@@ -0,0 +1,120 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const electron_1 = require("electron");
|
||||
const path = __importStar(require("path"));
|
||||
const os = __importStar(require("os"));
|
||||
const config_loader_1 = require("./config_loader");
|
||||
const api_client_1 = require("./api_client");
|
||||
const shell_handlers_1 = require("./shell_handlers");
|
||||
const file_handlers_1 = require("./file_handlers");
|
||||
const system_handlers_1 = require("./system_handlers");
|
||||
let mainWindow = null;
|
||||
let cachedSeed = null;
|
||||
let cachedFullConfig = null;
|
||||
async function createWindow() {
|
||||
cachedSeed = await (0, config_loader_1.loadSeedConfig)();
|
||||
if (cachedSeed) {
|
||||
cachedFullConfig = await (0, api_client_1.fetchFullConfig)(cachedSeed);
|
||||
}
|
||||
mainWindow = new electron_1.BrowserWindow({
|
||||
width: 1600,
|
||||
height: 900,
|
||||
title: 'OSIT Aether Launcher (Native)',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
let targetUrl = 'http://demo.localhost:5173';
|
||||
if (cachedFullConfig && cachedFullConfig.native_device) {
|
||||
const device = cachedFullConfig.native_device;
|
||||
const eventId = device.event_id_random || device.event_id;
|
||||
const locationId = device.event_location_id_random || device.event_location_id || '';
|
||||
// Use app_base_url from the device record (e.g. bgh.oneskyit.com).
|
||||
// Fall back to localhost only if nothing is configured — never override a real domain.
|
||||
const host = device.app_base_url || 'demo.localhost:5173';
|
||||
// Use https for real domains; localhost dev URLs stay on http
|
||||
const protocol = host.includes('localhost') ? 'http' : 'https';
|
||||
targetUrl = `${protocol}://${host}/events/${eventId}/launcher/${locationId}`;
|
||||
}
|
||||
// Only open DevTools in development (not in a packaged .app build)
|
||||
if (!electron_1.app.isPackaged)
|
||||
mainWindow.webContents.openDevTools();
|
||||
mainWindow.loadURL(targetUrl).catch(() => {
|
||||
mainWindow?.loadURL('https://dev-demo.oneskyit.com/');
|
||||
});
|
||||
mainWindow.on('closed', () => { mainWindow = null; });
|
||||
}
|
||||
(0, shell_handlers_1.registerShellHandlers)();
|
||||
(0, file_handlers_1.registerFileHandlers)();
|
||||
(0, system_handlers_1.registerSystemHandlers)();
|
||||
electron_1.app.on('ready', createWindow);
|
||||
electron_1.app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin')
|
||||
electron_1.app.quit();
|
||||
});
|
||||
electron_1.app.on('activate', () => {
|
||||
if (mainWindow === null)
|
||||
createWindow();
|
||||
});
|
||||
electron_1.ipcMain.handle('get-seed-config', async () => cachedSeed || await (0, config_loader_1.loadSeedConfig)());
|
||||
electron_1.ipcMain.handle('get-device-config', async () => cachedFullConfig);
|
||||
electron_1.ipcMain.handle('get-jwt', async () => null);
|
||||
electron_1.ipcMain.handle('get-device-info', async () => {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const addresses = [];
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const net of interfaces[name]) {
|
||||
if (net.family === 'IPv4' && !net.internal) {
|
||||
addresses.push(net.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
platform: os.platform(),
|
||||
release: os.release(),
|
||||
arch: os.arch(),
|
||||
hostname: os.hostname(),
|
||||
cpus: os.cpus().length,
|
||||
total_mem: os.totalmem(),
|
||||
free_mem: os.freemem(),
|
||||
ip_addresses: addresses,
|
||||
home_directory: os.homedir(),
|
||||
tmp_directory: os.tmpdir()
|
||||
};
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/main/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/main/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAuD;AACvD,2CAA6B;AAC7B,uCAAyB;AACzB,mDAAiD;AACjD,6CAA+C;AAC/C,qDAAyD;AACzD,mDAAuD;AACvD,uDAA2D;AAG3D,IAAI,UAAU,GAAyB,IAAI,CAAC;AAC5C,IAAI,UAAU,GAAsB,IAAI,CAAC;AACzC,IAAI,gBAAgB,GAAQ,IAAI,CAAC;AAEjC,KAAK,UAAU,YAAY;IACzB,UAAU,GAAG,MAAM,IAAA,8BAAc,GAAE,CAAC;IACpC,IAAI,UAAU,EAAE,CAAC;QACf,gBAAgB,GAAG,MAAM,IAAA,4BAAe,EAAC,UAAU,CAAC,CAAC;IACvD,CAAC;IAED,UAAU,GAAG,IAAI,wBAAa,CAAC;QAC7B,KAAK,EAAE,IAAI;QACX,MAAM,EAAE,GAAG;QACX,KAAK,EAAE,+BAA+B;QACtC,cAAc,EAAE;YACd,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC;YACpD,gBAAgB,EAAE,IAAI;YACtB,eAAe,EAAE,KAAK;SACvB;KACF,CAAC,CAAC;IAEH,IAAI,SAAS,GAAG,4BAA4B,CAAC;IAC7C,IAAI,gBAAgB,IAAI,gBAAgB,CAAC,aAAa,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,gBAAgB,CAAC,aAAa,CAAC;QAC9C,MAAM,OAAO,GAAG,MAAM,CAAC,eAAe,IAAI,MAAM,CAAC,QAAQ,CAAC;QAC1D,MAAM,UAAU,GAAG,MAAM,CAAC,wBAAwB,IAAI,MAAM,CAAC,iBAAiB,IAAI,EAAE,CAAC;QACrF,mEAAmE;QACnE,uFAAuF;QACvF,MAAM,IAAI,GAAG,MAAM,CAAC,YAAY,IAAI,qBAAqB,CAAC;QAC1D,8DAA8D;QAC9D,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;QAC/D,SAAS,GAAG,GAAG,QAAQ,MAAM,IAAI,WAAW,OAAO,aAAa,UAAU,EAAE,CAAC;IAC/E,CAAC;IAED,mEAAmE;IACnE,IAAI,CAAC,cAAG,CAAC,UAAU;QAAE,UAAU,CAAC,WAAW,CAAC,YAAY,EAAE,CAAC;IAC3D,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;QACvC,UAAU,EAAE,OAAO,CAAC,gCAAgC,CAAC,CAAC;IACxD,CAAC,CAAC,CAAC;IAEH,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD,CAAC;AAED,IAAA,sCAAqB,GAAE,CAAC;AACxB,IAAA,oCAAoB,GAAE,CAAC;AACvB,IAAA,wCAAsB,GAAE,CAAC;AAEzB,cAAG,CAAC,EAAE,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;AAE9B,cAAG,CAAC,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;IAC/B,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ;QAAE,cAAG,CAAC,IAAI,EAAE,CAAC;AAChD,CAAC,CAAC,CAAC;AAEH,cAAG,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,EAAE;IACtB,IAAI,UAAU,KAAK,IAAI;QAAE,YAAY,EAAE,CAAC;AAC1C,CAAC,CAAC,CAAC;AAEH,kBAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,IAAI,EAAE,CAAC,UAAU,IAAI,MAAM,IAAA,8BAAc,GAAE,CAAC,CAAC;AACpF,kBAAO,CAAC,MAAM,CAAC,mBAAmB,EAAE,KAAK,IAAI,EAAE,CAAC,gBAAgB,CAAC,CAAC;AAClE,kBAAO,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,IAAI,EAAE,CAAC,IAAI,CAAC,CAAC;AAE5C,kBAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,KAAK,IAAI,EAAE;IAC3C,MAAM,UAAU,GAAG,EAAE,CAAC,iBAAiB,EAAE,CAAC;IAC1C,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,UAAU,CAAC,IAAI,CAAE,EAAE,CAAC;YACpC,IAAI,GAAG,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;gBAC3C,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YAC9B,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO;QACL,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;QACvB,OAAO,EAAE,EAAE,CAAC,OAAO,EAAE;QACrB,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE;QACf,QAAQ,EAAE,EAAE,CAAC,QAAQ,EAAE;QACvB,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,MAAM;QACtB,SAAS,EAAE,EAAE,CAAC,QAAQ,EAAE;QACxB,QAAQ,EAAE,EAAE,CAAC,OAAO,EAAE;QACtB,YAAY,EAAE,SAAS;QACvB,cAAc,EAAE,EAAE,CAAC,OAAO,EAAE;QAC5B,aAAa,EAAE,EAAE,CAAC,MAAM,EAAE;KAC3B,CAAC;AACJ,CAAC,CAAC,CAAC"}
|
||||
340
dist/main/shell_handlers.js
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.registerShellHandlers = registerShellHandlers;
|
||||
const electron_1 = require("electron");
|
||||
const child_process_1 = require("child_process");
|
||||
const fs = __importStar(require("fs"));
|
||||
const path = __importStar(require("path"));
|
||||
const os = __importStar(require("os"));
|
||||
const file_utils_1 = require("./file_utils");
|
||||
function registerShellHandlers() {
|
||||
electron_1.ipcMain.handle('native:open-folder', async (event, folderPath) => {
|
||||
const cleanPath = (0, file_utils_1.expandPath)(folderPath);
|
||||
const error = await electron_1.shell.openPath(cleanPath);
|
||||
return { success: !error, error };
|
||||
});
|
||||
electron_1.ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => {
|
||||
const cleanCmd = (0, file_utils_1.expandPath)(cmd);
|
||||
return new Promise((resolve) => {
|
||||
(0, child_process_1.exec)(cleanCmd, { timeout }, (error, stdout, stderr) => {
|
||||
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
|
||||
});
|
||||
});
|
||||
});
|
||||
electron_1.ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => {
|
||||
const cleanCmd = (0, file_utils_1.expandPath)(cmd);
|
||||
try {
|
||||
const stdout = (0, child_process_1.execSync)(cleanCmd).toString();
|
||||
return { success: true, stdout: stdout.trim() };
|
||||
}
|
||||
catch (error) {
|
||||
return { success: false, error: error.message, stderr: error.stderr?.toString() };
|
||||
}
|
||||
});
|
||||
electron_1.ipcMain.handle('native:run-osascript', async (event, script) => {
|
||||
if (os.platform() !== 'darwin')
|
||||
return { success: false, error: 'AppleScript is only available on macOS' };
|
||||
// HARDENED: Write script to a temp .scpt file rather than passing inline via -e.
|
||||
// The old -e approach (`osascript -e "..."`) has two fatal flaws:
|
||||
// 1. It breaks on multi-line scripts.
|
||||
// 2. It breaks on paths containing spaces or special characters (quotes, parens, etc.)
|
||||
// Writing to a file sidesteps both — no shell escaping needed at all.
|
||||
// The .scpt file is deleted immediately after execution (success or failure).
|
||||
// Worst case on crash: a stale .scpt in /tmp, cleared on next OS reboot.
|
||||
//
|
||||
// LEGACY (removed): const cmd = `osascript -e "${script.replace(/"/g, '\\"')}"`;
|
||||
const tmp_script_path = path.join(os.tmpdir(), `ae_osa_${Date.now()}.scpt`);
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
fs.writeFileSync(tmp_script_path, script.trim());
|
||||
}
|
||||
catch (e) {
|
||||
resolve({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (error, stdout, stderr) => {
|
||||
try {
|
||||
fs.unlinkSync(tmp_script_path);
|
||||
}
|
||||
catch { }
|
||||
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
|
||||
});
|
||||
});
|
||||
});
|
||||
electron_1.ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => {
|
||||
console.log(`Native: Killing processes -> `, process_name_li);
|
||||
const results = [];
|
||||
for (const name of process_name_li) {
|
||||
const cmd = os.platform() === 'win32'
|
||||
? `taskkill /F /IM ${name} /T`
|
||||
: `pkill -f ${name}`;
|
||||
try {
|
||||
(0, child_process_1.execSync)(cmd);
|
||||
results.push({ name, success: true });
|
||||
}
|
||||
catch (e) {
|
||||
results.push({ name, success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
return { success: true, results };
|
||||
});
|
||||
electron_1.ipcMain.handle('native:open-local-file-v2', async (event, filePath) => {
|
||||
const cleanPath = (0, file_utils_1.expandPath)(filePath);
|
||||
const error = await electron_1.shell.openPath(cleanPath);
|
||||
return { success: !error, error };
|
||||
});
|
||||
electron_1.ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => {
|
||||
const cleanedPath = (0, file_utils_1.expandPath)(rawPath);
|
||||
console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`);
|
||||
if (os.platform() === 'linux') {
|
||||
const cmd = `libreoffice --impress "${cleanedPath}"`;
|
||||
return new Promise((resolve) => {
|
||||
(0, child_process_1.exec)(cmd, (err, stdout, stderr) => {
|
||||
if (err)
|
||||
resolve({ success: false, error: err.message });
|
||||
else
|
||||
resolve({ success: true, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
if (os.platform() === 'darwin') {
|
||||
let script = '';
|
||||
if (appType === 'keynote') {
|
||||
script = `
|
||||
tell application "Keynote"
|
||||
activate
|
||||
open (POSIX file "${cleanedPath}")
|
||||
delay 1
|
||||
start (front document)
|
||||
end tell
|
||||
`.trim();
|
||||
}
|
||||
else if (appType === 'powerpoint') {
|
||||
script = `
|
||||
tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
open (POSIX file "${cleanedPath}")
|
||||
delay 1
|
||||
run slide show of active presentation
|
||||
end tell
|
||||
`.trim();
|
||||
}
|
||||
if (script) {
|
||||
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
fs.writeFileSync(tmp_script_path, script);
|
||||
}
|
||||
catch (e) {
|
||||
resolve({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
(0, child_process_1.exec)(`osascript "${tmp_script_path}"`, (err) => {
|
||||
try {
|
||||
fs.unlinkSync(tmp_script_path);
|
||||
}
|
||||
catch { }
|
||||
if (err)
|
||||
resolve({ success: false, error: err.message });
|
||||
else
|
||||
resolve({ success: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
const error = await electron_1.shell.openPath(cleanedPath);
|
||||
return { success: !error, error };
|
||||
});
|
||||
electron_1.ipcMain.handle('native:control-presentation', async (event, { app, action }) => {
|
||||
if (os.platform() !== 'darwin')
|
||||
return { success: false, error: 'Presentation control is only available on macOS' };
|
||||
let script = '';
|
||||
if (app === 'powerpoint') {
|
||||
switch (action) {
|
||||
case 'next':
|
||||
script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation';
|
||||
break;
|
||||
case 'prev':
|
||||
script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation';
|
||||
break;
|
||||
case 'start':
|
||||
script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation';
|
||||
break;
|
||||
case 'stop':
|
||||
script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation';
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (app === 'keynote') {
|
||||
switch (action) {
|
||||
case 'next':
|
||||
script = 'tell application "Keynote" to show next';
|
||||
break;
|
||||
case 'prev':
|
||||
script = 'tell application "Keynote" to show previous';
|
||||
break;
|
||||
case 'start':
|
||||
script = 'tell application "Keynote" to start (front document)';
|
||||
break;
|
||||
case 'stop':
|
||||
script = 'tell application "Keynote" to stop';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!script)
|
||||
return { success: false, error: `Unsupported app or action: ${app}/${action}` };
|
||||
return new Promise((resolve) => {
|
||||
(0, child_process_1.exec)(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => {
|
||||
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
|
||||
});
|
||||
});
|
||||
});
|
||||
electron_1.ipcMain.handle('native:list-tools', async () => {
|
||||
return [
|
||||
// --- Config & Info ---
|
||||
{
|
||||
name: 'get_device_config',
|
||||
description: 'Returns hydrated device config injected at startup from seed.json + API.',
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
name: 'get_device_info',
|
||||
description: 'Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths.',
|
||||
params: {}
|
||||
},
|
||||
// --- File Cache ---
|
||||
{
|
||||
name: 'check_cache',
|
||||
description: 'Checks if a file exists in the hashed cache. verify_hash:true re-hashes to confirm integrity.',
|
||||
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number (optional, default 2)', verify_hash: 'boolean (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'download_to_cache',
|
||||
description: 'Streams a file from the API into the hashed cache with SHA-256 integrity check. Cleans stale .tmp files older than 5 min.',
|
||||
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'copy_from_cache_to_temp',
|
||||
description: 'Preferred primitive. Copies a cached file to temp with its original filename. Returns { success, path }. Caller decides what to do next.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'launch_from_cache',
|
||||
description: 'Combines copy_from_cache_to_temp + execute. Runs native_template after copying — AppleScript string with {{path}} placeholder, or "shell:<cmd>" prefix. Returns error if native_template is null.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional)', native_template: 'string | null' }
|
||||
},
|
||||
// --- Shell & OS ---
|
||||
{
|
||||
name: 'open_folder',
|
||||
description: 'Opens a directory in the OS file explorer (Finder on macOS).',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd',
|
||||
description: 'Async shell command execution with timeout.',
|
||||
params: { cmd: 'string', timeout: 'number (optional, default 30000ms)' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd_sync',
|
||||
description: 'Synchronous shell command execution.',
|
||||
params: { cmd: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_osascript',
|
||||
description: 'Hardened AppleScript executor — writes to temp .scpt file, handles multi-line scripts and paths with special characters. macOS only.',
|
||||
params: { script: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'kill_processes',
|
||||
description: 'Terminates processes by name. macOS/Linux: pkill -f. Windows: taskkill /F.',
|
||||
params: { process_name_li: 'string[]' }
|
||||
},
|
||||
{
|
||||
name: 'open_local_file_v2',
|
||||
description: 'Opens a file with its default OS application via shell.openPath.',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'open_external',
|
||||
description: 'Opens a URL in Chrome, Firefox, or the system default browser.',
|
||||
params: { url: 'string', app: 'chrome | firefox | default (optional)' }
|
||||
},
|
||||
// --- Presentations ---
|
||||
{
|
||||
name: 'launch_presentation',
|
||||
description: 'Platform-aware launcher for PowerPoint, Keynote, LibreOffice. Resolves [home]/[tmp] placeholders. Hardened AppleScript (2026-05-11). Prefer copy_from_cache_to_temp + run_osascript for new flows.',
|
||||
params: { path: 'string', app: 'default | powerpoint | keynote (optional)', os_platform: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'control_presentation',
|
||||
description: 'Slide navigation for active PowerPoint or Keynote via AppleScript. macOS only.',
|
||||
params: { app: 'powerpoint | keynote', action: 'next | prev | start | stop' }
|
||||
},
|
||||
// --- System Management ---
|
||||
{
|
||||
name: 'set_wallpaper',
|
||||
description: 'Sets desktop wallpaper. Downloads from url (cached to ~/Library/Caches/OSIT/wallpaper/) or applies local path. url_external targets projector/second display separately. macOS only in production.',
|
||||
params: { path: 'string (optional)', url: 'string (optional)', url_external: 'string (optional)', display: 'all | primary | external (optional, default all)', api_key: 'string (optional)', account_id: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'set_display_layout',
|
||||
description: 'Mirror or extend displays via bundled displayplacer. macOS only. Auto-detects displays when no configStr given; configStr is an optional manual override.',
|
||||
params: { mode: 'mirror | extend', configStr: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'window_control',
|
||||
description: 'Electron window management.',
|
||||
params: { action: 'maximize | unmaximize | minimize | restore | close | fullscreen | kiosk | devtools | reload', value: 'boolean (optional, used by fullscreen/kiosk/devtools)' }
|
||||
},
|
||||
{
|
||||
name: 'power_control',
|
||||
description: 'Shutdown, reboot, or sleep the host machine. macOS + Linux. May require sudo for shutdown/reboot.',
|
||||
params: { action: 'shutdown | reboot | sleep' }
|
||||
},
|
||||
{
|
||||
name: 'manage_recording',
|
||||
description: 'Screen recording via bundled aperture binary. macOS only.',
|
||||
params: { action: 'start | stop | status', options: '{ fps?, audioDeviceId?, output? } (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'update_app',
|
||||
description: 'STUB: Downloads update package but does not install. Not functional.',
|
||||
params: { source: 'url | file', url: 'string (optional)', path: 'string (optional)' }
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=shell_handlers.js.map
|
||||
1
dist/main/shell_handlers.js.map
vendored
Normal file
481
dist/main/system_handlers.js
vendored
Normal file
@@ -0,0 +1,481 @@
|
||||
"use strict";
|
||||
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
var desc = Object.getOwnPropertyDescriptor(m, k);
|
||||
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
||||
desc = { enumerable: true, get: function() { return m[k]; } };
|
||||
}
|
||||
Object.defineProperty(o, k2, desc);
|
||||
}) : (function(o, m, k, k2) {
|
||||
if (k2 === undefined) k2 = k;
|
||||
o[k2] = m[k];
|
||||
}));
|
||||
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
||||
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
||||
}) : function(o, v) {
|
||||
o["default"] = v;
|
||||
});
|
||||
var __importStar = (this && this.__importStar) || (function () {
|
||||
var ownKeys = function(o) {
|
||||
ownKeys = Object.getOwnPropertyNames || function (o) {
|
||||
var ar = [];
|
||||
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
||||
return ar;
|
||||
};
|
||||
return ownKeys(o);
|
||||
};
|
||||
return function (mod) {
|
||||
if (mod && mod.__esModule) return mod;
|
||||
var result = {};
|
||||
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
||||
__setModuleDefault(result, mod);
|
||||
return result;
|
||||
};
|
||||
})();
|
||||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
exports.registerSystemHandlers = registerSystemHandlers;
|
||||
const electron_1 = require("electron");
|
||||
const os = __importStar(require("os"));
|
||||
const path = __importStar(require("path"));
|
||||
const fs = __importStar(require("fs"));
|
||||
const child_process_1 = require("child_process");
|
||||
const axios_1 = __importDefault(require("axios"));
|
||||
const file_utils_1 = require("./file_utils");
|
||||
// Helper to execute shell commands
|
||||
const runExec = (cmd) => {
|
||||
return new Promise((resolve) => {
|
||||
(0, child_process_1.exec)(cmd, (error, stdout, stderr) => {
|
||||
resolve({
|
||||
success: !error,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
error: error ? error.message : undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
let recordingProcess = null;
|
||||
function registerSystemHandlers() {
|
||||
// 1. Window Control
|
||||
electron_1.ipcMain.handle('native:window-control', async (event, { action, value }) => {
|
||||
const win = electron_1.BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win)
|
||||
return { success: false, error: 'No window found' };
|
||||
switch (action) {
|
||||
case 'maximize':
|
||||
win.maximize();
|
||||
break;
|
||||
case 'unmaximize':
|
||||
win.unmaximize();
|
||||
break;
|
||||
case 'minimize':
|
||||
win.minimize();
|
||||
break;
|
||||
case 'restore':
|
||||
win.restore();
|
||||
break;
|
||||
case 'close':
|
||||
win.close();
|
||||
break;
|
||||
case 'devtools':
|
||||
if (value)
|
||||
win.webContents.openDevTools();
|
||||
else
|
||||
win.webContents.closeDevTools();
|
||||
break;
|
||||
case 'kiosk':
|
||||
win.setKiosk(!!value);
|
||||
break;
|
||||
case 'fullscreen':
|
||||
win.setFullScreen(!!value);
|
||||
break;
|
||||
case 'reload':
|
||||
win.reload();
|
||||
break;
|
||||
default: return { success: false, error: `Unknown action: ${action}` };
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
// 2. Set Wallpaper
|
||||
// Supports local path OR URL download. URL images are saved to a stable cache dir
|
||||
// so macOS can reference them persistently after reboot.
|
||||
// display: 'all' (default) | 'primary' (built-in) | 'external' (projector/second screen)
|
||||
// url_external: optional second URL for the external display only.
|
||||
electron_1.ipcMain.handle('native:set-wallpaper', async (event, { path: imagePath, url, url_external, display = 'all', api_key, account_id }) => {
|
||||
// Cache dir: ~/Library/Caches/OSIT/wallpaper on macOS, ~/.cache/osit/wallpaper on Linux.
|
||||
// Using a stable path means macOS keeps the reference across reboots.
|
||||
const wallpaper_cache_dir = os.platform() === 'darwin'
|
||||
? path.join(os.homedir(), 'Library', 'Caches', 'OSIT', 'wallpaper')
|
||||
: path.join(os.homedir(), '.cache', 'osit', 'wallpaper');
|
||||
async function download_wallpaper_image(image_url, basename) {
|
||||
if (!fs.existsSync(wallpaper_cache_dir)) {
|
||||
fs.mkdirSync(wallpaper_cache_dir, { recursive: true });
|
||||
}
|
||||
// Infer extension from URL path, fall back to .jpg
|
||||
let ext = '.jpg';
|
||||
try {
|
||||
const url_path = new URL(image_url).pathname;
|
||||
const inferred = path.extname(url_path).toLowerCase();
|
||||
if (['.jpg', '.jpeg', '.png', '.webp'].includes(inferred)) {
|
||||
ext = inferred === '.jpeg' ? '.jpg' : inferred;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
const dest_path = path.join(wallpaper_cache_dir, basename + ext);
|
||||
const headers = {};
|
||||
if (api_key)
|
||||
headers['x-aether-api-key'] = api_key;
|
||||
if (account_id)
|
||||
headers['x-account-id'] = account_id;
|
||||
try {
|
||||
const response = await (0, axios_1.default)({ method: 'get', url: image_url, responseType: 'stream', headers });
|
||||
const content_type = (response.headers['content-type'] ?? '');
|
||||
if (!content_type.startsWith('image/')) {
|
||||
response.data.destroy();
|
||||
return { success: false, error: `URL did not return an image (Content-Type: ${content_type})` };
|
||||
}
|
||||
const writer = fs.createWriteStream(dest_path);
|
||||
response.data.pipe(writer);
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
const file_size = fs.statSync(dest_path).size;
|
||||
if (file_size === 0) {
|
||||
try {
|
||||
fs.unlinkSync(dest_path);
|
||||
}
|
||||
catch { }
|
||||
return { success: false, error: 'Wallpaper download incomplete (0 bytes)' };
|
||||
}
|
||||
return { success: true, path: dest_path };
|
||||
}
|
||||
catch (e) {
|
||||
return { success: false, error: `Wallpaper download failed: ${e.message}` };
|
||||
}
|
||||
}
|
||||
// HARDENED: write AppleScript to a temp file, same pattern as native:run-osascript.
|
||||
// The old osascript -e approach breaks on paths with spaces or special characters.
|
||||
async function apply_mac_wallpaper(img_path, display_target) {
|
||||
const escaped = img_path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
let script;
|
||||
if (display_target === 'primary') {
|
||||
script = `tell application "System Events"\n\ttell desktop 1\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`;
|
||||
}
|
||||
else if (display_target === 'external') {
|
||||
script = `tell application "System Events"\n\ttell desktop 2\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`;
|
||||
}
|
||||
else {
|
||||
script = `tell application "System Events"\n\ttell every desktop\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`;
|
||||
}
|
||||
const script_path = path.join(os.tmpdir(), `ae_wallpaper_${Date.now()}.scpt`);
|
||||
fs.writeFileSync(script_path, script, 'utf-8');
|
||||
try {
|
||||
return await runExec(`osascript "${script_path}"`);
|
||||
}
|
||||
finally {
|
||||
try {
|
||||
fs.unlinkSync(script_path);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
if (os.platform() === 'darwin') {
|
||||
// Resolve primary image path
|
||||
let primary_path = null;
|
||||
if (imagePath) {
|
||||
const clean = (0, file_utils_1.expandPath)(imagePath);
|
||||
if (!fs.existsSync(clean))
|
||||
return { success: false, error: 'Image file not found' };
|
||||
primary_path = clean;
|
||||
}
|
||||
else if (url) {
|
||||
const result = await download_wallpaper_image(url, 'wallpaper_primary');
|
||||
if (!result.success || !result.path)
|
||||
return { success: false, error: result.error };
|
||||
primary_path = result.path;
|
||||
}
|
||||
if (!primary_path && url_external && display === 'external') {
|
||||
const ext_result = await download_wallpaper_image(url_external, 'wallpaper_external');
|
||||
if (!ext_result.success || !ext_result.path)
|
||||
return { success: false, error: ext_result.error };
|
||||
return await apply_mac_wallpaper(ext_result.path, 'external');
|
||||
}
|
||||
if (!primary_path)
|
||||
return { success: false, error: 'No image source provided' };
|
||||
if (url_external) {
|
||||
// Different images for each display: set primary display first, then external
|
||||
const primary_result = await apply_mac_wallpaper(primary_path, 'primary');
|
||||
if (!primary_result.success)
|
||||
return primary_result;
|
||||
const ext_result = await download_wallpaper_image(url_external, 'wallpaper_external');
|
||||
if (!ext_result.success || !ext_result.path)
|
||||
return { success: false, error: ext_result.error };
|
||||
return await apply_mac_wallpaper(ext_result.path, 'external');
|
||||
}
|
||||
else {
|
||||
return await apply_mac_wallpaper(primary_path, display);
|
||||
}
|
||||
}
|
||||
if (os.platform() === 'linux') {
|
||||
// Dev test mode: never touch the desktop on Linux. Running gsettings during
|
||||
// development would reset the dev workstation monitors on every test cycle.
|
||||
// Return what would have run so the Svelte side can show a debug popup.
|
||||
const would_run = [];
|
||||
const cache_dir = wallpaper_cache_dir;
|
||||
if (!imagePath && !url && !url_external) {
|
||||
return { success: false, error: 'No image source provided' };
|
||||
}
|
||||
if (imagePath) {
|
||||
const clean = (0, file_utils_1.expandPath)(imagePath);
|
||||
if (!fs.existsSync(clean))
|
||||
return { success: false, error: 'Image file not found' };
|
||||
}
|
||||
if (url)
|
||||
would_run.push(`download: ${url}\n → ${path.join(cache_dir, 'wallpaper_primary.jpg')}`);
|
||||
if (url_external)
|
||||
would_run.push(`download: ${url_external}\n → ${path.join(cache_dir, 'wallpaper_external.jpg')}`);
|
||||
if (imagePath) {
|
||||
would_run.push(`gsettings set org.gnome.desktop.background picture-uri "file://${(0, file_utils_1.expandPath)(imagePath)}"`);
|
||||
}
|
||||
else if (url) {
|
||||
would_run.push(`gsettings set org.gnome.desktop.background picture-uri "file://${path.join(cache_dir, 'wallpaper_primary.jpg')}"`);
|
||||
}
|
||||
if (url_external) {
|
||||
would_run.push(`(external display: gsettings has no per-display wallpaper support)`);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
linux_test_mode: true,
|
||||
platform: 'linux',
|
||||
display,
|
||||
url: url ?? null,
|
||||
url_external: url_external ?? null,
|
||||
would_run
|
||||
};
|
||||
}
|
||||
return { success: false, error: 'Platform not supported' };
|
||||
});
|
||||
// 3. Power Control
|
||||
electron_1.ipcMain.handle('native:power-control', async (event, { action }) => {
|
||||
let cmd = '';
|
||||
const isMac = os.platform() === 'darwin';
|
||||
const isLinux = os.platform() === 'linux';
|
||||
if (action === 'shutdown') {
|
||||
if (isMac)
|
||||
cmd = 'shutdown -h now'; // Requires sudo/admin usually
|
||||
if (isLinux)
|
||||
cmd = 'shutdown -h now';
|
||||
}
|
||||
else if (action === 'reboot') {
|
||||
if (isMac)
|
||||
cmd = 'shutdown -r now';
|
||||
if (isLinux)
|
||||
cmd = 'reboot';
|
||||
}
|
||||
else if (action === 'sleep') {
|
||||
if (isMac)
|
||||
cmd = 'pmset sleepnow';
|
||||
if (isLinux)
|
||||
cmd = 'systemctl suspend';
|
||||
}
|
||||
if (!cmd)
|
||||
return { success: false, error: 'Action not supported' };
|
||||
// NOTE: These commands often require root.
|
||||
// For a kiosk, you might configure sudoers to allow this specific command without password.
|
||||
return await runExec(cmd);
|
||||
});
|
||||
// 4. Open External (Browser)
|
||||
electron_1.ipcMain.handle('native:open-external', async (event, { url, app: appName }) => {
|
||||
if (appName === 'chrome') {
|
||||
if (os.platform() === 'darwin') {
|
||||
return await runExec(`open -a "Google Chrome" "${url}"`);
|
||||
}
|
||||
else if (os.platform() === 'linux') {
|
||||
return await runExec(`google-chrome "${url}"`);
|
||||
}
|
||||
}
|
||||
else if (appName === 'firefox') {
|
||||
if (os.platform() === 'darwin') {
|
||||
return await runExec(`open -a "Firefox" "${url}"`);
|
||||
}
|
||||
else if (os.platform() === 'linux') {
|
||||
return await runExec(`firefox "${url}"`);
|
||||
}
|
||||
}
|
||||
// Default system handler
|
||||
await electron_1.shell.openExternal(url);
|
||||
return { success: true };
|
||||
});
|
||||
// 5. Manage Recording (Aperture Wrapper)
|
||||
electron_1.ipcMain.handle('native:manage-recording', async (event, { action, options }) => {
|
||||
if (os.platform() !== 'darwin')
|
||||
return { success: false, error: 'Recording only supported on macOS' };
|
||||
// Path to bundled aperture binary
|
||||
// In dev: ./resources/bin/aperture
|
||||
// In prod: process.resourcesPath/bin/aperture
|
||||
const binPath = electron_1.app.isPackaged
|
||||
? path.join(process.resourcesPath, 'bin', 'aperture')
|
||||
: path.join(__dirname, '../../resources/bin/aperture'); // Adjust based on structure
|
||||
if (action === 'start') {
|
||||
if (recordingProcess)
|
||||
return { success: false, error: 'Recording already in progress' };
|
||||
const { fps = 30, audioDeviceId, output } = options || {};
|
||||
const cleanOutput = (0, file_utils_1.expandPath)(output || '~/tmp/recording.mp4');
|
||||
const args = ['run', '--fps', fps, '--output', cleanOutput];
|
||||
if (audioDeviceId)
|
||||
args.push('--audio-device-id', audioDeviceId);
|
||||
// Spawn process
|
||||
// Note: aperture is a CLI tool. We might need 'aperture' node package or the binary.
|
||||
// Assuming binary usage here.
|
||||
try {
|
||||
console.log(`Starting recording: ${binPath} ${args.join(' ')}`);
|
||||
recordingProcess = (0, child_process_1.spawn)(binPath, args);
|
||||
recordingProcess.on('error', (err) => {
|
||||
console.error('Recording error:', err);
|
||||
recordingProcess = null;
|
||||
});
|
||||
recordingProcess.on('exit', (code) => {
|
||||
console.log(`Recording exited with code ${code}`);
|
||||
recordingProcess = null;
|
||||
});
|
||||
return { success: true, pid: recordingProcess.pid };
|
||||
}
|
||||
catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
else if (action === 'stop') {
|
||||
if (!recordingProcess)
|
||||
return { success: false, error: 'No recording in progress' };
|
||||
recordingProcess.kill('SIGINT'); // Send interrupt to stop cleanly
|
||||
recordingProcess = null;
|
||||
return { success: true };
|
||||
}
|
||||
else if (action === 'status') {
|
||||
return { success: true, isRecording: !!recordingProcess };
|
||||
}
|
||||
return { success: false, error: 'Unknown action' };
|
||||
});
|
||||
// 6. Set Display Layout (Displayplacer)
|
||||
// Auto-detects connected displays via `displayplacer list` when no explicit configStr is given.
|
||||
// mirror: secondary display(s) mirror the primary.
|
||||
// extend: displays shown side-by-side; un-mirrors if currently mirrored.
|
||||
electron_1.ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
|
||||
if (os.platform() !== 'darwin')
|
||||
return { success: false, error: 'Display control only supported on macOS' };
|
||||
// Try bundled binary first; fall back to common Homebrew/system locations.
|
||||
// Install on a dev/venue Mac via: brew install displayplacer
|
||||
const _bin_candidates = electron_1.app.isPackaged
|
||||
? [path.join(process.resourcesPath, 'bin', 'displayplacer')]
|
||||
: [
|
||||
path.join(__dirname, '../../resources/bin/displayplacer'),
|
||||
'/opt/homebrew/bin/displayplacer', // Apple Silicon Homebrew
|
||||
'/usr/local/bin/displayplacer', // Intel Homebrew
|
||||
];
|
||||
const binPath = _bin_candidates.find(p => fs.existsSync(p)) ?? _bin_candidates[0];
|
||||
// Explicit config string always takes priority — allows manual override per device.
|
||||
if (configStr) {
|
||||
return await runExec(`"${binPath}" ${configStr}`);
|
||||
}
|
||||
// Auto-detect: `displayplacer list` emits a ready-to-run command line at the bottom.
|
||||
// We parse the quoted display strings from that line and modify them for the requested mode.
|
||||
const list_result = await runExec(`"${binPath}" list`);
|
||||
if (!list_result.success || !list_result.stdout) {
|
||||
return { success: false, error: `displayplacer list failed: ${list_result.error ?? 'no output'}` };
|
||||
}
|
||||
// The command line looks like: displayplacer "id:xxx res:... origin:(0,0) ..." "id:yyy ..."
|
||||
const cmd_line = list_result.stdout.split('\n').find(l => l.trim().startsWith('displayplacer "'));
|
||||
if (!cmd_line) {
|
||||
return { success: false, error: 'Only one display connected or displayplacer list output unrecognised' };
|
||||
}
|
||||
const display_strings = [...cmd_line.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
||||
if (display_strings.length < 2) {
|
||||
return { success: false, error: 'Only one display found; cannot change layout' };
|
||||
}
|
||||
if (mode === 'mirror') {
|
||||
const primary_id_match = display_strings[0].match(/\bid:([^\s]+)/);
|
||||
if (!primary_id_match) {
|
||||
return { success: false, error: 'Could not parse primary display ID from displayplacer output' };
|
||||
}
|
||||
const primary_id = primary_id_match[1];
|
||||
// Primary display unchanged; secondary display(s) get mirror_of_display:<primary_id>.
|
||||
const mirror_args = display_strings.map((s, i) => {
|
||||
if (i === 0)
|
||||
return `"${s}"`;
|
||||
const without_existing_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
|
||||
return `"${without_existing_mirror} mirror_of_display:${primary_id}"`;
|
||||
}).join(' ');
|
||||
return await runExec(`"${binPath}" ${mirror_args}`);
|
||||
}
|
||||
if (mode === 'extend') {
|
||||
const any_mirrored = display_strings.some(s => /\bmirror_of_display:\S+/.test(s));
|
||||
if (!any_mirrored) {
|
||||
// Already extended — re-apply current layout to ensure it is active.
|
||||
return await runExec(`"${binPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`);
|
||||
}
|
||||
// Remove mirror keys and compute side-by-side origins from each display's resolution.
|
||||
let x_offset = 0;
|
||||
const extend_args = display_strings.map((s) => {
|
||||
const without_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
|
||||
const res_match = without_mirror.match(/\bres:(\d+)x\d+/);
|
||||
const width = res_match ? parseInt(res_match[1]) : 1920;
|
||||
const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`);
|
||||
x_offset += width;
|
||||
return `"${updated}"`;
|
||||
}).join(' ');
|
||||
return await runExec(`"${binPath}" ${extend_args}`);
|
||||
}
|
||||
return { success: false, error: `Unsupported display mode: ${mode}` };
|
||||
});
|
||||
// 7. Update App
|
||||
electron_1.ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
|
||||
// 1. Determine Source File
|
||||
let updateFile = '';
|
||||
const tempDir = os.tmpdir();
|
||||
const destName = 'update_package.zip'; // Or .app, .AppImage
|
||||
const destPath = path.join(tempDir, destName);
|
||||
if (source === 'url' && url) {
|
||||
// Download
|
||||
try {
|
||||
const response = await (0, axios_1.default)({
|
||||
method: 'get',
|
||||
url: url,
|
||||
responseType: 'stream'
|
||||
});
|
||||
const writer = fs.createWriteStream(destPath);
|
||||
response.data.pipe(writer);
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve(true));
|
||||
writer.on('error', reject);
|
||||
});
|
||||
updateFile = destPath;
|
||||
}
|
||||
catch (e) {
|
||||
return { success: false, error: `Download failed: ${e.message}` };
|
||||
}
|
||||
}
|
||||
else if (source === 'file' && localPath) {
|
||||
const cleanLocal = (0, file_utils_1.expandPath)(localPath);
|
||||
if (fs.existsSync(cleanLocal)) {
|
||||
updateFile = cleanLocal;
|
||||
}
|
||||
else {
|
||||
return { success: false, error: 'Local update file not found' };
|
||||
}
|
||||
}
|
||||
if (!updateFile)
|
||||
return { success: false, error: 'No update source provided' };
|
||||
// 2. Install Logic (Stub)
|
||||
// Real implementation depends on OS and packaging format.
|
||||
// macOS: Mount DMG, copy .app to /Applications? Or Unzip .app?
|
||||
// Linux: chmod +x AppImage and move?
|
||||
console.log(`Ready to install update from: ${updateFile}`);
|
||||
// For now, just return success so the UI knows we "downloaded" it.
|
||||
return { success: true, message: 'Update downloaded/located. Installation logic requires packaging specifics.', downloadedPath: updateFile };
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=system_handlers.js.map
|
||||
1
dist/main/system_handlers.js.map
vendored
Normal file
31
dist/preload/index.js
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const electron_1 = require("electron");
|
||||
electron_1.contextBridge.exposeInMainWorld('aetherNative', {
|
||||
get_seed_config: () => electron_1.ipcRenderer.invoke('get-seed-config'),
|
||||
get_device_config: () => electron_1.ipcRenderer.invoke('get-device-config'),
|
||||
get_jwt: () => electron_1.ipcRenderer.invoke('get-jwt'),
|
||||
get_device_info: () => electron_1.ipcRenderer.invoke('get-device-info'),
|
||||
open_folder: (path) => electron_1.ipcRenderer.invoke('native:open-folder', path),
|
||||
run_cmd: (args) => electron_1.ipcRenderer.invoke('native:run-cmd', args),
|
||||
run_cmd_sync: (args) => electron_1.ipcRenderer.invoke('native:run-cmd-sync', args),
|
||||
run_osascript: (script) => electron_1.ipcRenderer.invoke('native:run-osascript', script),
|
||||
kill_processes: (args) => electron_1.ipcRenderer.invoke('native:kill-processes', args),
|
||||
open_local_file_v2: (path) => electron_1.ipcRenderer.invoke('native:open-local-file-v2', path),
|
||||
check_cache: (args) => electron_1.ipcRenderer.invoke('native:check-cache', args),
|
||||
download_to_cache: (args) => electron_1.ipcRenderer.invoke('native:download-to-cache', args),
|
||||
copy_from_cache_to_temp: (args) => electron_1.ipcRenderer.invoke('native:copy-from-cache-to-temp', args),
|
||||
launch_from_cache: (args) => electron_1.ipcRenderer.invoke('native:launch-from-cache', args),
|
||||
launch_presentation: (args) => electron_1.ipcRenderer.invoke('native:launch-presentation', args),
|
||||
control_presentation: (args) => electron_1.ipcRenderer.invoke('native:control-presentation', args),
|
||||
list_tools: () => electron_1.ipcRenderer.invoke('native:list-tools'),
|
||||
// System Handlers (V5)
|
||||
set_wallpaper: (args) => electron_1.ipcRenderer.invoke('native:set-wallpaper', args),
|
||||
update_app: (args) => electron_1.ipcRenderer.invoke('native:update-app', args),
|
||||
window_control: (args) => electron_1.ipcRenderer.invoke('native:window-control', args),
|
||||
manage_recording: (args) => electron_1.ipcRenderer.invoke('native:manage-recording', args),
|
||||
set_display_layout: (args) => electron_1.ipcRenderer.invoke('native:set-display-layout', args),
|
||||
power_control: (args) => electron_1.ipcRenderer.invoke('native:power-control', args),
|
||||
open_external: (args) => electron_1.ipcRenderer.invoke('native:open-external', args),
|
||||
});
|
||||
//# sourceMappingURL=index.js.map
|
||||
1
dist/preload/index.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/preload/index.ts"],"names":[],"mappings":";;AAAA,uCAAsD;AAEtD,wBAAa,CAAC,iBAAiB,CAAC,cAAc,EAAE;IAC9C,eAAe,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAC5D,iBAAiB,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,mBAAmB,CAAC;IAChE,OAAO,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,SAAS,CAAC;IAC5C,eAAe,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,iBAAiB,CAAC;IAE5D,WAAW,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,oBAAoB,EAAE,IAAI,CAAC;IAC7E,OAAO,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,gBAAgB,EAAE,IAAI,CAAC;IAClE,YAAY,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,qBAAqB,EAAE,IAAI,CAAC;IAC5E,aAAa,EAAE,CAAC,MAAc,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACrF,cAAc,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,uBAAuB,EAAE,IAAI,CAAC;IAChF,kBAAkB,EAAE,CAAC,IAAY,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,2BAA2B,EAAE,IAAI,CAAC;IAE3F,WAAW,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,oBAAoB,EAAE,IAAI,CAAC;IAC1E,iBAAiB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,0BAA0B,EAAE,IAAI,CAAC;IACtF,uBAAuB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,gCAAgC,EAAE,IAAI,CAAC;IAClG,iBAAiB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,0BAA0B,EAAE,IAAI,CAAC;IACtF,mBAAmB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,4BAA4B,EAAE,IAAI,CAAC;IAC1F,oBAAoB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,6BAA6B,EAAE,IAAI,CAAC;IAC5F,UAAU,EAAE,GAAG,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,mBAAmB,CAAC;IAEzD,uBAAuB;IACvB,aAAa,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC;IAC9E,UAAU,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,mBAAmB,EAAE,IAAI,CAAC;IACxE,cAAc,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,uBAAuB,EAAE,IAAI,CAAC;IAChF,gBAAgB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,yBAAyB,EAAE,IAAI,CAAC;IACpF,kBAAkB,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,2BAA2B,EAAE,IAAI,CAAC;IACxF,aAAa,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC;IAC9E,aAAa,EAAE,CAAC,IAAS,EAAE,EAAE,CAAC,sBAAW,CAAC,MAAM,CAAC,sBAAsB,EAAE,IAAI,CAAC;CAC/E,CAAC,CAAC"}
|
||||
3
dist/shared/types.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
//# sourceMappingURL=types.js.map
|
||||
1
dist/shared/types.js.map
vendored
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":""}
|
||||
116
documentation/TODO_AGENTS.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# Native App Agent Task List
|
||||
> Use this file to track steps for complex features or bug fixes.
|
||||
> **Status:** Stable - ongoing development.
|
||||
|
||||
## Current Investigation
|
||||
- This started as an API contract review for the native Electron bootstrap path and expanded into a packaging/runtime issue after the deploy step stopped producing bundles.
|
||||
- We now know the API side was not the root cause. The bootstrap request shape in `src/main/api_client.ts` was wrong and has been corrected.
|
||||
- The packaging blocker has been diagnosed and fixed (see below).
|
||||
|
||||
## Launcher Terminology Cleanup
|
||||
- Align the native docs/comments with the Svelte-side terminology: **Launch Profile** = the
|
||||
Svelte config object keyed by extension; **Native Template** = the AppleScript or shell
|
||||
string Electron actually executes after the file lands in temp.
|
||||
- Update `src/main/file_handlers.ts` comments and any bridge-facing wording so they describe the
|
||||
resolved `native_template` string accurately. `launch_profiles` is only the Svelte-side map;
|
||||
the IPC payload is the single executable string. Do not reintroduce `launch_scripts` as the
|
||||
public term.
|
||||
- Keep the source of truth in Svelte. Electron should remain a thin executor/copy layer.
|
||||
- After source/docs updates, rebuild/regenerate `dist/main/file_handlers.js` from source; do not
|
||||
hand-edit the generated file.
|
||||
- If any parameter names remain awkward in source, preserve runtime behavior first and rename
|
||||
only when the signature ripple is understood.
|
||||
|
||||
## What Was Fixed
|
||||
- Updated the native bootstrap flow to use the direct `site_domain/search` request body expected by API V3.
|
||||
- Standardized the account-bypass header to `x-no-account-id: bypass` where that narrow bypass is intended.
|
||||
- Removed a redundant `x-no-account-id` header from file download calls.
|
||||
- Rewrote the device lookup smoke test so it validates the real two-step bootstrap path end to end.
|
||||
- Upgraded Electron from 34.x to 42.0.1.
|
||||
- Replaced deprecated `electron-packager` with `@electron/packager` 20.0.0.
|
||||
- Added a `package:linux` smoke test path so packaging failures can be isolated from macOS-specific behavior.
|
||||
- **Fixed packaging hang on Node 26:** `yauzl` 2.10.0 (used by `extract-zip` in `@electron/packager`) emits no `data` events on Node 26 streams, causing zip extraction to hang indefinitely. Fix: patched `@electron/packager/dist/unzip.js` to use `bsdtar` (libarchive) instead of `extract-zip`. `bsdtar` was chosen over `7z` because `7z` refuses macOS `.app` bundles with chained symlinks inside framework bundles. Patch is re-applied on every `npm install` via the `postinstall` script at `scripts/patch-packager-unzip.js`.
|
||||
|
||||
## Verified So Far
|
||||
- `npm run dev` works once the Electron binary is present locally.
|
||||
- Manual Electron cache extraction restored a runnable checkout on this machine.
|
||||
- API validation confirmed the backend responds correctly for:
|
||||
- `event_device/{id}` lookup
|
||||
- `site_domain/search?limit=1` with the direct `SearchQuery` body
|
||||
- The returned `site_domain.account_id` matches the device account context in the verified bootstrap flow.
|
||||
- The SvelteKit frontend bootstrap path already follows the correct API contract and does not need the same fix.
|
||||
- **`npm run package:linux` now produces `builds/aether_launcher-linux-x64/`** with a complete bundle (confirmed 2026-05-11).
|
||||
- **`npm run package:mac` now produces `builds/aether_launcher-darwin-x64/` and `builds/aether_launcher-darwin-arm64/`** with `aether_launcher.app` inside each (confirmed 2026-05-11). Initial fix used `7z` but it refused chained symlinks inside macOS framework bundles; switched to `bsdtar` (libarchive) which handles both Linux and macOS zips correctly.
|
||||
- `deploy/deploy.sh` output directory names (`aether_launcher-darwin-x64`, `aether_launcher-darwin-arm64`) match packager output — no script changes needed.
|
||||
|
||||
## Remaining Items
|
||||
1. Test that the packaged Linux binary runs end-to-end against the dev API.
|
||||
|
||||
## Root Cause Summary (Packaging Hang)
|
||||
- **Tool chain:** Node 26.1.0 + `@electron/packager` 20.0.0 + `extract-zip` 2.0.1 + `yauzl` 2.10.0
|
||||
- **Symptom:** `npm run package:linux` exits 0 but produces no output. Debug log shows it starts extraction but never finishes.
|
||||
- **Root cause:** `yauzl` opens a read stream for the first zip entry, but on Node 26, no `data` events are ever emitted on that stream. The `pipeline(readStream, writeStream)` call in `extract-zip` blocks forever.
|
||||
- **Fix:** Replace the one-liner `extractElectronZip` function in `node_modules/@electron/packager/dist/unzip.js` with a `child_process.execSync` call to `bsdtar -xf`. `bsdtar` was chosen over `7z` because `7z` refuses macOS `.app` bundles with chained symlinks (e.g. `Electron Framework.framework/Libraries → Versions/Current/Libraries`). A `postinstall` npm script re-applies this patch after each `npm install`.
|
||||
- **Build-time dependency:** `libarchive` (provides `bsdtar`) must be installed on the build host. On Arch: `pacman -S libarchive`; macOS: included in Xcode CLT or `brew install libarchive`; Ubuntu/Debian: `apt install libarchive-tools`.
|
||||
|
||||
## References
|
||||
- Electron 42 release notes: https://www.electronjs.org/blog/electron-42-0
|
||||
- Related Electron packaging discussion: https://github.com/aaddrick/claude-desktop-debian/pull/587
|
||||
- Electron packaging/runtime change reference: https://github.com/electron/electron/pull/49328
|
||||
- yauzl Node 26 stream issue: `yauzl` 2.10.0 uses legacy Node streams (streams1 style); Node 26 changed stream internal behavior so `openReadStream` returns a stream that never emits `data` without a proper pipeline consumer.
|
||||
|
||||
## Notes
|
||||
- Was on Electron 34.
|
||||
- The problem is not the backend API keys or the frontend site bootstrap flow.
|
||||
- The packaging fix is a node_modules patch, not upstream. If `@electron/packager` or `extract-zip` releases a Node 26-compatible version, the `postinstall` script should be removed.
|
||||
|
||||
---
|
||||
|
||||
## set_display_layout — Setup & Status (updated 2026-05-20)
|
||||
|
||||
**Primary approach: `display_control` (native CoreGraphics — no Homebrew required)**
|
||||
- Source: `scripts/display_control.m` (derived from OSIT MasterKey app, Ian Kohl 2019)
|
||||
- Build: run `scripts/build-display-control.sh` on a Mac (requires Xcode CLT only)
|
||||
- Output: `resources/bin/display_control` — commit this binary to the repo
|
||||
- Uses `CGConfigureDisplayMirrorOfDisplay` — same CoreGraphics API macOS uses internally
|
||||
- Supports 3+ displays; auto-detects all connected displays; no config string needed
|
||||
|
||||
**Fallback approach: `displayplacer` (requires `brew install displayplacer` on each venue Mac)**
|
||||
- Reference: [jakehilborn/displayplacer](https://github.com/jakehilborn/displayplacer)
|
||||
- Still used when `display_control` binary is not present
|
||||
- Also used for per-device `configStr` overrides (displayplacer-format strings in `event_device.data_json`)
|
||||
|
||||
**Current state (2026-05-20):**
|
||||
|
||||
- ✅ Correct `mirror_of_display:<uuid>` syntax used in displayplacer fallback (was `mirror:` — wrong, now fixed)
|
||||
- ✅ Failures logged to Electron console (`[Launcher] set_display_layout:`) instead of silently swallowed
|
||||
- ✅ **Display Mode toggle** added to Launcher config (Native OS section) — Extend/Mirror buttons always visible, no Technical Mode required
|
||||
- ⏳ `display_control` binary not yet built — must be compiled on a Mac and committed
|
||||
|
||||
**To build `display_control` (do this on a Mac):**
|
||||
```bash
|
||||
# One-time: install Xcode Command Line Tools if not already installed
|
||||
xcode-select --install
|
||||
|
||||
# Then:
|
||||
./scripts/build-display-control.sh
|
||||
|
||||
# Test it with a second display connected:
|
||||
./resources/bin/display_control status
|
||||
./resources/bin/display_control extend
|
||||
./resources/bin/display_control mirror
|
||||
|
||||
# Commit the binary:
|
||||
git add resources/bin/display_control
|
||||
git commit -m "build: add display_control binary (macOS CoreGraphics)"
|
||||
```
|
||||
|
||||
**Optional per-device override (displayplacer format, for edge cases):**
|
||||
For rooms where auto-detection produces the wrong result, store the raw configStr in `event_device.data_json`:
|
||||
```json
|
||||
{
|
||||
"displayplacer_config_extend": "<output of displayplacer list in extended layout>",
|
||||
"displayplacer_config_mirror": "<output of displayplacer list in mirrored layout>"
|
||||
}
|
||||
```
|
||||
`configStr` is passed from the Svelte call site and uses the displayplacer fallback path directly.
|
||||
0
documentation/old_custom_OSIT_Master_Key_app/oneskyit-master-key-da2e467baf22/.gitignore
vendored
Normal file
@@ -0,0 +1,567 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
3C278DA7225CF5C6004FFC53 /* masterkey.app in CopyFiles */ = {isa = PBXBuildFile; fileRef = 3C41339F2227E09B0041136E /* masterkey.app */; };
|
||||
3C39A72F2267ABA90061367B /* WordMacAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A72E2267ABA90061367B /* WordMacAssistant.swift */; };
|
||||
3C39A7312267ACCF0061367B /* AcrobatWindowsAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A7302267ACCF0061367B /* AcrobatWindowsAssistant.swift */; };
|
||||
3C39A7332267ACF60061367B /* Windows Application Names in Resources */ = {isa = PBXBuildFile; fileRef = 3C39A7322267ACF60061367B /* Windows Application Names */; };
|
||||
3C39A7352267AF040061367B /* WindowsVideoAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A7342267AF040061367B /* WindowsVideoAssistant.swift */; };
|
||||
3C39A739226A347A0061367B /* ExcelAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C39A738226A347A0061367B /* ExcelAssistant.swift */; };
|
||||
3C4133A32227E09B0041136E /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133A22227E09B0041136E /* AppDelegate.swift */; };
|
||||
3C4133A52227E09F0041136E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3C4133A42227E09F0041136E /* Assets.xcassets */; };
|
||||
3C4133A82227E09F0041136E /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3C4133A62227E09F0041136E /* MainMenu.xib */; };
|
||||
3C4133B42227E09F0041136E /* masterkeyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133B32227E09F0041136E /* masterkeyTests.swift */; };
|
||||
3C4133C02227E1B70041136E /* PowerPointMacAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133BF2227E1B70041136E /* PowerPointMacAssistant.swift */; };
|
||||
3C4133C22227E1C90041136E /* PowerPointWindowsAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C12227E1C90041136E /* PowerPointWindowsAssistant.swift */; };
|
||||
3C4133C42227E1D60041136E /* KeynoteAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C32227E1D60041136E /* KeynoteAssistant.swift */; };
|
||||
3C4133C62227E1E50041136E /* LegacyKeynoteAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C52227E1E40041136E /* LegacyKeynoteAssistant.swift */; };
|
||||
3C4133C82227E2010041136E /* AcrobatAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C72227E2010041136E /* AcrobatAssistant.swift */; };
|
||||
3C4133CA2227E2960041136E /* ApplicationAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4133C92227E2960041136E /* ApplicationAssistant.swift */; };
|
||||
3C88C06A225DADDD0048E01A /* VideoAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C88C069225DADDD0048E01A /* VideoAssistant.swift */; };
|
||||
3CE21AD62259B3EE00F666B9 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE21AD52259B3EE00F666B9 /* Utilities.swift */; };
|
||||
3CE21AD92259B4A000F666B9 /* LegacyUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CE21AD82259B4A000F666B9 /* LegacyUtilities.m */; };
|
||||
3CE21ADC225A674800F666B9 /* LibreOfficeAssistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CE21ADB225A674800F666B9 /* LibreOfficeAssistant.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
3C4133B02227E09F0041136E /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 3C4133972227E09B0041136E /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 3C41339E2227E09B0041136E;
|
||||
remoteInfo = masterkey;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
3CE0D699224155AE00E7FC1C /* CopyFiles */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = /Users/Ian/Desktop;
|
||||
dstSubfolderSpec = 0;
|
||||
files = (
|
||||
3C278DA7225CF5C6004FFC53 /* masterkey.app in CopyFiles */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
3C39A72E2267ABA90061367B /* WordMacAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordMacAssistant.swift; sourceTree = "<group>"; };
|
||||
3C39A7302267ACCF0061367B /* AcrobatWindowsAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcrobatWindowsAssistant.swift; sourceTree = "<group>"; };
|
||||
3C39A7322267ACF60061367B /* Windows Application Names */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Windows Application Names"; sourceTree = "<group>"; };
|
||||
3C39A7342267AF040061367B /* WindowsVideoAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowsVideoAssistant.swift; sourceTree = "<group>"; };
|
||||
3C39A738226A347A0061367B /* ExcelAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExcelAssistant.swift; sourceTree = "<group>"; };
|
||||
3C41339F2227E09B0041136E /* masterkey.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = masterkey.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3C4133A22227E09B0041136E /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
3C4133A42227E09F0041136E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
3C4133A72227E09F0041136E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; };
|
||||
3C4133A92227E09F0041136E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3C4133AA2227E09F0041136E /* masterkey.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = masterkey.entitlements; sourceTree = "<group>"; };
|
||||
3C4133AF2227E09F0041136E /* masterkeyTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = masterkeyTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3C4133B32227E09F0041136E /* masterkeyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = masterkeyTests.swift; sourceTree = "<group>"; };
|
||||
3C4133B52227E09F0041136E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
3C4133BF2227E1B70041136E /* PowerPointMacAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerPointMacAssistant.swift; sourceTree = "<group>"; };
|
||||
3C4133C12227E1C90041136E /* PowerPointWindowsAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerPointWindowsAssistant.swift; sourceTree = "<group>"; };
|
||||
3C4133C32227E1D60041136E /* KeynoteAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeynoteAssistant.swift; sourceTree = "<group>"; };
|
||||
3C4133C52227E1E40041136E /* LegacyKeynoteAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyKeynoteAssistant.swift; sourceTree = "<group>"; };
|
||||
3C4133C72227E2010041136E /* AcrobatAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcrobatAssistant.swift; sourceTree = "<group>"; };
|
||||
3C4133C92227E2960041136E /* ApplicationAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationAssistant.swift; sourceTree = "<group>"; };
|
||||
3C88C069225DADDD0048E01A /* VideoAssistant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoAssistant.swift; sourceTree = "<group>"; };
|
||||
3CE21AD52259B3EE00F666B9 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
|
||||
3CE21AD72259B4A000F666B9 /* masterkey-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "masterkey-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
3CE21AD82259B4A000F666B9 /* LegacyUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LegacyUtilities.m; sourceTree = "<group>"; };
|
||||
3CE21ADA2259B5B900F666B9 /* LegacyUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LegacyUtilities.h; sourceTree = "<group>"; };
|
||||
3CE21ADB225A674800F666B9 /* LibreOfficeAssistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibreOfficeAssistant.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
3C41339C2227E09B0041136E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3C4133AC2227E09F0041136E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
3C4133962227E09B0041136E = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3C4133A12227E09B0041136E /* masterkey */,
|
||||
3C4133B22227E09F0041136E /* masterkeyTests */,
|
||||
3C4133A02227E09B0041136E /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3C4133A02227E09B0041136E /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3C41339F2227E09B0041136E /* masterkey.app */,
|
||||
3C4133AF2227E09F0041136E /* masterkeyTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3C4133A12227E09B0041136E /* masterkey */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3C4133BE2227E0B90041136E /* ApplicationAssistants */,
|
||||
3C4133C92227E2960041136E /* ApplicationAssistant.swift */,
|
||||
3C4133A22227E09B0041136E /* AppDelegate.swift */,
|
||||
3C4133A42227E09F0041136E /* Assets.xcassets */,
|
||||
3C4133A62227E09F0041136E /* MainMenu.xib */,
|
||||
3C4133A92227E09F0041136E /* Info.plist */,
|
||||
3C4133AA2227E09F0041136E /* masterkey.entitlements */,
|
||||
3CE21AD52259B3EE00F666B9 /* Utilities.swift */,
|
||||
3CE21ADA2259B5B900F666B9 /* LegacyUtilities.h */,
|
||||
3CE21AD82259B4A000F666B9 /* LegacyUtilities.m */,
|
||||
3CE21AD72259B4A000F666B9 /* masterkey-Bridging-Header.h */,
|
||||
3C39A7322267ACF60061367B /* Windows Application Names */,
|
||||
);
|
||||
path = masterkey;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3C4133B22227E09F0041136E /* masterkeyTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3C4133B32227E09F0041136E /* masterkeyTests.swift */,
|
||||
3C4133B52227E09F0041136E /* Info.plist */,
|
||||
);
|
||||
path = masterkeyTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3C4133BE2227E0B90041136E /* ApplicationAssistants */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3C4133BF2227E1B70041136E /* PowerPointMacAssistant.swift */,
|
||||
3C4133C12227E1C90041136E /* PowerPointWindowsAssistant.swift */,
|
||||
3C39A72E2267ABA90061367B /* WordMacAssistant.swift */,
|
||||
3C4133C32227E1D60041136E /* KeynoteAssistant.swift */,
|
||||
3C4133C52227E1E40041136E /* LegacyKeynoteAssistant.swift */,
|
||||
3C4133C72227E2010041136E /* AcrobatAssistant.swift */,
|
||||
3C39A7302267ACCF0061367B /* AcrobatWindowsAssistant.swift */,
|
||||
3CE21ADB225A674800F666B9 /* LibreOfficeAssistant.swift */,
|
||||
3C88C069225DADDD0048E01A /* VideoAssistant.swift */,
|
||||
3C39A7342267AF040061367B /* WindowsVideoAssistant.swift */,
|
||||
3C39A738226A347A0061367B /* ExcelAssistant.swift */,
|
||||
);
|
||||
path = ApplicationAssistants;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
3C41339E2227E09B0041136E /* masterkey */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3C4133B82227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkey" */;
|
||||
buildPhases = (
|
||||
3C41339B2227E09B0041136E /* Sources */,
|
||||
3C41339C2227E09B0041136E /* Frameworks */,
|
||||
3C41339D2227E09B0041136E /* Resources */,
|
||||
3CE0D699224155AE00E7FC1C /* CopyFiles */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = masterkey;
|
||||
productName = masterkey;
|
||||
productReference = 3C41339F2227E09B0041136E /* masterkey.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
3C4133AE2227E09F0041136E /* masterkeyTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3C4133BB2227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkeyTests" */;
|
||||
buildPhases = (
|
||||
3C4133AB2227E09F0041136E /* Sources */,
|
||||
3C4133AC2227E09F0041136E /* Frameworks */,
|
||||
3C4133AD2227E09F0041136E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3C4133B12227E09F0041136E /* PBXTargetDependency */,
|
||||
);
|
||||
name = masterkeyTests;
|
||||
productName = masterkeyTests;
|
||||
productReference = 3C4133AF2227E09F0041136E /* masterkeyTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
3C4133972227E09B0041136E /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1010;
|
||||
LastUpgradeCheck = 1010;
|
||||
ORGANIZATIONNAME = "One Sky IT";
|
||||
TargetAttributes = {
|
||||
3C41339E2227E09B0041136E = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
LastSwiftMigration = 1020;
|
||||
SystemCapabilities = {
|
||||
com.apple.Sandbox = {
|
||||
enabled = 0;
|
||||
};
|
||||
};
|
||||
};
|
||||
3C4133AE2227E09F0041136E = {
|
||||
CreatedOnToolsVersion = 10.1;
|
||||
TestTargetID = 3C41339E2227E09B0041136E;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 3C41339A2227E09B0041136E /* Build configuration list for PBXProject "masterkey" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = 3C4133962227E09B0041136E;
|
||||
productRefGroup = 3C4133A02227E09B0041136E /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
3C41339E2227E09B0041136E /* masterkey */,
|
||||
3C4133AE2227E09F0041136E /* masterkeyTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
3C41339D2227E09B0041136E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3C4133A52227E09F0041136E /* Assets.xcassets in Resources */,
|
||||
3C4133A82227E09F0041136E /* MainMenu.xib in Resources */,
|
||||
3C39A7332267ACF60061367B /* Windows Application Names in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3C4133AD2227E09F0041136E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
3C41339B2227E09B0041136E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3C4133CA2227E2960041136E /* ApplicationAssistant.swift in Sources */,
|
||||
3C4133C22227E1C90041136E /* PowerPointWindowsAssistant.swift in Sources */,
|
||||
3C4133A32227E09B0041136E /* AppDelegate.swift in Sources */,
|
||||
3C4133C42227E1D60041136E /* KeynoteAssistant.swift in Sources */,
|
||||
3C39A739226A347A0061367B /* ExcelAssistant.swift in Sources */,
|
||||
3C39A72F2267ABA90061367B /* WordMacAssistant.swift in Sources */,
|
||||
3C4133C02227E1B70041136E /* PowerPointMacAssistant.swift in Sources */,
|
||||
3CE21AD92259B4A000F666B9 /* LegacyUtilities.m in Sources */,
|
||||
3C4133C62227E1E50041136E /* LegacyKeynoteAssistant.swift in Sources */,
|
||||
3C39A7312267ACCF0061367B /* AcrobatWindowsAssistant.swift in Sources */,
|
||||
3CE21AD62259B3EE00F666B9 /* Utilities.swift in Sources */,
|
||||
3C39A7352267AF040061367B /* WindowsVideoAssistant.swift in Sources */,
|
||||
3C4133C82227E2010041136E /* AcrobatAssistant.swift in Sources */,
|
||||
3C88C06A225DADDD0048E01A /* VideoAssistant.swift in Sources */,
|
||||
3CE21ADC225A674800F666B9 /* LibreOfficeAssistant.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3C4133AB2227E09F0041136E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
3C4133B42227E09F0041136E /* masterkeyTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
3C4133B12227E09F0041136E /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 3C41339E2227E09B0041136E /* masterkey */;
|
||||
targetProxy = 3C4133B02227E09F0041136E /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
3C4133A62227E09F0041136E /* MainMenu.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
3C4133A72227E09F0041136E /* Base */,
|
||||
);
|
||||
name = MainMenu.xib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
3C4133B62227E09F0041136E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3C4133B72227E09F0041136E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = macosx;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3C4133B92227E09F0041136E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = masterkey/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkey;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "masterkey/masterkey-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3C4133BA2227E09F0041136E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "-";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = masterkey/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkey;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "masterkey/masterkey-Bridging-Header.h";
|
||||
SWIFT_VERSION = 4.2;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3C4133BC2227E09F0041136E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = masterkeyTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkeyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/masterkey.app/Contents/MacOS/masterkey";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3C4133BD2227E09F0041136E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
INFOPLIST_FILE = masterkeyTests/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@loader_path/../Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.oneskyit.masterkeyTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_VERSION = 4.2;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/masterkey.app/Contents/MacOS/masterkey";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
3C41339A2227E09B0041136E /* Build configuration list for PBXProject "masterkey" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3C4133B62227E09F0041136E /* Debug */,
|
||||
3C4133B72227E09F0041136E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3C4133B82227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkey" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3C4133B92227E09F0041136E /* Debug */,
|
||||
3C4133BA2227E09F0041136E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3C4133BB2227E09F0041136E /* Build configuration list for PBXNativeTarget "masterkeyTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3C4133BC2227E09F0041136E /* Debug */,
|
||||
3C4133BD2227E09F0041136E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = 3C4133972227E09B0041136E /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:masterkey.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>masterkey.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,275 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
//Open/Close
|
||||
//quicktime - MP4
|
||||
//Text
|
||||
//Doc
|
||||
|
||||
import Cocoa
|
||||
import OSAKit
|
||||
|
||||
enum ApplicationNameConstants: String {
|
||||
case PowerPointMac = "Microsoft PowerPoint"
|
||||
case PowerPointWin = "Microsoft Office PowerPoint"
|
||||
case Keynote = "Keynote"
|
||||
case LibreOffice = "LibreOffice"
|
||||
case AdobeAcrobat = "Adobe Acrobat Reader DC"
|
||||
case GoogleChrome = "Google Chrome"
|
||||
case VLC = "VLC"
|
||||
}
|
||||
|
||||
extension ApplicationNameConstants: CaseIterable {}
|
||||
|
||||
extension String {
|
||||
func fileName() -> String {
|
||||
return NSURL(fileURLWithPath: self).deletingPathExtension?.lastPathComponent ?? ""
|
||||
}
|
||||
|
||||
func fileExtension() -> String {
|
||||
return NSURL(fileURLWithPath: self).pathExtension ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
var assistant:ApplicationAssistant!
|
||||
|
||||
var openingFile = false
|
||||
|
||||
//No window
|
||||
//@IBOutlet weak var window: NSWindow!
|
||||
|
||||
func applicationDidFinishLaunching(_ aNotification: Notification) {
|
||||
if !openingFile {
|
||||
NSLog("Not opening a file. Displaying setup dialog...")
|
||||
if setupDialog() {
|
||||
NSLog("Proceeding to trigger all security warnings...")
|
||||
if triggerSecurity() {
|
||||
NSLog("Presentation applications have been opened..")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NSLog("Terminating applicaiton...")
|
||||
terminateApplication()
|
||||
}
|
||||
|
||||
func terminateApplication() {
|
||||
NSLog("Terminating...")
|
||||
NSApplication.shared.terminate(self)
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ aNotification: Notification) {
|
||||
|
||||
}
|
||||
|
||||
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||
NSLog("Version 2.0")
|
||||
|
||||
openingFile = true
|
||||
|
||||
//Used to open a group of files
|
||||
//let first_filename = filenames[0];
|
||||
|
||||
//open a single file
|
||||
let filename_extension = String(filename.fileExtension().lowercased());
|
||||
|
||||
NSLog("Filename Extension: \(filename_extension)")
|
||||
|
||||
var assistantFlag = false
|
||||
|
||||
//Args to pass to application - not currently used
|
||||
//var args = ""
|
||||
|
||||
//once we are ready with a single presentation file (lowercase, valid ext) get into the switch
|
||||
switch filename_extension {
|
||||
case "pptx",
|
||||
"ppt",
|
||||
"pptmac",
|
||||
"pptxmac":
|
||||
NSLog("Mac PPT File...");
|
||||
assistant = PowerPointMacAssistant();
|
||||
case "pptxwin",
|
||||
"pptwin":
|
||||
NSLog("Windows PPT File...")
|
||||
assistant = PowerPointWindowsAssistant();
|
||||
case "pdf",
|
||||
"pdfmac":
|
||||
NSLog("PDF File...")
|
||||
//args = "pagemode=FullScreen"
|
||||
assistant = AcrobatAssistant();
|
||||
case "pdfwin":
|
||||
NSLog("PDF Windows File...")
|
||||
assistant = AcrobatWindowsAssistant();
|
||||
case "key":
|
||||
NSLog("Keynote File...")
|
||||
assistant = KeynoteAssistant();
|
||||
case "odp":
|
||||
NSLog("LibreOffice File...")
|
||||
assistant = LibreOfficeAssistant();
|
||||
case "mp4",
|
||||
"mkv",
|
||||
"mov",
|
||||
"mpeg",
|
||||
"avi",
|
||||
"flv",
|
||||
"ogg",
|
||||
"mp3",
|
||||
"ogv":
|
||||
NSLog("Video (VLC) File...")
|
||||
assistant = VideoAssistant();
|
||||
case "wmv":
|
||||
NSLog("Windows Video (VLC) File...")
|
||||
assistant = WindowsVideoAssistant();
|
||||
case "doc",
|
||||
"docx":
|
||||
NSLog("Microsoft Word Document...")
|
||||
assistant = WordMacAssistant();
|
||||
default:
|
||||
NSLog("Generic File...")
|
||||
assistantFlag = true
|
||||
}
|
||||
|
||||
let legacyUtilities: LegacyUtilities = LegacyUtilities()
|
||||
|
||||
if assistantFlag {
|
||||
NSLog("Is assistant...")
|
||||
legacyUtilities.undoMirror()
|
||||
//TODO: Window to mirror
|
||||
NSWorkspace.shared.openFile(filename)
|
||||
} else {
|
||||
if assistant.shouldUnmirror {
|
||||
legacyUtilities.undoMirror()
|
||||
}
|
||||
|
||||
NSLog("Opening file...")
|
||||
NSWorkspace.shared.openFile(filename, withApplication: assistant.applicationName)
|
||||
NSLog("Sleeping...")
|
||||
sleep(assistant!.delay)
|
||||
NSLog("Activating application...")
|
||||
let apps = NSRunningApplication.runningApplications(withBundleIdentifier: assistant.bundleID)
|
||||
for app in apps as [NSRunningApplication] {
|
||||
if app.bundleIdentifier == assistant.bundleID {
|
||||
app.activate(options: .activateIgnoringOtherApps)
|
||||
}
|
||||
}
|
||||
|
||||
NSLog("Running Automation...")
|
||||
if self.runAutomation() {
|
||||
NSLog("Mirror check...")
|
||||
if assistant.mirrored {
|
||||
NSLog("Mirroring...")
|
||||
legacyUtilities.setupMirror()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Start recording code
|
||||
var screens = 2
|
||||
|
||||
//TODO: get actual number of screens as mirroring/unmirroring should be done at this point
|
||||
if assistant.mirrored {
|
||||
screens = 1
|
||||
}
|
||||
|
||||
let utils = Utilities()
|
||||
let rec_result = utils.startRecording(filename: filename,num_screens: screens)
|
||||
NSLog("Recording result...")
|
||||
NSLog(rec_result)
|
||||
//End recording code
|
||||
|
||||
// Return true for successfully opening the file.
|
||||
return true
|
||||
}
|
||||
|
||||
//TODO: Merge this and security as it is the same code
|
||||
func runAutomation() -> Bool {
|
||||
NSLog("Run automation...")
|
||||
NSLog(assistant.scriptString)
|
||||
let script = OSAScript.init(source: assistant.scriptString, language: OSALanguage.init(forName: "JavaScript"));
|
||||
var compileError : NSDictionary?
|
||||
script.compileAndReturnError(&compileError)
|
||||
if let compileError = compileError {
|
||||
NSLog("Compile Error...")
|
||||
print(compileError);
|
||||
return false;
|
||||
}
|
||||
var scriptError : NSDictionary?
|
||||
let result = script.executeAndReturnError(&scriptError)
|
||||
if let scriptError = scriptError {
|
||||
NSLog("Print error...")
|
||||
NSLog(scriptError.description)
|
||||
}
|
||||
else if let result = result?.stringValue {
|
||||
NSLog("Result...")
|
||||
print(result)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func triggerSecurity() -> Bool {
|
||||
NSLog("Trigger Security...")
|
||||
|
||||
ApplicationNameConstants.allCases.forEach {
|
||||
let application_name = $0.rawValue
|
||||
let application_security_script = """
|
||||
var app = Application('\(application_name)')
|
||||
delay(1)
|
||||
app.activate()
|
||||
"""
|
||||
executeSecurityScript(script: application_security_script)
|
||||
}
|
||||
|
||||
let final_security_script = """
|
||||
var finder = Application('Finder')
|
||||
finder.activate()
|
||||
delay(1)
|
||||
var se = Application('System Events')
|
||||
se.keyCode(50)
|
||||
"""
|
||||
NSLog(final_security_script)
|
||||
executeSecurityScript(script: final_security_script)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func executeSecurityScript(script: String) {
|
||||
let script = OSAScript.init(source: script, language: OSALanguage.init(forName: "JavaScript"));
|
||||
var compileError : NSDictionary?
|
||||
script.compileAndReturnError(&compileError)
|
||||
if let compileError = compileError {
|
||||
NSLog("Security Script Compile Error...")
|
||||
print(compileError);
|
||||
}
|
||||
var scriptError : NSDictionary?
|
||||
let result = script.executeAndReturnError(&scriptError)
|
||||
if let scriptError = scriptError {
|
||||
NSLog("Print security script error...")
|
||||
NSLog(scriptError.description)
|
||||
}
|
||||
else if let result = result?.stringValue {
|
||||
NSLog("Trigger Security Result...")
|
||||
print(result)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func setupDialog() -> Bool {
|
||||
let alert = NSAlert()
|
||||
alert.messageText = "Are you setting up a new version of Master Key?"
|
||||
alert.informativeText = "If you answer Yes, each applicaiton (PowerPoint, Keynote, etc.) will open so that security permissions can be granted for automation for each application. If you answer no, no action will be taken and the application will exit immediately."
|
||||
alert.alertStyle = .warning
|
||||
alert.addButton(withTitle: "Yes")
|
||||
alert.addButton(withTitle: "No")
|
||||
return alert.runModal() == .alertFirstButtonReturn
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// ApplicationAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class ApplicationAssistant {
|
||||
var mirrored: Bool
|
||||
|
||||
var shouldUnmirror: Bool
|
||||
|
||||
var delay: UInt32
|
||||
|
||||
var applicationName: String
|
||||
|
||||
var applicationPath: String
|
||||
|
||||
var scriptString: String
|
||||
|
||||
var bundleID: String
|
||||
|
||||
init() {
|
||||
mirrored = false
|
||||
shouldUnmirror = true
|
||||
delay = 0
|
||||
applicationName = ""
|
||||
applicationPath = ""
|
||||
scriptString = """
|
||||
"""
|
||||
bundleID = "xxxxxxxxxxxx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// PowerPointMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AcrobatAssistant: ApplicationAssistant {
|
||||
// let mirrored = false
|
||||
// let delay = 3.0
|
||||
// let automationType = "JavaScript" //JSX or AppleScript, osascript might not care therefore remove this
|
||||
//
|
||||
// let applicationName = "Microsoft PowerPoint"
|
||||
//
|
||||
// let applicationPath = "/Applications/Microsoft PowerPoint.app"
|
||||
//
|
||||
// let scriptString =
|
||||
// """
|
||||
// delay .5\nactivate application \"Adobe Reader\"\ntell application \"System Events\"\ntell process \"Adobe Reader\"\nclick menu item \"Cascade\" of menu 1 of menu bar item \"Window\" of menu bar 1\ndelay 1\nclick menu item \"Full Screen Mode\" of menu 1 of menu bar item \"View\" of menu bar 1\nend tell\nend tell\n
|
||||
//
|
||||
// se.processes['Finder'].windows[0].toolbars[0].actions['AXShowMenu'].perform()
|
||||
//
|
||||
//
|
||||
// var se = Application('System Events')
|
||||
//
|
||||
// seApp.keystroke('c', { using: 'command down' }) // Press ⌘C
|
||||
// delay(0.2) // adjust the delay as needed
|
||||
//
|
||||
// var app = Application.currentApplication()
|
||||
// app.includeStandardAdditions = true
|
||||
//
|
||||
// var powerpoint = Application('Microsoft PowerPoint')
|
||||
// powerpoint.activate()
|
||||
// delay(1)
|
||||
// powerpoint.activate()
|
||||
// """
|
||||
|
||||
// osascript -l JavaScript -e 'Application("iTunes").currentTrack.name()'
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = true
|
||||
|
||||
shouldUnmirror = false
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "Adobe Acrobat Reader DC"
|
||||
|
||||
applicationPath = "/Applications/Adobe Acrobat Reader DC.app"
|
||||
|
||||
bundleID = "com.adobe.Reader"
|
||||
|
||||
// tell application "Adobe Acrobat Reader DC"
|
||||
// open input -- args "pagemode=FullScreen"
|
||||
// activate
|
||||
// end tell
|
||||
|
||||
// var acrobat = Application('Adobe Acrobat Reader DC')
|
||||
//
|
||||
// acrobat.activate()
|
||||
//
|
||||
// var app = Application.currentApplication()
|
||||
// app.includeStandardAdditions = true
|
||||
//
|
||||
// acrobat.activate()
|
||||
// delay(1)
|
||||
// acrobat.activate()
|
||||
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var se = Application('System Events')
|
||||
se.keyCode(37, { using: 'command down' })
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// AcrobatWindowsAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/17/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class AcrobatWindowsAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = true
|
||||
|
||||
shouldUnmirror = false
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "Acrobat Reader Windows"
|
||||
|
||||
applicationPath = "/Applications/Acrobat Reader Windows.app"
|
||||
|
||||
//ctrl + l
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var se = Application('System Events')
|
||||
se.keyCode(108, { using: 'control down' })
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//
|
||||
// ExcelAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/19/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// PowerPointMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class KeynoteAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = false
|
||||
shouldUnmirror = true
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "Keynote"
|
||||
|
||||
applicationPath = "/Applications/Keynote.app"
|
||||
|
||||
bundleID = "com.apple.iWork.Keynote"
|
||||
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var keynote = Application('Keynote')
|
||||
|
||||
var app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
keynote.activate()
|
||||
delay(1)
|
||||
keynote.activate()
|
||||
|
||||
var doc = keynote.documents[0]
|
||||
var slide = doc.slides[0]
|
||||
doc.start()
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// PowerPointMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class LegacyKeynoteAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = false
|
||||
|
||||
shouldUnmirror = true
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "Keynote"
|
||||
|
||||
applicationPath = "/Applications/iWork '09/Keynote.app"
|
||||
|
||||
bundleID = "com.apple.iWork.Keynote"
|
||||
|
||||
|
||||
|
||||
// delay(1)
|
||||
// var keynote = Application('Keynote')
|
||||
//
|
||||
// var app = Application.currentApplication()
|
||||
// app.includeStandardAdditions = true
|
||||
//
|
||||
// keynote.activate()
|
||||
// delay(1)
|
||||
// keynote.activate()
|
||||
//
|
||||
// var doc = keynote.documents[0]
|
||||
// var slide = doc.slides[0]
|
||||
// doc.start()
|
||||
|
||||
//cmd + esc
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var keynote = Application('Keynote')
|
||||
|
||||
var app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
keynote.activate()
|
||||
delay(1)
|
||||
keynote.activate()
|
||||
|
||||
var doc = keynote.documents[0]
|
||||
var slide = doc.slides[0]
|
||||
doc.start()
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// LibreOfficeAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/7/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class LibreOfficeAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = false
|
||||
shouldUnmirror = true
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "LibreOffice"
|
||||
|
||||
applicationPath = "/Applications/LibreOffice.app"
|
||||
|
||||
bundleID = "org.libreoffice.script"
|
||||
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var lo = Application('LibreOffice')
|
||||
|
||||
lo.activate()
|
||||
|
||||
var app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
lo.activate()
|
||||
delay(1)
|
||||
lo.activate()
|
||||
|
||||
var se = Application('System Events')
|
||||
se.keyCode(96)
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// PowerPointMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PowerPointMacAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = false
|
||||
|
||||
shouldUnmirror = true
|
||||
|
||||
delay = 3
|
||||
|
||||
applicationName = "Microsoft PowerPoint"
|
||||
|
||||
applicationPath = "/Applications/Microsoft PowerPoint.app"
|
||||
|
||||
bundleID = "com.microsoft.Powerpoint"
|
||||
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var se = Application('System Events')
|
||||
se.keyCode(36, { using: 'command down' })
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// PowerPointMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class PowerPointWindowsAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = false
|
||||
|
||||
shouldUnmirror = true
|
||||
|
||||
delay = 3
|
||||
|
||||
applicationName = "Microsoft Office PowerPoint"
|
||||
|
||||
applicationPath = "/Applications/Microsoft Office PowerPoint.app"
|
||||
|
||||
//Not overriding bundleID and keeping the superclass xxxxxxxxxxxx bundleID. Automation code handles making sure Windows PPTs are active.
|
||||
|
||||
scriptString = """
|
||||
var powerpoint = Application('Microsoft Office PowerPoint')
|
||||
|
||||
var app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
delay(1)
|
||||
powerpoint.activate()
|
||||
|
||||
var se = Application('System Events')
|
||||
se.keyCode(96)
|
||||
delay (2)
|
||||
powerpoint.activate()
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// PowerPointMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class VideoAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = true
|
||||
shouldUnmirror = false
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "VLC"
|
||||
|
||||
applicationPath = "/Applications/VLC.app"
|
||||
|
||||
bundleID = "org.videolan.vlc"
|
||||
|
||||
// tell application "Adobe Acrobat Reader DC"
|
||||
// open input -- args "pagemode=FullScreen"
|
||||
// activate
|
||||
// end tell
|
||||
|
||||
// var acrobat = Application('Adobe Acrobat Reader DC')
|
||||
//
|
||||
// acrobat.activate()
|
||||
//
|
||||
// var app = Application.currentApplication()
|
||||
// app.includeStandardAdditions = true
|
||||
//
|
||||
// acrobat.activate()
|
||||
// delay(1)
|
||||
// acrobat.activate()
|
||||
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var se = Application('System Events')
|
||||
se.keyCode(3, { using: 'command down' })
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// WindowsVideoAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/17/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class WindowsVideoAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = true
|
||||
|
||||
shouldUnmirror = false
|
||||
|
||||
delay = 2
|
||||
|
||||
applicationName = "VLC Windows"
|
||||
|
||||
applicationPath = "/Applications/VLC Windows.app"
|
||||
|
||||
//alt + enter
|
||||
scriptString = """
|
||||
delay(1)
|
||||
var se = Application('System Events')
|
||||
se.keyCode(36, { using: 'option down' })
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//
|
||||
// WordMacAssistant.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/17/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class WordMacAssistant: ApplicationAssistant {
|
||||
override init() {
|
||||
super.init()
|
||||
|
||||
mirrored = true
|
||||
|
||||
shouldUnmirror = false
|
||||
|
||||
delay = 3
|
||||
|
||||
applicationName = "Microsoft Word"
|
||||
|
||||
applicationPath = "/Applications/Microsoft Word.app"
|
||||
|
||||
bundleID = "com.microsoft.Word"
|
||||
|
||||
scriptString = """
|
||||
var word = Application('Microsoft Word')
|
||||
|
||||
var app = Application.currentApplication()
|
||||
app.includeStandardAdditions = true
|
||||
|
||||
delay(1)
|
||||
word.activate()
|
||||
|
||||
var se = Application('System Events')
|
||||
se.keyCode(53, { using: 'command down' })
|
||||
delay (2)
|
||||
word.activate()
|
||||
"""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "16x16",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "32x32",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "128x128",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "256x256",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "mac",
|
||||
"size" : "512x512",
|
||||
"scale" : "2x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication">
|
||||
<connections>
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="GzC-gU-4Uq"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="masterkey" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<menu title="Main Menu" systemMenu="main" id="AYu-sK-qS6">
|
||||
<items>
|
||||
<menuItem title="Master Key" id="1Xt-HY-uBw">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Master Key" systemMenu="apple" id="uQy-DD-JDr">
|
||||
<items>
|
||||
<menuItem title="About masterkey" id="5kV-Vb-QxS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="orderFrontStandardAboutPanel:" target="-1" id="Exp-CZ-Vem"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
|
||||
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
|
||||
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
|
||||
<menuItem title="Services" id="NMo-om-nkz">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
|
||||
<menuItem title="Hide masterkey" keyEquivalent="h" id="Olw-nP-bQN">
|
||||
<connections>
|
||||
<action selector="hide:" target="-1" id="PnN-Uc-m68"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Hide Others" keyEquivalent="h" id="Vdr-fp-XzO">
|
||||
<modifierMask key="keyEquivalentModifierMask" option="YES" command="YES"/>
|
||||
<connections>
|
||||
<action selector="hideOtherApplications:" target="-1" id="VT4-aY-XCT"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem title="Show All" id="Kd2-mp-pUS">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<connections>
|
||||
<action selector="unhideAllApplications:" target="-1" id="Dhg-Le-xox"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
<menuItem isSeparatorItem="YES" id="kCx-OE-vgT"/>
|
||||
<menuItem title="Quit masterkey" keyEquivalent="q" id="4sb-4s-VLi">
|
||||
<connections>
|
||||
<action selector="terminate:" target="-1" id="Te7-pn-YzF"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
<menuItem title="Help" id="wpr-3q-Mcd">
|
||||
<modifierMask key="keyEquivalentModifierMask"/>
|
||||
<menu key="submenu" title="Help" systemMenu="help" id="F2S-fz-NVQ">
|
||||
<items>
|
||||
<menuItem title="Master Key Help" keyEquivalent="?" id="FKE-Sm-Kum">
|
||||
<connections>
|
||||
<action selector="showHelp:" target="-1" id="y7X-2Q-9no"/>
|
||||
</connections>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</menuItem>
|
||||
</items>
|
||||
</menu>
|
||||
</objects>
|
||||
</document>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string></string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Any File</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.content</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>pptxmac</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>OSIT PPTX Mac</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>pptxwin</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>OSIT PPTX Win</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>pptmac</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>OSIT PPT Mac</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>pptwin</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>OSIT PPT Win</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string></string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.6</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2019 One Sky IT. All rights reserved.</string>
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// LegacyUtilities.h
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/6/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#ifndef LegacyUtilities_h
|
||||
#define LegacyUtilities_h
|
||||
|
||||
@interface LegacyUtilities: NSObject
|
||||
|
||||
-(void) setupMirror;
|
||||
-(void) doMirror;
|
||||
|
||||
-(void) undoMirror;
|
||||
-(void) executeMirrorAction;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* LegacyUtilities_h */
|
||||
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// LegacyUtilities.m
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/6/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <Carbon/Carbon.h>
|
||||
|
||||
#import "LegacyUtilities.h"
|
||||
|
||||
@implementation LegacyUtilities : NSObject
|
||||
|
||||
-(void) setupMirror {
|
||||
CGDisplayCount numberOfActiveDspys;
|
||||
CGDisplayCount numberOfOnlineDspys;
|
||||
|
||||
CGDisplayCount numberOfTotalDspys = 2; // The number of total displays
|
||||
|
||||
CGDirectDisplayID activeDspys[] = {0,0};
|
||||
CGDirectDisplayID onlineDspys[] = {0,0};
|
||||
CGDirectDisplayID secondaryDspy;
|
||||
|
||||
CGDisplayErr activeError = CGGetActiveDisplayList (numberOfTotalDspys,activeDspys,&numberOfActiveDspys);
|
||||
|
||||
if (activeError!=0) NSLog(@"Error in obtaining active diplay list: %d\n",activeError);
|
||||
|
||||
CGDisplayErr onlineError = CGGetOnlineDisplayList (numberOfTotalDspys,onlineDspys,&numberOfOnlineDspys);
|
||||
|
||||
if (onlineError!=0) NSLog(@"Error in obtaining online diplay list: %d\n",onlineError);
|
||||
|
||||
CGDisplayConfigRef configRef;
|
||||
CGError err = CGBeginDisplayConfiguration (&configRef);
|
||||
if (err != 0) NSLog(@"Error with CGBeginDisplayConfiguration: %d\n",err);
|
||||
|
||||
if (numberOfOnlineDspys==2) {
|
||||
|
||||
if (onlineDspys[0]==CGMainDisplayID()){
|
||||
secondaryDspy = onlineDspys[1];
|
||||
} else {
|
||||
secondaryDspy = onlineDspys[0];
|
||||
}
|
||||
|
||||
CGDisplayConfigRef configRef;
|
||||
CGError err = CGBeginDisplayConfiguration (&configRef);
|
||||
if (err != 0) NSLog(@"Error with CGBeginDisplayConfiguration: %d\n",err);
|
||||
|
||||
if (numberOfActiveDspys==2) { // Displays are unmirrored -> mirror them
|
||||
err = CGConfigureDisplayMirrorOfDisplay (configRef,secondaryDspy,CGMainDisplayID());
|
||||
NSLog(@"ConfigureMirror Error: %d",err);
|
||||
}
|
||||
err = CGCompleteDisplayConfiguration (configRef,kCGConfigurePermanently);
|
||||
NSLog(@"Mirror CompleteConfig Error: %d",err);
|
||||
|
||||
} else {
|
||||
if (numberOfOnlineDspys>2) {
|
||||
printf("Cannot handle more than 2 displays at this time. %d displays detected.\n",numberOfOnlineDspys);
|
||||
} else {
|
||||
printf("No secondary display detected.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void) doMirror {
|
||||
NSEvent *thisEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:CGPointMake(0,0) modifierFlags:0xa00 timestamp:0 windowNumber:0 context:0 subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xa <<8)) data2:-1];
|
||||
CGEventRef cThisEvent = [thisEvent CGEvent];
|
||||
CGEventPost(0, (cThisEvent));
|
||||
|
||||
NSEvent *thatEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:NSMakePoint(0,0) modifierFlags:0xb00 timestamp:0 windowNumber:0 context:[NSGraphicsContext currentContext] subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xb <<8)) data2:-1];
|
||||
CGEventRef cThatEvent = [thatEvent CGEvent];
|
||||
CGEventPost(0, (cThatEvent));
|
||||
}
|
||||
|
||||
-(void) undoMirror {
|
||||
enum MirrorMode {
|
||||
off,
|
||||
} mode;
|
||||
|
||||
mode = off;
|
||||
|
||||
CGDisplayCount numberOfActiveDspys;
|
||||
CGDisplayCount numberOfOnlineDspys;
|
||||
|
||||
CGDisplayCount numberOfTotalDspys = 2;
|
||||
|
||||
CGDirectDisplayID activeDspys[] = {0,0};
|
||||
CGDirectDisplayID onlineDspys[] = {0,0};
|
||||
CGDirectDisplayID secondaryDspy;
|
||||
|
||||
CGDisplayErr activeError = CGGetActiveDisplayList (numberOfTotalDspys,activeDspys,&numberOfActiveDspys);
|
||||
|
||||
if (activeError!=0) NSLog(@"Error in obtaining active diplay list: %d\n",activeError);
|
||||
|
||||
CGDisplayErr onlineError = CGGetOnlineDisplayList (numberOfTotalDspys,onlineDspys,&numberOfOnlineDspys);
|
||||
|
||||
if (onlineError!=0) NSLog(@"Error in obtaining online diplay list: %d\n",onlineError);
|
||||
|
||||
if (numberOfOnlineDspys==2) { // Online displays = physical displays regardless of mirror status
|
||||
|
||||
if (onlineDspys[0]==CGMainDisplayID()){
|
||||
secondaryDspy = onlineDspys[1];
|
||||
} else {
|
||||
secondaryDspy = onlineDspys[0];
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case off:
|
||||
if (numberOfActiveDspys!=2) // Active displays = software displays (mirror = 1)
|
||||
[self executeMirrorAction];
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (numberOfOnlineDspys>2) {
|
||||
printf("Cannot handle more than 2 displays at this time. %d displays detected.\n",numberOfOnlineDspys);
|
||||
} else {
|
||||
printf("No secondary display detected.\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void) executeMirrorAction {
|
||||
NSEvent *thisEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:CGPointMake(0,0) modifierFlags:0xa00 timestamp:0 windowNumber:0 context:0 subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xa <<8)) data2:-1];
|
||||
CGEventRef cThisEvent = [thisEvent CGEvent];
|
||||
CGEventPost(0, (cThisEvent));
|
||||
|
||||
NSEvent *thatEvent = [NSEvent otherEventWithType:NSEventTypeSystemDefined location:NSMakePoint(0,0) modifierFlags:0xb00 timestamp:0 windowNumber:0 context:[NSGraphicsContext currentContext] subtype:8 data1:(NX_KEYTYPE_VIDMIRROR<<16|(0xb <<8)) data2:-1];
|
||||
CGEventRef cThatEvent = [thatEvent CGEvent];
|
||||
CGEventPost(0, (cThatEvent));
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Utilities.swift
|
||||
// masterkey
|
||||
//
|
||||
// Created by Ian Kohl on 4/6/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Utilities {
|
||||
func startRecording(filename: String, num_screens: Int) -> String
|
||||
{
|
||||
//Create output filename
|
||||
let timestamp = String(NSDate().timeIntervalSince1970)
|
||||
let home = NSHomeDirectory()
|
||||
let recording_filename = timestamp + filename + ".mkv"
|
||||
let recording_path = "\(home)/recordings/\(recording_filename)"
|
||||
|
||||
let device_string = "\"\(num_screens):0\""
|
||||
|
||||
//Get Devices
|
||||
//ffmpeg -f avfoundation -list_devices true -i ""
|
||||
|
||||
//Start recording
|
||||
//ffmpeg -f avfoundation -i "2:<audio device index>" ~/recordings/filename.mkv
|
||||
|
||||
let task:Process = Process()
|
||||
let pipe:Pipe = Pipe()
|
||||
|
||||
task.launchPath = "/usr/local/bin/ffmpeg"
|
||||
task.arguments = ["-f","avfoundation","-i",device_string,recording_path]
|
||||
task.standardOutput = pipe
|
||||
task.launch()
|
||||
|
||||
let handle = pipe.fileHandleForReading
|
||||
let data = handle.readDataToEndOfFile()
|
||||
let result_s = String(data: data, encoding: String.Encoding.utf8)!
|
||||
return result_s
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Microsoft Office PowerPoint
|
||||
VLC Windows
|
||||
Acrobat Reader Windows
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Use this file to import your target's public headers that you would like to expose to Swift.
|
||||
//
|
||||
|
||||
#import "LegacyUtilities.h"
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
</plist>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSAppleEventsUsageDescription</key>
|
||||
<string></string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// masterkeyTests.swift
|
||||
// masterkeyTests
|
||||
//
|
||||
// Created by Ian Kohl on 2/28/19.
|
||||
// Copyright © 2019 One Sky IT. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import masterkey
|
||||
|
||||
class masterkeyTests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
315
index.js
@@ -1,315 +0,0 @@
|
||||
const { app, BrowserWindow, ipcMain, shell, systemPreferences } = require('electron');
|
||||
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
//const http = require('http');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const process = require('process');
|
||||
//const request = require('request');
|
||||
//const url = require('url');
|
||||
// const usb = require('usb') // Compiled with an old version of Node.js
|
||||
|
||||
let home_directory = require('os').homedir();
|
||||
console.log('Home: '+home_directory);
|
||||
|
||||
let tmp_directory = require('os').tmpdir();
|
||||
console.log('Temporary: '+tmp_directory);
|
||||
|
||||
let config = null;
|
||||
let host_file_cache_path = null;
|
||||
let host_file_temp_path = null;
|
||||
|
||||
console.log(os.type());
|
||||
console.log(process.getSystemVersion());
|
||||
|
||||
|
||||
if (os.type == 'Darwin') {
|
||||
if (systemPreferences.getMediaAccessStatus('microphone') != 'granted') {
|
||||
systemPreferences.askForMediaAccess('microphone');
|
||||
} else {
|
||||
console.log(systemPreferences.getMediaAccessStatus('microphone'));
|
||||
}
|
||||
|
||||
if (systemPreferences.getMediaAccessStatus('camera') != 'granted') {
|
||||
systemPreferences.askForMediaAccess('camera');
|
||||
} else {
|
||||
console.log(systemPreferences.getMediaAccessStatus('camera'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createWindow () {
|
||||
// Create the browser window.
|
||||
win = new BrowserWindow({
|
||||
width: 1500, // 1500 1280
|
||||
height: 1024, // 1024
|
||||
backgroundColor: '#aaa',
|
||||
icon: './app/img/favicon.ico',
|
||||
webPreferences: {
|
||||
contextIsolation: false,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true
|
||||
}
|
||||
})
|
||||
|
||||
// win.setMinimumSize(1024, 768);
|
||||
win.setMinimumSize(1280, 768);
|
||||
|
||||
//win.setFullScreenable(false)
|
||||
win.FullScreenable = false;
|
||||
|
||||
// win.webContents.session.clearStorageData(['filesystem']); // Does this do anything???
|
||||
|
||||
// and load the index.html of the app.
|
||||
win.loadFile('app/index.html');
|
||||
|
||||
// Open the DevTools.
|
||||
win.webContents.openDevTools();
|
||||
|
||||
// Emitted when the window is closed.
|
||||
win.on('closed', () => {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
win = null;
|
||||
})
|
||||
|
||||
win.on('minimize', () => {
|
||||
//win.restore();
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.on('ready', createWindow);
|
||||
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', () => {
|
||||
// On macOS it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
app.on('activate', () => {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.location_files
|
||||
if (win === null) {
|
||||
createWindow();
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Import config data
|
||||
// Updated 2022-04-16
|
||||
ipcMain.handle('import_config', async (event, config_data) => {
|
||||
console.log('*** Electron IPC Main: import_config() ***');
|
||||
// console.log('ipcMain on download_file: api_base_url='+api_base_url+' | api_temporary_token='+api_temporary_token);
|
||||
console.log('ipcMain on import_config:');
|
||||
console.log(config_data);
|
||||
|
||||
config = config_data;
|
||||
|
||||
host_file_cache_path = config.host_file_cache_path;
|
||||
host_file_temp_path = config.host_file_temp_path;
|
||||
|
||||
if (fs.existsSync(host_file_cache_path)) {
|
||||
console.log('Host file cache path exists: '+host_file_cache_path);
|
||||
} else {
|
||||
fs.mkdirSync(host_file_cache_path, true);
|
||||
console.log('Host file cache path created: '+host_file_cache_path);
|
||||
}
|
||||
|
||||
if (fs.existsSync(host_file_temp_path)) {
|
||||
console.log('Host file temp path exists: '+host_file_temp_path);
|
||||
} else {
|
||||
fs.mkdirSync(host_file_temp_path, true);
|
||||
console.log('Host file temp path created: '+host_file_temp_path);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
// Download file to path
|
||||
// full_save_path should be the full path that includes the filename
|
||||
// Updated 2022-03-09
|
||||
ipcMain.handle('download_file', async (event, api_base_url, api_endpoint, full_save_path) => {
|
||||
console.log('*** Electron IPC Main: download_file() ***');
|
||||
// console.log('ipcMain on download_file: api_base_url='+api_base_url+' | api_temporary_token='+api_temporary_token);
|
||||
console.log('ipcMain on download_file: api_base_url='+api_base_url);
|
||||
console.log('ipcMain download and save file: HTTP '+api_endpoint+' -> FILE '+full_save_path);
|
||||
|
||||
let result = await download_file(api_base_url, api_endpoint, full_save_path);
|
||||
|
||||
console.log(result);
|
||||
console.log('End: Electron IPC Main: download_file()');
|
||||
// return 'Return from Electron IPC Main download_file()';
|
||||
return result;
|
||||
});
|
||||
|
||||
|
||||
// Download file to path
|
||||
// full_save_path should be the full path that includes the filename
|
||||
// Updated 2022-03-09
|
||||
async function download_file(api_base_url, api_endpoint, full_save_path) {
|
||||
console.log('*** node.js: download_file() ***');
|
||||
|
||||
axios.defaults.baseURL = api_base_url;
|
||||
axios.defaults.headers.common['Access-Control-Allow-Origin'] = config.access_control_allow_origin; // '*'; // app_config.access_control_allow_origin;
|
||||
axios.defaults.headers.common['content-type'] = 'application/json';
|
||||
axios.defaults.headers.common['x-aether-api-key'] = config.api_secret_key; // 'dFP6J9DVj9hUgIMn-fNIqg'; // api_secret_key;
|
||||
axios.defaults.headers.common['x-account-id'] = config.account_id; // '_XY7DXtc9MY'; // account_id;
|
||||
|
||||
const url = api_endpoint;
|
||||
|
||||
let download_result = await axios({
|
||||
method: 'get',
|
||||
url: url,
|
||||
responseType: 'stream' /* responseType must be stream */
|
||||
}).then(function (response) {
|
||||
console.log('Downloading...');
|
||||
// response.data.pipe(fs.createWriteStream(full_save_path));
|
||||
// return true;
|
||||
|
||||
// response.data.pipe(fs.createWriteStream(full_save_path));
|
||||
// return true;
|
||||
|
||||
const writer = fs.createWriteStream(full_save_path);
|
||||
|
||||
console.log('Write stream created');
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
response.data.pipe(writer);
|
||||
let error = null;
|
||||
writer.on('error', err => {
|
||||
console.log('Writer error!');
|
||||
error = err;
|
||||
console.log(error);
|
||||
writer.close();
|
||||
reject(err);
|
||||
});
|
||||
writer.on('close', () => {
|
||||
console.log('Writer close!');
|
||||
if (!error) {
|
||||
resolve(true);
|
||||
}
|
||||
//no need to call the reject here, as it will have been called in the
|
||||
//'error' stream;
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(`Error downloading! Endpoint: ${api_endpoint}`);
|
||||
// console.log(error);
|
||||
// console.log(error.response);
|
||||
if (error.response && error.response.status === 404) {
|
||||
return null; // Returning null since there were no results
|
||||
}
|
||||
console.log(`Response Status: ${error.response.status}; Status Text: ${error.response.statusText}`);
|
||||
return false; // Returning false since something may have gone wrong. Also more in line with what the API returns.
|
||||
});
|
||||
|
||||
console.log(download_result);
|
||||
console.log('End: download_file()');
|
||||
// return 'Return from download_file()';
|
||||
return download_result;
|
||||
}
|
||||
|
||||
|
||||
ipcMain.handle('open_hash_file_to_temp', async (event, host_file_cache_path, hash, host_file_temp_path, filename) => {
|
||||
console.log('*** Electron IPC Main: open_hash_file_to_temp() ***');
|
||||
console.log('ipcMain on open_hash_file_to_temp');
|
||||
console.log(`ipcMain open hash file from temp directory: ${host_file_cache_path} -> ${host_file_temp_path}/${filename}`);
|
||||
|
||||
// NOTE: This may be needed later? Uncomment if paths are relative to working directory.
|
||||
// let cache_file_path = path.join(process.cwd(), host_file_cache_path);
|
||||
let cache_file_path = host_file_cache_path;
|
||||
console.log(cache_file_path);
|
||||
|
||||
let hash_filename = hash+'.file';
|
||||
let full_cache_file_path = path.join(cache_file_path, hash_filename);
|
||||
console.log(full_cache_file_path);
|
||||
|
||||
// NOTE: This may be needed later? Uncomment if paths are relative to working directory.
|
||||
// open_temp_file_path = path.join(process.cwd(), host_file_temp_path, filename); // 'temp/'
|
||||
open_temp_file_path = path.join(host_file_temp_path, filename); // 'temp/'
|
||||
console.log(open_temp_file_path);
|
||||
|
||||
if (fs.existsSync(open_temp_file_path)) {
|
||||
console.log('A file with the same name already exists in the local temp directory: '+open_temp_file_path);
|
||||
// NOTE: Should the file be checked to see if it has changed from the hashed cache version???
|
||||
// NOTE: What if they made changes to the file locally in temp? The changed file would be used since a new copy is not being made.
|
||||
// NOTE: It might make sense for this to be a configurable option depending on the group. Some do not allow changes. This helps enforce that.
|
||||
}
|
||||
|
||||
if (fs.existsSync(full_cache_file_path)) {
|
||||
console.log(`Hashed file exists in cache: ${full_cache_file_path}`);
|
||||
console.log(`Copying file to temp: ${open_temp_file_path}`);
|
||||
try {
|
||||
await fs.copyFileSync(full_cache_file_path, open_temp_file_path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
// console.log('Creating file link: '+open_temp_file_path);
|
||||
// fs.linkSync(full_cache_file_path, open_temp_file_path);
|
||||
} else {
|
||||
console.log(`Hashed file not found in cache: ${full_cache_file_path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await shell.openPath(open_temp_file_path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(true);
|
||||
console.log('End: Electron IPC Main: open_hash_file_to_temp()');
|
||||
// return 'Return from Electron IPC Main open_hash_file_to_temp()';
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
ipcMain.handle('open_local_file', async (event, local_file_path, filename, use_cwd=true) => {
|
||||
console.log('*** Electron IPC Main: open_local_file() ***');
|
||||
console.log('ipcMain on open_local_file');
|
||||
console.log(`ipcMain open local file from directory: ${local_file_path}/${filename}`);
|
||||
|
||||
let full_local_file_path = null;
|
||||
|
||||
if (use_cwd) {
|
||||
full_local_file_path = path.join(process.cwd(), local_file_path, filename);
|
||||
console.log(full_local_file_path);
|
||||
} else {
|
||||
full_local_file_path = path.join(local_file_path, filename);
|
||||
console.log(full_local_file_path);
|
||||
}
|
||||
|
||||
if (fs.existsSync(full_local_file_path)) {
|
||||
console.log(`Local file exists: ${full_local_file_path}`);
|
||||
} else {
|
||||
console.log(`Local file not found: ${full_local_file_path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await shell.openPath(full_local_file_path);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log('End: Electron IPC Main: open_local_file()');
|
||||
return true;
|
||||
});
|
||||
4931
package-lock.json
generated
40
package.json
@@ -1,27 +1,25 @@
|
||||
{
|
||||
"name": "aether_app_native",
|
||||
"productName": "Aether App",
|
||||
"version": "3.0.0",
|
||||
"description": "One Sky IT's Native Aether App",
|
||||
"main": "index.js",
|
||||
"name": "aether_app_native_electron",
|
||||
"version": "3.0.20",
|
||||
"description": "AE Native Launcher V3",
|
||||
"main": "dist/main/index.js",
|
||||
"scripts": {
|
||||
"start": "electron .",
|
||||
"start_nogpu": "electron . --disable-gpu",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"fs": "0.0.1-security",
|
||||
"os": "^0.1.1",
|
||||
"path": "^0.12.7",
|
||||
"request": "^2.88.2",
|
||||
"screen": "^0.2.10",
|
||||
"usb": "^1.6.2"
|
||||
"postinstall": "node scripts/patch-packager-unzip.js",
|
||||
"start": "tsc && electron .",
|
||||
"dev": "tsc && electron .",
|
||||
"build": "tsc",
|
||||
"watch": "tsc -w",
|
||||
"package:linux": "tsc && electron-packager . aether_launcher --platform=linux --arch=x64 --out=builds --overwrite --prune=true",
|
||||
"package:mac": "tsc && electron-packager . aether_launcher --platform=darwin --arch=x64,arm64 --out=builds --overwrite --prune=true --icon=resources/img/osit_logo.icns"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "^17.4.0",
|
||||
"electron-packager": "^15.4.0"
|
||||
"@types/node": "^22.19.0",
|
||||
"electron": "^42.0.1",
|
||||
"@electron/packager": "^20.0.0",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
resources/bin/display_control
Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 9.5 KiB After Width: | Height: | Size: 9.5 KiB |
BIN
resources/img/osit_logo.icns
Normal file
BIN
resources/img/osit_logo_150.png
Normal file
|
After Width: | Height: | Size: 9.5 KiB |
53
scripts/build-display-control.sh
Executable file
@@ -0,0 +1,53 @@
|
||||
#!/bin/bash
|
||||
# scripts/build-display-control.sh
|
||||
# Compile the display_control binary for macOS.
|
||||
#
|
||||
# Requirements: Xcode Command Line Tools
|
||||
# xcode-select --install
|
||||
#
|
||||
# Run this on a Mac. Commit the resulting binary to resources/bin/
|
||||
# so it is bundled into the packaged Electron app without any Homebrew dependency.
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SRC="$SCRIPT_DIR/display_control.m"
|
||||
OUT_DIR="$REPO_ROOT/resources/bin"
|
||||
OUT_BIN="$OUT_DIR/display_control"
|
||||
|
||||
if ! command -v clang &>/dev/null; then
|
||||
echo "ERROR: clang not found."
|
||||
echo "Install Xcode Command Line Tools: xcode-select --install"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
TMP_X86="$OUT_DIR/display_control_x86_64"
|
||||
TMP_ARM="$OUT_DIR/display_control_arm64"
|
||||
|
||||
echo "Building display_control (x86_64)..."
|
||||
clang -arch x86_64 -framework Cocoa -framework Carbon \
|
||||
-o "$TMP_X86" "$SRC"
|
||||
|
||||
echo "Building display_control (arm64)..."
|
||||
clang -arch arm64 -framework Cocoa -framework Carbon \
|
||||
-o "$TMP_ARM" "$SRC"
|
||||
|
||||
echo "Linking universal binary..."
|
||||
lipo -create -output "$OUT_BIN" "$TMP_X86" "$TMP_ARM"
|
||||
rm "$TMP_X86" "$TMP_ARM"
|
||||
|
||||
chmod +x "$OUT_BIN"
|
||||
|
||||
echo ""
|
||||
echo "Built universal binary: $OUT_BIN"
|
||||
lipo -archs "$OUT_BIN"
|
||||
echo ""
|
||||
echo "Test it:"
|
||||
echo " $OUT_BIN status"
|
||||
echo " $OUT_BIN extend"
|
||||
echo " $OUT_BIN mirror"
|
||||
echo ""
|
||||
echo "Once verified, commit resources/bin/display_control to the repo."
|
||||
149
scripts/display_control.m
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* display_control.m
|
||||
* Native macOS CLI for programmatic display mirror/extend control.
|
||||
*
|
||||
* Derived from the OSIT MasterKey app (LegacyUtilities.m, Ian Kohl 2019).
|
||||
* Uses CoreGraphics APIs — no external dependencies (no displayplacer/Homebrew required).
|
||||
*
|
||||
* Build: run scripts/build-display-control.sh on a Mac (requires Xcode Command Line Tools)
|
||||
* Usage: display_control <mirror|extend|status>
|
||||
*/
|
||||
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include <Carbon/Carbon.h>
|
||||
|
||||
#define MAX_DISPLAYS 8
|
||||
|
||||
static int mirror_displays(void) {
|
||||
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||||
CGDisplayCount numOnline = 0;
|
||||
|
||||
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||||
|
||||
if (numOnline < 2) {
|
||||
fprintf(stderr, "No secondary display detected (%u online).\n", numOnline);
|
||||
return 1;
|
||||
}
|
||||
|
||||
CGDirectDisplayID mainID = CGMainDisplayID();
|
||||
|
||||
CGDisplayConfigRef config;
|
||||
CGError err = CGBeginDisplayConfiguration(&config);
|
||||
if (err != kCGErrorSuccess) {
|
||||
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
BOOL any_configured = NO;
|
||||
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||||
if (onlineDspys[i] != mainID) {
|
||||
err = CGConfigureDisplayMirrorOfDisplay(config, onlineDspys[i], mainID);
|
||||
if (err != kCGErrorSuccess) {
|
||||
fprintf(stderr, "CGConfigureDisplayMirrorOfDisplay failed: %d\n", err);
|
||||
CGCancelDisplayConfiguration(config);
|
||||
return 1;
|
||||
}
|
||||
any_configured = YES;
|
||||
}
|
||||
}
|
||||
|
||||
if (!any_configured) {
|
||||
CGCancelDisplayConfiguration(config);
|
||||
fprintf(stderr, "No secondary displays to mirror.\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||||
if (err != kCGErrorSuccess) {
|
||||
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Mirrored %u display(s).\n", numOnline - 1);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int extend_displays(void) {
|
||||
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||||
CGDirectDisplayID activeDspys[MAX_DISPLAYS] = {0};
|
||||
CGDisplayCount numOnline = 0, numActive = 0;
|
||||
|
||||
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||||
CGGetActiveDisplayList(MAX_DISPLAYS, activeDspys, &numActive);
|
||||
|
||||
if (numOnline < 2) {
|
||||
fprintf(stderr, "No secondary display detected (%u online).\n", numOnline);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (numActive >= numOnline) {
|
||||
printf("Displays already extended.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
CGDirectDisplayID mainID = CGMainDisplayID();
|
||||
|
||||
CGDisplayConfigRef config;
|
||||
CGError err = CGBeginDisplayConfiguration(&config);
|
||||
if (err != kCGErrorSuccess) {
|
||||
fprintf(stderr, "CGBeginDisplayConfiguration failed: %d\n", err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
for (CGDisplayCount i = 0; i < numOnline; i++) {
|
||||
if (onlineDspys[i] != mainID) {
|
||||
// kCGNullDirectDisplay as master = un-mirror (extend)
|
||||
err = CGConfigureDisplayMirrorOfDisplay(config, onlineDspys[i], kCGNullDirectDisplay);
|
||||
if (err != kCGErrorSuccess) {
|
||||
fprintf(stderr, "CGConfigureDisplayMirrorOfDisplay(null) failed: %d\n", err);
|
||||
CGCancelDisplayConfiguration(config);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = CGCompleteDisplayConfiguration(config, kCGConfigurePermanently);
|
||||
if (err != kCGErrorSuccess) {
|
||||
fprintf(stderr, "CGCompleteDisplayConfiguration failed: %d\n", err);
|
||||
return 1;
|
||||
}
|
||||
|
||||
printf("Displays extended.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void print_status(void) {
|
||||
CGDirectDisplayID onlineDspys[MAX_DISPLAYS] = {0};
|
||||
CGDirectDisplayID activeDspys[MAX_DISPLAYS] = {0};
|
||||
CGDisplayCount numOnline = 0, numActive = 0;
|
||||
|
||||
CGGetOnlineDisplayList(MAX_DISPLAYS, onlineDspys, &numOnline);
|
||||
CGGetActiveDisplayList(MAX_DISPLAYS, activeDspys, &numActive);
|
||||
|
||||
printf("online=%u active=%u %s\n",
|
||||
numOnline, numActive,
|
||||
(numOnline > 1 && numActive < numOnline) ? "mirrored" : "extended");
|
||||
}
|
||||
|
||||
int main(int argc, const char * argv[]) {
|
||||
@autoreleasepool {
|
||||
if (argc < 2) {
|
||||
fprintf(stderr, "Usage: display_control <mirror|extend|status>\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *cmd = argv[1];
|
||||
|
||||
if (strcmp(cmd, "mirror") == 0) {
|
||||
return mirror_displays();
|
||||
} else if (strcmp(cmd, "extend") == 0) {
|
||||
return extend_displays();
|
||||
} else if (strcmp(cmd, "status") == 0) {
|
||||
print_status();
|
||||
return 0;
|
||||
} else {
|
||||
fprintf(stderr, "Unknown command: %s\nUsage: display_control <mirror|extend|status>\n", cmd);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
23
scripts/patch-packager-unzip.js
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
// extract-zip/yauzl streams hang on Node 26 (no data events emitted).
|
||||
// bsdtar (libarchive) handles both Linux and macOS zips including chained symlinks in .app bundles.
|
||||
const { writeFileSync, existsSync } = require('fs');
|
||||
const { resolve } = require('path');
|
||||
|
||||
const target = resolve('node_modules/@electron/packager/dist/unzip.js');
|
||||
if (!existsSync(target)) {
|
||||
console.log('patch-packager-unzip: target not found, skipping');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const patched = `import { execSync } from 'node:child_process';
|
||||
// extract-zip/yauzl streams are broken on Node 26; use bsdtar (libarchive) instead.
|
||||
// bsdtar correctly handles chained symlinks in macOS .app bundles that 7z refuses.
|
||||
export async function extractElectronZip(zipPath, targetDir) {
|
||||
execSync(\`bsdtar -xf "\${zipPath}" -C "\${targetDir}"\`, { stdio: 'pipe' });
|
||||
}
|
||||
//# sourceMappingURL=unzip.js.map
|
||||
`;
|
||||
|
||||
writeFileSync(target, patched);
|
||||
console.log('patch-packager-unzip: patched @electron/packager/dist/unzip.js to use bsdtar');
|
||||
94
scripts/remote-build-display-control.sh
Executable file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/remote-build-display-control.sh
|
||||
# Build the display_control universal binary on a remote Mac via SSH,
|
||||
# then pull the result back to resources/bin/display_control.
|
||||
#
|
||||
# Use this from the Linux workstation when you don't have a Mac locally.
|
||||
# The target Mac must have Xcode Command Line Tools installed.
|
||||
#
|
||||
# USAGE:
|
||||
# ./scripts/remote-build-display-control.sh # uses default Mac (laptop 01)
|
||||
# ./scripts/remote-build-display-control.sh 192.168.32.102
|
||||
#
|
||||
# The default IP is laptop 01 — the designated build Mac.
|
||||
# Change DEFAULT_IP below if Xcode moves to a different laptop.
|
||||
#
|
||||
# SSH key must already be installed on the target:
|
||||
# ssh-copy-id "speaker ready"@192.168.32.101
|
||||
|
||||
set -eo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SRC="$SCRIPT_DIR/display_control.m"
|
||||
OUT_BIN="$REPO_ROOT/resources/bin/display_control"
|
||||
|
||||
SSH_USER="speaker ready"
|
||||
DEFAULT_IP="192.168.32.101"
|
||||
IP="${1:-$DEFAULT_IP}"
|
||||
|
||||
REMOTE_TMP="/tmp/ae_display_control_build"
|
||||
|
||||
echo "═══════════════════════════════════════════════"
|
||||
echo " Remote build: display_control"
|
||||
echo " Build Mac: $SSH_USER @ $IP"
|
||||
echo "═══════════════════════════════════════════════"
|
||||
|
||||
# ── Step 1: Copy source to remote ────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Step 1/4 — Copying source to $IP..."
|
||||
ssh "$SSH_USER@$IP" "mkdir -p $REMOTE_TMP"
|
||||
ssh "$SSH_USER@$IP" "cat > $REMOTE_TMP/display_control.m" < "$SRC"
|
||||
|
||||
# ── Step 2: Build universal binary on remote ──────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Step 2/4 — Building on $IP..."
|
||||
ssh "$SSH_USER@$IP" bash << 'ENDSSH'
|
||||
set -e
|
||||
REMOTE_TMP="/tmp/ae_display_control_build"
|
||||
SRC="$REMOTE_TMP/display_control.m"
|
||||
TMP_X86="$REMOTE_TMP/display_control_x86_64"
|
||||
TMP_ARM="$REMOTE_TMP/display_control_arm64"
|
||||
OUT="$REMOTE_TMP/display_control"
|
||||
|
||||
echo " Compiling x86_64..."
|
||||
clang -arch x86_64 -framework Cocoa -framework Carbon -o "$TMP_X86" "$SRC"
|
||||
|
||||
echo " Compiling arm64..."
|
||||
clang -arch arm64 -framework Cocoa -framework Carbon -o "$TMP_ARM" "$SRC"
|
||||
|
||||
echo " Linking universal binary..."
|
||||
lipo -create -output "$OUT" "$TMP_X86" "$TMP_ARM"
|
||||
rm "$TMP_X86" "$TMP_ARM"
|
||||
chmod +x "$OUT"
|
||||
|
||||
echo " Archs: $(lipo -archs "$OUT")"
|
||||
ENDSSH
|
||||
|
||||
# ── Step 3: Pull binary back ──────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Step 3/4 — Pulling binary to resources/bin/..."
|
||||
mkdir -p "$(dirname "$OUT_BIN")"
|
||||
ssh "$SSH_USER@$IP" "cat $REMOTE_TMP/display_control" > "$OUT_BIN"
|
||||
chmod +x "$OUT_BIN"
|
||||
|
||||
# ── Step 4: Clean up remote ───────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "Step 4/4 — Cleaning up remote..."
|
||||
ssh "$SSH_USER@$IP" "rm -rf $REMOTE_TMP"
|
||||
|
||||
# ── Done ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════"
|
||||
echo " Built: $OUT_BIN"
|
||||
file "$OUT_BIN"
|
||||
echo ""
|
||||
echo " If both archs shown above, you're good:"
|
||||
echo " git add resources/bin/display_control"
|
||||
echo " git commit -m \"build: update display_control binary (universal)\""
|
||||
echo "═══════════════════════════════════════════════"
|
||||
75
src/main/api_client.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { SeedConfig } from '../shared/types';
|
||||
|
||||
export async function fetchFullConfig(seed: SeedConfig): Promise<any> {
|
||||
const apiUrls = [
|
||||
seed.onsite_api_base_url,
|
||||
seed.primary_api_base_url,
|
||||
seed.backup_api_base_url
|
||||
].filter(url => url !== null && url !== undefined) as string[];
|
||||
|
||||
let lastError: any = null;
|
||||
|
||||
for (const baseUrl of apiUrls) {
|
||||
try {
|
||||
console.log(`Bootstrap: Attempting connection to ${baseUrl}...`);
|
||||
|
||||
// --- STEP 1: Get Device Config ---
|
||||
const deviceUrl = `${baseUrl}/v3/crud/event_device/${seed.event_device_id}`;
|
||||
const deviceResponse = await fetch(deviceUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-aether-api-key': seed.aether_api_key,
|
||||
'x-no-account-id': 'bypass'
|
||||
},
|
||||
});
|
||||
|
||||
if (!deviceResponse.ok) {
|
||||
throw new Error(`Device lookup failed (${deviceResponse.status})`);
|
||||
}
|
||||
|
||||
const deviceResult = await deviceResponse.json();
|
||||
const deviceData = deviceResult.data || deviceResult;
|
||||
|
||||
// Use 'app_base_url' as the FQDN for the site lookup
|
||||
const fqdn = deviceData.app_base_url || 'native-demo.oneskyit.com';
|
||||
|
||||
// --- STEP 2: Get Site Context ---
|
||||
const searchUrl = `${baseUrl}/v3/crud/site_domain/search?limit=1`;
|
||||
const siteResponse = await fetch(searchUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'x-aether-api-key': seed.aether_api_key,
|
||||
'x-account-id': deviceData.account_id_random || deviceData.account_id || ''
|
||||
},
|
||||
body: JSON.stringify({
|
||||
and: [{ field: 'fqdn', op: 'eq', value: fqdn }]
|
||||
})
|
||||
});
|
||||
|
||||
if (!siteResponse.ok) {
|
||||
throw new Error(`Site context lookup failed (${siteResponse.status})`);
|
||||
}
|
||||
|
||||
const siteResult = await siteResponse.json();
|
||||
const siteDomain = (siteResult.data && siteResult.data.length > 0) ? siteResult.data[0] : null;
|
||||
|
||||
console.log(`Bootstrap Success using ${baseUrl}`);
|
||||
|
||||
return {
|
||||
...siteDomain,
|
||||
native_device: deviceData,
|
||||
aether_api_key: seed.aether_api_key // Include the key for frontend use
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`Bootstrap failed for ${baseUrl}: `, error);
|
||||
lastError = error;
|
||||
continue; // Try next URL
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Bootstrap Critical Failure: All API endpoints exhausted.', lastError);
|
||||
return null;
|
||||
}
|
||||
30
src/main/config_loader.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { SeedConfig } from '../shared/types';
|
||||
|
||||
export async function loadSeedConfig(): Promise<SeedConfig | null> {
|
||||
// For development, we look in the home directory
|
||||
const configPath = path.join(os.homedir(), 'seed.json');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
console.log(`Seed config not found at: ${configPath}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(configPath, 'utf-8');
|
||||
const config = JSON.parse(data) as SeedConfig;
|
||||
|
||||
// Basic validation
|
||||
if (!config.event_device_id) {
|
||||
console.error('Invalid seed config: missing event_device_id');
|
||||
return null;
|
||||
}
|
||||
|
||||
return config;
|
||||
} catch (error) {
|
||||
console.error('Error loading seed config:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
188
src/main/file_handlers.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as crypto from 'crypto';
|
||||
import { exec } from 'child_process';
|
||||
import axios from 'axios';
|
||||
import { expandPath } from './file_utils';
|
||||
|
||||
let endpoints_in_progress: string[] = [];
|
||||
|
||||
export function registerFileHandlers() {
|
||||
// Flexible organization: [root]/[prefix_len-char-prefix]/[hash].file
|
||||
function get_organized_hashed_path(root: string, hash: string, prefix_len: number = 2) {
|
||||
const expanded_root = expandPath(root);
|
||||
const prefix = hash.substring(0, Math.max(1, Math.min(prefix_len, 8)));
|
||||
const dir = path.join(expanded_root, prefix);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
return path.join(dir, `${hash}.file`);
|
||||
}
|
||||
|
||||
ipcMain.handle('native:check-cache', async (event, { cache_root, hash, hash_prefix_length = 2, verify_hash = false }) => {
|
||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
|
||||
if (!fs.existsSync(full_path)) return false;
|
||||
|
||||
if (verify_hash) {
|
||||
try {
|
||||
const file_buffer = fs.readFileSync(full_path);
|
||||
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||
return actual_hash === hash;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
ipcMain.handle('native:download-to-cache', async (event, { url, cache_root, hash, api_key, account_id, hash_prefix_length = 2 }) => {
|
||||
const full_path = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
const tmp_path = `${full_path}.tmp`;
|
||||
|
||||
if (endpoints_in_progress.includes(url)) return { success: true, status: 'in_progress' };
|
||||
|
||||
// 1. If final file exists, skip
|
||||
if (fs.existsSync(full_path)) return { success: true, path: full_path, status: 'exists' };
|
||||
|
||||
// 2. Handle stale .tmp files (Legacy "Trust No One" pattern)
|
||||
if (fs.existsSync(tmp_path)) {
|
||||
const stats = fs.statSync(tmp_path);
|
||||
const age_ms = Date.now() - stats.mtimeMs;
|
||||
// If the tmp file is older than 5 minutes, assume previous download crashed and delete it
|
||||
if (age_ms > 5 * 60 * 1000) {
|
||||
console.log(`Native: Deleting stale temp file (${Math.round(age_ms/1000)}s old)`);
|
||||
fs.unlinkSync(tmp_path);
|
||||
} else {
|
||||
return { success: true, status: 'in_progress', detail: 'fresh_tmp_exists' };
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Native: Hardened Download -> ${full_path}`);
|
||||
|
||||
try {
|
||||
endpoints_in_progress.push(url);
|
||||
const response = await axios({
|
||||
method: 'get', url, responseType: 'stream',
|
||||
headers: {
|
||||
'x-aether-api-key': api_key,
|
||||
'x-account-id': account_id || ''
|
||||
}
|
||||
});
|
||||
|
||||
const writer = fs.createWriteStream(tmp_path);
|
||||
response.data.pipe(writer);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
writer.on('finish', () => resolve());
|
||||
writer.on('error', reject);
|
||||
});
|
||||
|
||||
// 3. Verify Integrity before renaming (The "Trust No One" Check)
|
||||
const file_buffer = fs.readFileSync(tmp_path);
|
||||
const actual_hash = crypto.createHash('sha256').update(file_buffer).digest('hex');
|
||||
|
||||
if (actual_hash !== hash) {
|
||||
console.error(`Native: Hash Mismatch! Expected ${hash}, got ${actual_hash}`);
|
||||
fs.unlinkSync(tmp_path);
|
||||
return { success: false, error: 'Integrity check failed: Hash mismatch' };
|
||||
}
|
||||
|
||||
fs.renameSync(tmp_path, full_path);
|
||||
console.log(`Native: Cache Integrity Verified. File moved to final destination.`);
|
||||
return { success: true, path: full_path };
|
||||
} catch (error: any) {
|
||||
if (fs.existsSync(tmp_path)) fs.unlinkSync(tmp_path);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
endpoints_in_progress = endpoints_in_progress.filter(e => e !== url);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('native:launch-from-cache', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2, native_template = null }) => {
|
||||
try {
|
||||
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
const expanded_temp = expandPath(temp_root);
|
||||
const target = path.join(expanded_temp, filename);
|
||||
|
||||
console.log(`Native: Launching from Cache -> ${filename}`);
|
||||
|
||||
if (!fs.existsSync(source)) {
|
||||
return { success: false, error: `File not in cache: ${hash}` };
|
||||
}
|
||||
|
||||
if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true });
|
||||
|
||||
// 1. Copy the file to temp folder with original name
|
||||
fs.copyFileSync(source, target);
|
||||
|
||||
// 2a. Data-driven launcher template (no rebuild needed for config changes).
|
||||
// Svelte resolves a Launch Profile to a single native_template string.
|
||||
// Format: AppleScript string with {{path}} placeholder, OR "shell:<cmd> {{path}}"
|
||||
if (!native_template) {
|
||||
return { success: false, error: 'No native template configured for this file' };
|
||||
}
|
||||
|
||||
const resolved = native_template.replace(/\{\{path\}\}/g, target);
|
||||
if (resolved.startsWith('shell:')) {
|
||||
const cmd = resolved.slice(6).trim();
|
||||
console.log(`Native: Running custom shell template for ${filename}`);
|
||||
return new Promise((resolve_fn) => {
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) resolve_fn({ success: false, error: err.message, stderr: stderr.trim() });
|
||||
else resolve_fn({ success: true, stdout: stdout.trim() });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`Native: Running custom AppleScript template for ${filename}`);
|
||||
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
|
||||
return new Promise((resolve_fn) => {
|
||||
try {
|
||||
fs.writeFileSync(tmp_script_path, resolved.trim());
|
||||
} catch (e: any) {
|
||||
resolve_fn({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
exec(`osascript "${tmp_script_path}"`, (err) => {
|
||||
try { fs.unlinkSync(tmp_script_path); } catch {}
|
||||
if (err) resolve_fn({ success: false, error: err.message });
|
||||
else resolve_fn({ success: true });
|
||||
});
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
// Thin primitive: copy a cached file to the temp directory with its original filename,
|
||||
// then return the resolved path. The caller (Svelte side) decides what to do next —
|
||||
// run_osascript, run_cmd, open_local_file, etc.
|
||||
//
|
||||
// This is the preferred building block for custom launch flows. Use launch_from_cache
|
||||
// when Svelte has already resolved a native template; use copy_from_cache_to_temp when
|
||||
// you want full control over what happens after the file lands in temp.
|
||||
ipcMain.handle('native:copy-from-cache-to-temp', async (event, { cache_root, hash, temp_root, filename, hash_prefix_length = 2 }) => {
|
||||
try {
|
||||
const source = get_organized_hashed_path(cache_root, hash, hash_prefix_length);
|
||||
|
||||
if (!fs.existsSync(source)) {
|
||||
return { success: false, error: `File not in cache: ${hash}` };
|
||||
}
|
||||
|
||||
const expanded_temp = expandPath(temp_root);
|
||||
const target = path.join(expanded_temp, filename);
|
||||
|
||||
if (!fs.existsSync(expanded_temp)) fs.mkdirSync(expanded_temp, { recursive: true });
|
||||
|
||||
fs.copyFileSync(source, target);
|
||||
console.log(`Native: Copied from cache to temp -> ${target}`);
|
||||
|
||||
return { success: true, path: target };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
}
|
||||
16
src/main/file_utils.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
export function expandPath(filePath: string): string {
|
||||
if (!filePath) return filePath;
|
||||
// Resolve all instances of [home] and [tmp] using global regex
|
||||
return filePath
|
||||
.replace(/\[home\]/g, os.homedir())
|
||||
.replace(/\[tmp\]/g, os.tmpdir());
|
||||
}
|
||||
|
||||
export function getHashedPath(cacheRoot: string, hash: string): string {
|
||||
const expandedRoot = expandPath(cacheRoot);
|
||||
const subdirectory = hash.substring(0, 2);
|
||||
return path.join(expandedRoot, subdirectory, `${hash}.file`);
|
||||
}
|
||||
94
src/main/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { loadSeedConfig } from './config_loader';
|
||||
import { fetchFullConfig } from './api_client';
|
||||
import { registerShellHandlers } from './shell_handlers';
|
||||
import { registerFileHandlers } from './file_handlers';
|
||||
import { registerSystemHandlers } from './system_handlers';
|
||||
import { SeedConfig } from '../shared/types';
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
let cachedSeed: SeedConfig | null = null;
|
||||
let cachedFullConfig: any = null;
|
||||
|
||||
async function createWindow() {
|
||||
cachedSeed = await loadSeedConfig();
|
||||
if (cachedSeed) {
|
||||
cachedFullConfig = await fetchFullConfig(cachedSeed);
|
||||
}
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1600,
|
||||
height: 900,
|
||||
title: 'OSIT Aether Launcher (Native)',
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
contextIsolation: true,
|
||||
nodeIntegration: false,
|
||||
},
|
||||
});
|
||||
|
||||
let targetUrl = 'http://demo.localhost:5173';
|
||||
if (cachedFullConfig && cachedFullConfig.native_device) {
|
||||
const device = cachedFullConfig.native_device;
|
||||
const eventId = device.event_id_random || device.event_id;
|
||||
const locationId = device.event_location_id_random || device.event_location_id || '';
|
||||
// Use app_base_url from the device record (e.g. bgh.oneskyit.com).
|
||||
// Fall back to localhost only if nothing is configured — never override a real domain.
|
||||
const host = device.app_base_url || 'demo.localhost:5173';
|
||||
// Use https for real domains; localhost dev URLs stay on http
|
||||
const protocol = host.includes('localhost') ? 'http' : 'https';
|
||||
targetUrl = `${protocol}://${host}/events/${eventId}/launcher/${locationId}`;
|
||||
}
|
||||
|
||||
// Only open DevTools in development (not in a packaged .app build)
|
||||
if (!app.isPackaged) mainWindow.webContents.openDevTools();
|
||||
mainWindow.loadURL(targetUrl).catch(() => {
|
||||
mainWindow?.loadURL('https://dev-demo.oneskyit.com/');
|
||||
});
|
||||
|
||||
mainWindow.on('closed', () => { mainWindow = null; });
|
||||
}
|
||||
|
||||
registerShellHandlers();
|
||||
registerFileHandlers();
|
||||
registerSystemHandlers();
|
||||
|
||||
app.on('ready', createWindow);
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
if (mainWindow === null) createWindow();
|
||||
});
|
||||
|
||||
ipcMain.handle('get-seed-config', async () => cachedSeed || await loadSeedConfig());
|
||||
ipcMain.handle('get-device-config', async () => cachedFullConfig);
|
||||
ipcMain.handle('get-jwt', async () => null);
|
||||
|
||||
ipcMain.handle('get-device-info', async () => {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const addresses: string[] = [];
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const net of interfaces[name]!) {
|
||||
if (net.family === 'IPv4' && !net.internal) {
|
||||
addresses.push(net.address);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
platform: os.platform(),
|
||||
release: os.release(),
|
||||
arch: os.arch(),
|
||||
hostname: os.hostname(),
|
||||
cpus: os.cpus().length,
|
||||
total_mem: os.totalmem(),
|
||||
free_mem: os.freemem(),
|
||||
ip_addresses: addresses,
|
||||
home_directory: os.homedir(),
|
||||
tmp_directory: os.tmpdir()
|
||||
};
|
||||
});
|
||||
286
src/main/shell_handlers.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import { ipcMain, shell } from 'electron';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { expandPath } from './file_utils';
|
||||
|
||||
export function registerShellHandlers() {
|
||||
ipcMain.handle('native:open-folder', async (event, folderPath: string) => {
|
||||
const cleanPath = expandPath(folderPath);
|
||||
const error = await shell.openPath(cleanPath);
|
||||
return { success: !error, error };
|
||||
});
|
||||
|
||||
ipcMain.handle('native:run-cmd', async (event, { cmd, timeout = 30000 }) => {
|
||||
const cleanCmd = expandPath(cmd);
|
||||
return new Promise((resolve) => {
|
||||
exec(cleanCmd, { timeout }, (error, stdout, stderr) => {
|
||||
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('native:run-cmd-sync', async (event, { cmd }) => {
|
||||
const cleanCmd = expandPath(cmd);
|
||||
try {
|
||||
const stdout = execSync(cleanCmd).toString();
|
||||
return { success: true, stdout: stdout.trim() };
|
||||
} catch (error: any) {
|
||||
return { success: false, error: error.message, stderr: error.stderr?.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('native:run-osascript', async (event, script: string) => {
|
||||
if (os.platform() !== 'darwin') return { success: false, error: 'AppleScript is only available on macOS' };
|
||||
|
||||
// HARDENED: Write script to a temp .scpt file rather than passing inline via -e.
|
||||
// The old -e approach (`osascript -e "..."`) has two fatal flaws:
|
||||
// 1. It breaks on multi-line scripts.
|
||||
// 2. It breaks on paths containing spaces or special characters (quotes, parens, etc.)
|
||||
// Writing to a file sidesteps both — no shell escaping needed at all.
|
||||
// The .scpt file is deleted immediately after execution (success or failure).
|
||||
// Worst case on crash: a stale .scpt in /tmp, cleared on next OS reboot.
|
||||
//
|
||||
// LEGACY (removed): const cmd = `osascript -e "${script.replace(/"/g, '\\"')}"`;
|
||||
|
||||
const tmp_script_path = path.join(os.tmpdir(), `ae_osa_${Date.now()}.scpt`);
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
fs.writeFileSync(tmp_script_path, script.trim());
|
||||
} catch (e: any) {
|
||||
resolve({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
exec(`osascript "${tmp_script_path}"`, (error, stdout, stderr) => {
|
||||
try { fs.unlinkSync(tmp_script_path); } catch {}
|
||||
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('native:kill-processes', async (event, { process_name_li = [] }) => {
|
||||
console.log(`Native: Killing processes -> `, process_name_li);
|
||||
const results = [];
|
||||
for (const name of process_name_li) {
|
||||
const cmd = os.platform() === 'win32'
|
||||
? `taskkill /F /IM ${name} /T`
|
||||
: `pkill -f ${name}`;
|
||||
try {
|
||||
execSync(cmd);
|
||||
results.push({ name, success: true });
|
||||
} catch (e: any) {
|
||||
results.push({ name, success: false, error: e.message });
|
||||
}
|
||||
}
|
||||
return { success: true, results };
|
||||
});
|
||||
|
||||
ipcMain.handle('native:open-local-file-v2', async (event, filePath: string) => {
|
||||
const cleanPath = expandPath(filePath);
|
||||
const error = await shell.openPath(cleanPath);
|
||||
return { success: !error, error };
|
||||
});
|
||||
|
||||
ipcMain.handle('native:launch-presentation', async (event, { path: rawPath, app: appType = 'default' }) => {
|
||||
const cleanedPath = expandPath(rawPath);
|
||||
console.log(`Native: Launching Presentation -> ${cleanedPath} (App: ${appType})`);
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
const cmd = `libreoffice --impress "${cleanedPath}"`;
|
||||
return new Promise((resolve) => {
|
||||
exec(cmd, (err, stdout, stderr) => {
|
||||
if (err) resolve({ success: false, error: err.message });
|
||||
else resolve({ success: true, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
let script = '';
|
||||
if (appType === 'keynote') {
|
||||
script = `
|
||||
tell application "Keynote"
|
||||
activate
|
||||
open (POSIX file "${cleanedPath}")
|
||||
delay 1
|
||||
start (front document)
|
||||
end tell
|
||||
`.trim();
|
||||
} else if (appType === 'powerpoint') {
|
||||
script = `
|
||||
tell application "Microsoft PowerPoint"
|
||||
activate
|
||||
open (POSIX file "${cleanedPath}")
|
||||
delay 1
|
||||
run slide show of active presentation
|
||||
end tell
|
||||
`.trim();
|
||||
}
|
||||
|
||||
if (script) {
|
||||
const tmp_script_path = path.join(os.tmpdir(), `ae_launch_${Date.now()}.scpt`);
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
fs.writeFileSync(tmp_script_path, script);
|
||||
} catch (e: any) {
|
||||
resolve({ success: false, error: `Failed to write AppleScript temp file: ${e.message}` });
|
||||
return;
|
||||
}
|
||||
exec(`osascript "${tmp_script_path}"`, (err) => {
|
||||
try { fs.unlinkSync(tmp_script_path); } catch {}
|
||||
if (err) resolve({ success: false, error: err.message });
|
||||
else resolve({ success: true });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const error = await shell.openPath(cleanedPath);
|
||||
return { success: !error, error };
|
||||
});
|
||||
|
||||
ipcMain.handle('native:control-presentation', async (event, { app, action }) => {
|
||||
if (os.platform() !== 'darwin') return { success: false, error: 'Presentation control is only available on macOS' };
|
||||
|
||||
let script = '';
|
||||
if (app === 'powerpoint') {
|
||||
switch (action) {
|
||||
case 'next': script = 'tell application "Microsoft PowerPoint" to next slide of slide show view of active presentation'; break;
|
||||
case 'prev': script = 'tell application "Microsoft PowerPoint" to previous slide of slide show view of active presentation'; break;
|
||||
case 'start': script = 'tell application "Microsoft PowerPoint" to run slide show of active presentation'; break;
|
||||
case 'stop': script = 'tell application "Microsoft PowerPoint" to stop slide show of active presentation'; break;
|
||||
}
|
||||
} else if (app === 'keynote') {
|
||||
switch (action) {
|
||||
case 'next': script = 'tell application "Keynote" to show next'; break;
|
||||
case 'prev': script = 'tell application "Keynote" to show previous'; break;
|
||||
case 'start': script = 'tell application "Keynote" to start (front document)'; break;
|
||||
case 'stop': script = 'tell application "Keynote" to stop'; break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!script) return { success: false, error: `Unsupported app or action: ${app}/${action}` };
|
||||
|
||||
return new Promise((resolve) => {
|
||||
exec(`osascript -e "${script.replace(/"/g, '\\"')}"`, (error, stdout, stderr) => {
|
||||
resolve({ success: !error, stdout: stdout.trim(), stderr: stderr.trim(), error: error ? error.message : null });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('native:list-tools', async () => {
|
||||
return [
|
||||
// --- Config & Info ---
|
||||
{
|
||||
name: 'get_device_config',
|
||||
description: 'Returns hydrated device config injected at startup from seed.json + API.',
|
||||
params: {}
|
||||
},
|
||||
{
|
||||
name: 'get_device_info',
|
||||
description: 'Returns OS metadata: platform, hostname, IPs, CPU count, free RAM, home/tmp paths.',
|
||||
params: {}
|
||||
},
|
||||
// --- File Cache ---
|
||||
{
|
||||
name: 'check_cache',
|
||||
description: 'Checks if a file exists in the hashed cache. verify_hash:true re-hashes to confirm integrity.',
|
||||
params: { cache_root: 'string', hash: 'string', hash_prefix_length: 'number (optional, default 2)', verify_hash: 'boolean (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'download_to_cache',
|
||||
description: 'Streams a file from the API into the hashed cache with SHA-256 integrity check. Cleans stale .tmp files older than 5 min.',
|
||||
params: { url: 'string', cache_root: 'string', hash: 'string', api_key: 'string', account_id: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'copy_from_cache_to_temp',
|
||||
description: 'Preferred primitive. Copies a cached file to temp with its original filename. Returns { success, path }. Caller decides what to do next.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional, default 2)' }
|
||||
},
|
||||
{
|
||||
name: 'launch_from_cache',
|
||||
description: 'Combines copy_from_cache_to_temp + execute. Runs native_template after copying — AppleScript string with {{path}} placeholder, or "shell:<cmd>" prefix. Returns error if native_template is null.',
|
||||
params: { cache_root: 'string', hash: 'string', temp_root: 'string', filename: 'string', hash_prefix_length: 'number (optional)', native_template: 'string | null' }
|
||||
},
|
||||
// --- Shell & OS ---
|
||||
{
|
||||
name: 'open_folder',
|
||||
description: 'Opens a directory in the OS file explorer (Finder on macOS).',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd',
|
||||
description: 'Async shell command execution with timeout.',
|
||||
params: { cmd: 'string', timeout: 'number (optional, default 30000ms)' }
|
||||
},
|
||||
{
|
||||
name: 'run_cmd_sync',
|
||||
description: 'Synchronous shell command execution.',
|
||||
params: { cmd: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'run_osascript',
|
||||
description: 'Hardened AppleScript executor — writes to temp .scpt file, handles multi-line scripts and paths with special characters. macOS only.',
|
||||
params: { script: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'kill_processes',
|
||||
description: 'Terminates processes by name. macOS/Linux: pkill -f. Windows: taskkill /F.',
|
||||
params: { process_name_li: 'string[]' }
|
||||
},
|
||||
{
|
||||
name: 'open_local_file_v2',
|
||||
description: 'Opens a file with its default OS application via shell.openPath.',
|
||||
params: { path: 'string' }
|
||||
},
|
||||
{
|
||||
name: 'open_external',
|
||||
description: 'Opens a URL in Chrome, Firefox, or the system default browser.',
|
||||
params: { url: 'string', app: 'chrome | firefox | default (optional)' }
|
||||
},
|
||||
// --- Presentations ---
|
||||
{
|
||||
name: 'launch_presentation',
|
||||
description: 'Platform-aware launcher for PowerPoint, Keynote, LibreOffice. Resolves [home]/[tmp] placeholders. Hardened AppleScript (2026-05-11). Prefer copy_from_cache_to_temp + run_osascript for new flows.',
|
||||
params: { path: 'string', app: 'default | powerpoint | keynote (optional)', os_platform: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'control_presentation',
|
||||
description: 'Slide navigation for active PowerPoint or Keynote via AppleScript. macOS only.',
|
||||
params: { app: 'powerpoint | keynote', action: 'next | prev | start | stop' }
|
||||
},
|
||||
// --- System Management ---
|
||||
{
|
||||
name: 'set_wallpaper',
|
||||
description: 'Sets desktop wallpaper. Downloads from url (cached to ~/Library/Caches/OSIT/wallpaper/) or applies local path. url_external targets projector/second display separately. macOS only in production.',
|
||||
params: { path: 'string (optional)', url: 'string (optional)', url_external: 'string (optional)', display: 'all | primary | external (optional, default all)', api_key: 'string (optional)', account_id: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'set_display_layout',
|
||||
description: 'Mirror or extend displays via bundled displayplacer. macOS only. Auto-detects displays when no configStr given; configStr is an optional manual override.',
|
||||
params: { mode: 'mirror | extend', configStr: 'string (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'window_control',
|
||||
description: 'Electron window management.',
|
||||
params: { action: 'maximize | unmaximize | minimize | restore | close | fullscreen | kiosk | devtools | reload', value: 'boolean (optional, used by fullscreen/kiosk/devtools)' }
|
||||
},
|
||||
{
|
||||
name: 'power_control',
|
||||
description: 'Shutdown, reboot, or sleep the host machine. macOS + Linux. May require sudo for shutdown/reboot.',
|
||||
params: { action: 'shutdown | reboot | sleep' }
|
||||
},
|
||||
{
|
||||
name: 'manage_recording',
|
||||
description: 'Screen recording via bundled aperture binary. macOS only.',
|
||||
params: { action: 'start | stop | status', options: '{ fps?, audioDeviceId?, output? } (optional)' }
|
||||
},
|
||||
{
|
||||
name: 'update_app',
|
||||
description: 'STUB: Downloads update package but does not install. Not functional.',
|
||||
params: { source: 'url | file', url: 'string (optional)', path: 'string (optional)' }
|
||||
}
|
||||
];
|
||||
});
|
||||
}
|
||||
465
src/main/system_handlers.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
import { ipcMain, BrowserWindow, app, shell } from 'electron';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { exec, spawn } from 'child_process';
|
||||
import axios from 'axios';
|
||||
import { expandPath } from './file_utils';
|
||||
|
||||
// Helper to execute shell commands
|
||||
const runExec = (cmd: string): Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }> => {
|
||||
return new Promise((resolve) => {
|
||||
exec(cmd, (error, stdout, stderr) => {
|
||||
resolve({
|
||||
success: !error,
|
||||
stdout: stdout.trim(),
|
||||
stderr: stderr.trim(),
|
||||
error: error ? error.message : undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
let recordingProcess: any = null;
|
||||
|
||||
export function registerSystemHandlers() {
|
||||
|
||||
// 1. Window Control
|
||||
ipcMain.handle('native:window-control', async (event, { action, value }) => {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) return { success: false, error: 'No window found' };
|
||||
|
||||
switch (action) {
|
||||
case 'maximize': win.maximize(); break;
|
||||
case 'unmaximize': win.unmaximize(); break;
|
||||
case 'minimize': win.minimize(); break;
|
||||
case 'restore': win.restore(); break;
|
||||
case 'close': win.close(); break;
|
||||
case 'devtools':
|
||||
if (value) win.webContents.openDevTools();
|
||||
else win.webContents.closeDevTools();
|
||||
break;
|
||||
case 'kiosk': win.setKiosk(!!value); break;
|
||||
case 'fullscreen': win.setFullScreen(!!value); break;
|
||||
case 'reload': win.reload(); break;
|
||||
default: return { success: false, error: `Unknown action: ${action}` };
|
||||
}
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// 2. Set Wallpaper
|
||||
// Supports local path OR URL download. URL images are saved to a stable cache dir
|
||||
// so macOS can reference them persistently after reboot.
|
||||
// display: 'all' (default) | 'primary' (built-in) | 'external' (projector/second screen)
|
||||
// url_external: optional second URL for the external display only.
|
||||
ipcMain.handle('native:set-wallpaper', async (event, {
|
||||
path: imagePath,
|
||||
url,
|
||||
url_external,
|
||||
display = 'all',
|
||||
api_key,
|
||||
account_id
|
||||
}: {
|
||||
path?: string;
|
||||
url?: string;
|
||||
url_external?: string;
|
||||
display?: 'all' | 'primary' | 'external';
|
||||
api_key?: string;
|
||||
account_id?: string;
|
||||
}) => {
|
||||
// Cache dir: ~/Library/Caches/OSIT/wallpaper on macOS, ~/.cache/osit/wallpaper on Linux.
|
||||
// Using a stable path means macOS keeps the reference across reboots.
|
||||
const wallpaper_cache_dir = os.platform() === 'darwin'
|
||||
? path.join(os.homedir(), 'Library', 'Caches', 'OSIT', 'wallpaper')
|
||||
: path.join(os.homedir(), '.cache', 'osit', 'wallpaper');
|
||||
|
||||
async function download_wallpaper_image(image_url: string, basename: string): Promise<{ success: boolean; path?: string; error?: string }> {
|
||||
if (!fs.existsSync(wallpaper_cache_dir)) {
|
||||
fs.mkdirSync(wallpaper_cache_dir, { recursive: true });
|
||||
}
|
||||
|
||||
// Infer extension from URL path, fall back to .jpg
|
||||
let ext = '.jpg';
|
||||
try {
|
||||
const url_path = new URL(image_url).pathname;
|
||||
const inferred = path.extname(url_path).toLowerCase();
|
||||
if (['.jpg', '.jpeg', '.png', '.webp'].includes(inferred)) {
|
||||
ext = inferred === '.jpeg' ? '.jpg' : inferred;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const dest_path = path.join(wallpaper_cache_dir, basename + ext);
|
||||
|
||||
const headers: Record<string, string> = {};
|
||||
if (api_key) headers['x-aether-api-key'] = api_key;
|
||||
if (account_id) headers['x-account-id'] = account_id;
|
||||
|
||||
try {
|
||||
const response = await axios({ method: 'get', url: image_url, responseType: 'stream', headers });
|
||||
const content_type = (response.headers['content-type'] ?? '') as string;
|
||||
if (!content_type.startsWith('image/')) {
|
||||
response.data.destroy();
|
||||
return { success: false, error: `URL did not return an image (Content-Type: ${content_type})` };
|
||||
}
|
||||
|
||||
const writer = fs.createWriteStream(dest_path);
|
||||
response.data.pipe(writer);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
const file_size = fs.statSync(dest_path).size;
|
||||
if (file_size === 0) {
|
||||
try { fs.unlinkSync(dest_path); } catch {}
|
||||
return { success: false, error: 'Wallpaper download incomplete (0 bytes)' };
|
||||
}
|
||||
return { success: true, path: dest_path };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: `Wallpaper download failed: ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
// HARDENED: write AppleScript to a temp file, same pattern as native:run-osascript.
|
||||
// The old osascript -e approach breaks on paths with spaces or special characters.
|
||||
async function apply_mac_wallpaper(img_path: string, display_target: 'all' | 'primary' | 'external'): Promise<{ success: boolean; stdout?: string; stderr?: string; error?: string }> {
|
||||
const escaped = img_path.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||
let script: string;
|
||||
if (display_target === 'primary') {
|
||||
script = `tell application "System Events"\n\ttell desktop 1\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`;
|
||||
} else if (display_target === 'external') {
|
||||
script = `tell application "System Events"\n\ttell desktop 2\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`;
|
||||
} else {
|
||||
script = `tell application "System Events"\n\ttell every desktop\n\t\tset picture to "${escaped}"\n\tend tell\nend tell`;
|
||||
}
|
||||
|
||||
const script_path = path.join(os.tmpdir(), `ae_wallpaper_${Date.now()}.scpt`);
|
||||
fs.writeFileSync(script_path, script, 'utf-8');
|
||||
try {
|
||||
return await runExec(`osascript "${script_path}"`);
|
||||
} finally {
|
||||
try { fs.unlinkSync(script_path); } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
if (os.platform() === 'darwin') {
|
||||
// Resolve primary image path
|
||||
let primary_path: string | null = null;
|
||||
|
||||
if (imagePath) {
|
||||
const clean = expandPath(imagePath);
|
||||
if (!fs.existsSync(clean)) return { success: false, error: 'Image file not found' };
|
||||
primary_path = clean;
|
||||
} else if (url) {
|
||||
const result = await download_wallpaper_image(url, 'wallpaper_primary');
|
||||
if (!result.success || !result.path) return { success: false, error: result.error };
|
||||
primary_path = result.path;
|
||||
}
|
||||
|
||||
if (!primary_path && url_external && display === 'external') {
|
||||
const ext_result = await download_wallpaper_image(url_external, 'wallpaper_external');
|
||||
if (!ext_result.success || !ext_result.path) return { success: false, error: ext_result.error };
|
||||
return await apply_mac_wallpaper(ext_result.path, 'external');
|
||||
}
|
||||
|
||||
if (!primary_path) return { success: false, error: 'No image source provided' };
|
||||
|
||||
if (url_external) {
|
||||
// Different images for each display: set primary display first, then external
|
||||
const primary_result = await apply_mac_wallpaper(primary_path, 'primary');
|
||||
if (!primary_result.success) return primary_result;
|
||||
|
||||
const ext_result = await download_wallpaper_image(url_external, 'wallpaper_external');
|
||||
if (!ext_result.success || !ext_result.path) return { success: false, error: ext_result.error };
|
||||
return await apply_mac_wallpaper(ext_result.path, 'external');
|
||||
} else {
|
||||
return await apply_mac_wallpaper(primary_path, display);
|
||||
}
|
||||
}
|
||||
|
||||
if (os.platform() === 'linux') {
|
||||
// Dev test mode: never touch the desktop on Linux. Running gsettings during
|
||||
// development would reset the dev workstation monitors on every test cycle.
|
||||
// Return what would have run so the Svelte side can show a debug popup.
|
||||
const would_run: string[] = [];
|
||||
const cache_dir = wallpaper_cache_dir;
|
||||
|
||||
if (!imagePath && !url && !url_external) {
|
||||
return { success: false, error: 'No image source provided' };
|
||||
}
|
||||
|
||||
if (imagePath) {
|
||||
const clean = expandPath(imagePath);
|
||||
if (!fs.existsSync(clean)) return { success: false, error: 'Image file not found' };
|
||||
}
|
||||
|
||||
if (url) would_run.push(`download: ${url}\n → ${path.join(cache_dir, 'wallpaper_primary.jpg')}`);
|
||||
if (url_external) would_run.push(`download: ${url_external}\n → ${path.join(cache_dir, 'wallpaper_external.jpg')}`);
|
||||
|
||||
if (imagePath) {
|
||||
would_run.push(`gsettings set org.gnome.desktop.background picture-uri "file://${expandPath(imagePath)}"`);
|
||||
} else if (url) {
|
||||
would_run.push(`gsettings set org.gnome.desktop.background picture-uri "file://${path.join(cache_dir, 'wallpaper_primary.jpg')}"`);
|
||||
}
|
||||
if (url_external) {
|
||||
would_run.push(`(external display: gsettings has no per-display wallpaper support)`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
linux_test_mode: true,
|
||||
platform: 'linux',
|
||||
display,
|
||||
url: url ?? null,
|
||||
url_external: url_external ?? null,
|
||||
would_run
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, error: 'Platform not supported' };
|
||||
});
|
||||
|
||||
// 3. Power Control
|
||||
ipcMain.handle('native:power-control', async (event, { action }) => {
|
||||
let cmd = '';
|
||||
const isMac = os.platform() === 'darwin';
|
||||
const isLinux = os.platform() === 'linux';
|
||||
|
||||
if (action === 'shutdown') {
|
||||
if (isMac) cmd = 'shutdown -h now'; // Requires sudo/admin usually
|
||||
if (isLinux) cmd = 'shutdown -h now';
|
||||
} else if (action === 'reboot') {
|
||||
if (isMac) cmd = 'shutdown -r now';
|
||||
if (isLinux) cmd = 'reboot';
|
||||
} else if (action === 'sleep') {
|
||||
if (isMac) cmd = 'pmset sleepnow';
|
||||
if (isLinux) cmd = 'systemctl suspend';
|
||||
}
|
||||
|
||||
if (!cmd) return { success: false, error: 'Action not supported' };
|
||||
|
||||
// NOTE: These commands often require root.
|
||||
// For a kiosk, you might configure sudoers to allow this specific command without password.
|
||||
return await runExec(cmd);
|
||||
});
|
||||
|
||||
// 4. Open External (Browser)
|
||||
ipcMain.handle('native:open-external', async (event, { url, app: appName }) => {
|
||||
if (appName === 'chrome') {
|
||||
if (os.platform() === 'darwin') {
|
||||
return await runExec(`open -a "Google Chrome" "${url}"`);
|
||||
} else if (os.platform() === 'linux') {
|
||||
return await runExec(`google-chrome "${url}"`);
|
||||
}
|
||||
} else if (appName === 'firefox') {
|
||||
if (os.platform() === 'darwin') {
|
||||
return await runExec(`open -a "Firefox" "${url}"`);
|
||||
} else if (os.platform() === 'linux') {
|
||||
return await runExec(`firefox "${url}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Default system handler
|
||||
await shell.openExternal(url);
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
// 5. Manage Recording (Aperture Wrapper)
|
||||
ipcMain.handle('native:manage-recording', async (event, { action, options }) => {
|
||||
if (os.platform() !== 'darwin') return { success: false, error: 'Recording only supported on macOS' };
|
||||
|
||||
// Path to bundled aperture binary
|
||||
// In dev: ./resources/bin/aperture
|
||||
// In prod: process.resourcesPath/bin/aperture
|
||||
const binPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'bin', 'aperture')
|
||||
: path.join(__dirname, '../../resources/bin/aperture'); // Adjust based on structure
|
||||
|
||||
if (action === 'start') {
|
||||
if (recordingProcess) return { success: false, error: 'Recording already in progress' };
|
||||
|
||||
const { fps = 30, audioDeviceId, output } = options || {};
|
||||
const cleanOutput = expandPath(output || '~/tmp/recording.mp4');
|
||||
|
||||
const args = ['run', '--fps', fps, '--output', cleanOutput];
|
||||
if (audioDeviceId) args.push('--audio-device-id', audioDeviceId);
|
||||
|
||||
// Spawn process
|
||||
// Note: aperture is a CLI tool. We might need 'aperture' node package or the binary.
|
||||
// Assuming binary usage here.
|
||||
try {
|
||||
console.log(`Starting recording: ${binPath} ${args.join(' ')}`);
|
||||
recordingProcess = spawn(binPath, args);
|
||||
|
||||
recordingProcess.on('error', (err: any) => {
|
||||
console.error('Recording error:', err);
|
||||
recordingProcess = null;
|
||||
});
|
||||
|
||||
recordingProcess.on('exit', (code: any) => {
|
||||
console.log(`Recording exited with code ${code}`);
|
||||
recordingProcess = null;
|
||||
});
|
||||
|
||||
return { success: true, pid: recordingProcess.pid };
|
||||
} catch (e: any) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
|
||||
} else if (action === 'stop') {
|
||||
if (!recordingProcess) return { success: false, error: 'No recording in progress' };
|
||||
|
||||
recordingProcess.kill('SIGINT'); // Send interrupt to stop cleanly
|
||||
recordingProcess = null;
|
||||
return { success: true };
|
||||
} else if (action === 'status') {
|
||||
return { success: true, isRecording: !!recordingProcess };
|
||||
}
|
||||
|
||||
return { success: false, error: 'Unknown action' };
|
||||
});
|
||||
|
||||
// 6. Set Display Layout
|
||||
// Primary path: display_control (native CoreGraphics, no external deps).
|
||||
// Build from scripts/display_control.m via scripts/build-display-control.sh on a Mac.
|
||||
// Commit the resulting resources/bin/display_control binary to the repo.
|
||||
// Fallback: displayplacer (requires: brew install displayplacer on each venue Mac).
|
||||
// Also supports per-device configStr override (displayplacer format).
|
||||
ipcMain.handle('native:set-display-layout', async (event, { mode, configStr }) => {
|
||||
if (os.platform() !== 'darwin') return { success: false, error: 'Display control only supported on macOS' };
|
||||
|
||||
// Primary: display_control — native CoreGraphics, no Homebrew dependency.
|
||||
// Derived from OSIT MasterKey app (LegacyUtilities.m). No configStr support needed —
|
||||
// CoreGraphics auto-detects all connected displays.
|
||||
const dc_bin = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'bin', 'display_control')
|
||||
: path.join(__dirname, '../../resources/bin/display_control');
|
||||
|
||||
if (fs.existsSync(dc_bin) && !configStr) {
|
||||
if (mode !== 'mirror' && mode !== 'extend') {
|
||||
return { success: false, error: `Unsupported display mode: ${mode}` };
|
||||
}
|
||||
return await runExec(`"${dc_bin}" ${mode}`);
|
||||
}
|
||||
|
||||
// Fallback: displayplacer — required when display_control binary is not built yet,
|
||||
// or when a per-device configStr override is set (displayplacer-format string from event_device.data_json).
|
||||
// Install: brew install displayplacer
|
||||
const _dp_candidates = app.isPackaged
|
||||
? [path.join(process.resourcesPath, 'bin', 'displayplacer')]
|
||||
: [
|
||||
path.join(__dirname, '../../resources/bin/displayplacer'),
|
||||
'/opt/homebrew/bin/displayplacer', // Apple Silicon Homebrew
|
||||
'/usr/local/bin/displayplacer', // Intel Homebrew
|
||||
];
|
||||
const dpPath = _dp_candidates.find(p => fs.existsSync(p)) ?? _dp_candidates[0];
|
||||
|
||||
// Explicit configStr takes priority — allows manual per-device override.
|
||||
if (configStr) {
|
||||
return await runExec(`"${dpPath}" ${configStr}`);
|
||||
}
|
||||
|
||||
// Auto-detect via `displayplacer list`.
|
||||
const list_result = await runExec(`"${dpPath}" list`);
|
||||
if (!list_result.success || !list_result.stdout) {
|
||||
return { success: false, error: `displayplacer not available. Build display_control from scripts/build-display-control.sh or run: brew install displayplacer` };
|
||||
}
|
||||
|
||||
// The command line looks like: displayplacer "id:xxx res:... origin:(0,0) ..." "id:yyy ..."
|
||||
const cmd_line = list_result.stdout.split('\n').find(l => l.trim().startsWith('displayplacer "'));
|
||||
if (!cmd_line) {
|
||||
return { success: false, error: 'Only one display connected or displayplacer list output unrecognised' };
|
||||
}
|
||||
|
||||
const display_strings = [...cmd_line.matchAll(/"([^"]+)"/g)].map(m => m[1]);
|
||||
if (display_strings.length < 2) {
|
||||
return { success: false, error: 'Only one display found; cannot change layout' };
|
||||
}
|
||||
|
||||
if (mode === 'mirror') {
|
||||
const primary_id_match = display_strings[0].match(/\bid:([^\s]+)/);
|
||||
if (!primary_id_match) {
|
||||
return { success: false, error: 'Could not parse primary display ID from displayplacer output' };
|
||||
}
|
||||
const primary_id = primary_id_match[1];
|
||||
|
||||
const mirror_args = display_strings.map((s, i) => {
|
||||
if (i === 0) return `"${s}"`;
|
||||
const without_existing_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
|
||||
return `"${without_existing_mirror} mirror_of_display:${primary_id}"`;
|
||||
}).join(' ');
|
||||
|
||||
return await runExec(`"${dpPath}" ${mirror_args}`);
|
||||
}
|
||||
|
||||
if (mode === 'extend') {
|
||||
const any_mirrored = display_strings.some(s => /\bmirror_of_display:\S+/.test(s));
|
||||
if (!any_mirrored) {
|
||||
return await runExec(`"${dpPath}" ${display_strings.map(s => `"${s}"`).join(' ')}`);
|
||||
}
|
||||
|
||||
let x_offset = 0;
|
||||
const extend_args = display_strings.map((s) => {
|
||||
const without_mirror = s.replace(/\s*mirror_of_display:\S+/g, '').trim();
|
||||
const res_match = without_mirror.match(/\bres:(\d+)x\d+/);
|
||||
const width = res_match ? parseInt(res_match[1]) : 1920;
|
||||
const updated = without_mirror.replace(/\borigin:\([^)]+\)/, `origin:(${x_offset},0)`);
|
||||
x_offset += width;
|
||||
return `"${updated}"`;
|
||||
}).join(' ');
|
||||
|
||||
return await runExec(`"${dpPath}" ${extend_args}`);
|
||||
}
|
||||
|
||||
return { success: false, error: `Unsupported display mode: ${mode}` };
|
||||
});
|
||||
|
||||
// 7. Update App
|
||||
ipcMain.handle('native:update-app', async (event, { source, url, path: localPath }) => {
|
||||
// 1. Determine Source File
|
||||
let updateFile = '';
|
||||
const tempDir = os.tmpdir();
|
||||
const destName = 'update_package.zip'; // Or .app, .AppImage
|
||||
const destPath = path.join(tempDir, destName);
|
||||
|
||||
if (source === 'url' && url) {
|
||||
// Download
|
||||
try {
|
||||
const response = await axios({
|
||||
method: 'get',
|
||||
url: url,
|
||||
responseType: 'stream'
|
||||
});
|
||||
|
||||
const writer = fs.createWriteStream(destPath);
|
||||
response.data.pipe(writer);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on('finish', () => resolve(true));
|
||||
writer.on('error', reject);
|
||||
});
|
||||
updateFile = destPath;
|
||||
} catch (e: any) {
|
||||
return { success: false, error: `Download failed: ${e.message}` };
|
||||
}
|
||||
} else if (source === 'file' && localPath) {
|
||||
const cleanLocal = expandPath(localPath);
|
||||
if (fs.existsSync(cleanLocal)) {
|
||||
updateFile = cleanLocal;
|
||||
} else {
|
||||
return { success: false, error: 'Local update file not found' };
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateFile) return { success: false, error: 'No update source provided' };
|
||||
|
||||
// 2. Install Logic (Stub)
|
||||
// Real implementation depends on OS and packaging format.
|
||||
// macOS: Mount DMG, copy .app to /Applications? Or Unzip .app?
|
||||
// Linux: chmod +x AppImage and move?
|
||||
|
||||
console.log(`Ready to install update from: ${updateFile}`);
|
||||
|
||||
// For now, just return success so the UI knows we "downloaded" it.
|
||||
return { success: true, message: 'Update downloaded/located. Installation logic requires packaging specifics.', downloadedPath: updateFile };
|
||||
});
|
||||
}
|
||||
32
src/preload/index.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('aetherNative', {
|
||||
get_seed_config: () => ipcRenderer.invoke('get-seed-config'),
|
||||
get_device_config: () => ipcRenderer.invoke('get-device-config'),
|
||||
get_jwt: () => ipcRenderer.invoke('get-jwt'),
|
||||
get_device_info: () => ipcRenderer.invoke('get-device-info'),
|
||||
|
||||
open_folder: (path: string) => ipcRenderer.invoke('native:open-folder', path),
|
||||
run_cmd: (args: any) => ipcRenderer.invoke('native:run-cmd', args),
|
||||
run_cmd_sync: (args: any) => ipcRenderer.invoke('native:run-cmd-sync', args),
|
||||
run_osascript: (script: string) => ipcRenderer.invoke('native:run-osascript', script),
|
||||
kill_processes: (args: any) => ipcRenderer.invoke('native:kill-processes', args),
|
||||
open_local_file_v2: (path: string) => ipcRenderer.invoke('native:open-local-file-v2', path),
|
||||
|
||||
check_cache: (args: any) => ipcRenderer.invoke('native:check-cache', args),
|
||||
download_to_cache: (args: any) => ipcRenderer.invoke('native:download-to-cache', args),
|
||||
copy_from_cache_to_temp: (args: any) => ipcRenderer.invoke('native:copy-from-cache-to-temp', args),
|
||||
launch_from_cache: (args: any) => ipcRenderer.invoke('native:launch-from-cache', args),
|
||||
launch_presentation: (args: any) => ipcRenderer.invoke('native:launch-presentation', args),
|
||||
control_presentation: (args: any) => ipcRenderer.invoke('native:control-presentation', args),
|
||||
list_tools: () => ipcRenderer.invoke('native:list-tools'),
|
||||
|
||||
// System Handlers (V5)
|
||||
set_wallpaper: (args: any) => ipcRenderer.invoke('native:set-wallpaper', args),
|
||||
update_app: (args: any) => ipcRenderer.invoke('native:update-app', args),
|
||||
window_control: (args: any) => ipcRenderer.invoke('native:window-control', args),
|
||||
manage_recording: (args: any) => ipcRenderer.invoke('native:manage-recording', args),
|
||||
set_display_layout: (args: any) => ipcRenderer.invoke('native:set-display-layout', args),
|
||||
power_control: (args: any) => ipcRenderer.invoke('native:power-control', args),
|
||||
open_external: (args: any) => ipcRenderer.invoke('native:open-external', args),
|
||||
});
|
||||