chore: reformat codebase using Prettier due to frequent instability in the Deno formatter

This commit is contained in:
nekename committed 2026-06-26 18:49:33 +01:00
1 parent 494b3ac544
commit 3a4486e661
38 files changed
+431 -403

No files matched your search

+1
View File
@@ -0,0 +1 @@
LICENSE.md
+3 -6
View File
@@ -1,11 +1,8 @@
{
"folders": [
{ "path": ".." },
{ "path": "../plugins/com.amansprojects.starterpack.sdPlugin" }
],
"folders": [{ "path": ".." }, { "path": "../plugins/com.amansprojects.starterpack.sdPlugin" }],
"settings": {
"editor.insertSpaces": false,
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}
"files.trimTrailingWhitespace": true,
},
}
+2 -10
View File
@@ -6,11 +6,7 @@
"request": "launch",
"name": "Debug (debug)",
"cargo": {
"args": [
"build",
"--manifest-path=./src-tauri/Cargo.toml",
"--no-default-features"
]
"args": ["build", "--manifest-path=./src-tauri/Cargo.toml", "--no-default-features"]
},
"preLaunchTask": "frontend:dev"
},
@@ -19,11 +15,7 @@
"request": "launch",
"name": "Debug (release)",
"cargo": {
"args": [
"build",
"--release",
"--manifest-path=./src-tauri/Cargo.toml"
]
"args": ["build", "--release", "--manifest-path=./src-tauri/Cargo.toml"]
},
"preLaunchTask": "frontend:build"
}
+7
View File
@@ -3,6 +3,7 @@
## Token Efficiency
Respond like smart caveman. Cut all filler, keep technical substance.
- Drop articles (a, an, the), filler (just, really, basically, actually).
- Drop pleasantries (sure, certainly, happy to).
- No hedging. Fragments fine. Short synonyms.
@@ -12,6 +13,7 @@ Respond like smart caveman. Cut all filler, keep technical substance.
## Architecture Overview
OpenDeck is a Tauri desktop application for controlling Elgato Stream Deck devices. It's built with:
- **Backend**: Rust (Tauri v2) - device communication, plugin management, WebSocket/HTTP servers
- **Frontend**: SvelteKit + TypeScript + Tailwind CSS v4 - UI rendered in webview
- **Build Tool**: Deno (not Node.js) - manages tasks and dependencies
@@ -19,6 +21,7 @@ OpenDeck is a Tauri desktop application for controlling Elgato Stream Deck devic
### Core Architecture Pattern
OpenDeck acts as a **host application** that communicates with **plugins** (separate processes):
1. Plugins connect via WebSocket (port dynamically allocated, starting from 57116)
2. Static assets served via `tiny_http` webserver (port = WebSocket port + 2)
3. Plugin property inspectors (HTML/JS) run in iframes and use separate WebSocket connections
@@ -53,12 +56,14 @@ plugins/com.amansprojects.starterpack.sdPlugin/ # Plugin with basic actions
### Type Synchronization
TypeScript types in `src/lib/` **must** mirror Rust structs in `src-tauri/src/shared.rs`:
- `Context`, `ActionInstance`, `ActionState`, `DeviceInfo`, `Profile`
- Changes to Rust structs require updating corresponding TypeScript types
### Context System
A `Context` identifies a button/encoder position:
```rust
struct Context {
device: String, // Device vendor prefix and serial number
@@ -111,6 +116,7 @@ An `ActionContext` extends this with an action instance index for nested actions
### Profile Management
Profiles are device-specific JSON files in `<config_dir>/<device_id>/<profile_name>.json`:
```rust
// Read profile
let locks = crate::store::profiles::acquire_locks().await;
@@ -147,6 +153,7 @@ Auto-switching: `application_watcher.rs` polls active window every 250ms, trigge
### WebSocket Protocol
Port allocation: `PORT_BASE` (WebSocket), `PORT_BASE + 2` (HTTP static files)
- Dynamic port selection: Tries ports starting at 57116 until both WebSocket and HTTP ports are available
- Registration: Plugins send `RegisterEvent::RegisterPlugin { uuid }`, property inspectors send `RegisterPropertyInspector`
- Message queuing: `PLUGIN_QUEUES` buffers messages until plugin connects
+18 -15
View File
@@ -30,19 +30,21 @@ Special thanks go to the developers of [Tauri](https://github.com/tauri-apps/tau
> [!TIP]
> If you're using a Debian, Ubuntu, Fedora, Fedora Atomic, openSUSE or Arch-based distribution, you can try the automated installation script:
>
> ```bash
> curl -sSL https://raw.githubusercontent.com/nekename/OpenDeck/main/install_opendeck.sh | bash
> ```
>
> The script installs OpenDeck from a released .deb or .rpm file, the AUR, or Flathub, appropriately, and also installs and reloads the appropriate udev subsystem rules. Additionally, you can choose to install Wine and/or Node.js from your distribution during the process.
- Download the latest release from [GitHub Releases](https://github.com/nekename/OpenDeck/releases/latest).
- You should avoid AppImage releases of OpenDeck as they tend to have problems (you should also just avoid AppImages in general).
- For users of Arch-based distributions, there are the `opendeck` and `opendeck-bin` AUR packages for the latest release, as well as the `opendeck-git` AUR package for the latest commit on the `main` branch of this repository.
- You should avoid AppImage releases of OpenDeck as they tend to have problems (you should also just avoid AppImages in general).
- For users of Arch-based distributions, there are the `opendeck` and `opendeck-bin` AUR packages for the latest release, as well as the `opendeck-git` AUR package for the latest commit on the `main` branch of this repository.
- Install OpenDeck using your package manager of choice.
- Install the appropriate udev subsystem rules from [here](https://raw.githubusercontent.com/OpenActionAPI/rust-elgato-streamdeck/main/40-streamdeck.rules):
- If you're using a `.deb` or `.rpm` release artifact, this file should be installed automatically.
- Otherwise, download and copy it to the correct location with `sudo cp 40-streamdeck.rules /etc/udev/rules.d/`.
- In both cases, you will need to reload your udev subsystem rules with `sudo udevadm control --reload-rules && sudo udevadm trigger`.
- If you're using a `.deb` or `.rpm` release artifact, this file should be installed automatically.
- Otherwise, download and copy it to the correct location with `sudo cp 40-streamdeck.rules /etc/udev/rules.d/`.
- In both cases, you will need to reload your udev subsystem rules with `sudo udevadm control --reload-rules && sudo udevadm trigger`.
- If you intend to use some compiled plugins that are not compiled for Linux, you will need to have [Wine](https://www.winehq.org/) installed on your system. Some plugins may also depend on Wine Mono (which is sometimes, but not always included, in your distro's packaging of Wine).
> [!NOTE]
@@ -57,14 +59,14 @@ Special thanks go to the developers of [Tauri](https://github.com/tauri-apps/tau
- Download the latest release from [GitHub Releases](https://github.com/nekename/OpenDeck/releases/latest).
- If you downloaded a `.dmg`, open the downloaded disk image and drag the application inside into your Applications folder; otherwise, extract the `.tar.gz` to your Applications folder.
- Open the installed application. Note: if you receive a warning about OpenDeck being distributed by an unknown developer, *right-click the app in Finder and then click Open* to suppress the warning.
- Open the installed application. Note: if you receive a warning about OpenDeck being distributed by an unknown developer, _right-click the app in Finder and then click Open_ to suppress the warning.
- If you intend to use plugins that are only compiled for Windows, you will need to have [Wine](https://www.winehq.org/) installed on your system.
## Support
### How do I...?
To edit an action's settings, left-click on it to display its *property inspector*. To remove an action, right-click on it and choose "Delete" from the context menu.
To edit an action's settings, left-click on it to display its _property inspector_. To remove an action, right-click on it and choose "Delete" from the context menu.
To edit an action's appearance, right-click on it and select "Edit" from the context menu. You can then customise the image and text for each of its states. Left-click on the image to choose an image from your filesystem or right-click on the image to reset it to the plugin-provided default.
@@ -77,13 +79,13 @@ To change other options, open Settings. From here, you can also view information
- Ensure you are running the latest version of OpenDeck, as well as recent versions of related software (e.g. Spotify or OBS).
- Check the [FAQ](https://github.com/nekename/OpenDeck/wiki/0.-FAQ) and [GitHub Issues](https://github.com/nekename/OpenDeck/issues) to see if there's a fix for your problem already.
- Check the OpenDeck log file for any important messages. This file should be included with any support request.
- You can also run OpenDeck from the terminal to see the logs directly if it's easier than finding the log file or if the log file is empty or missing details.
- For issues with plugins, you can also check the plugin's logs (in the same folder, sometimes as well as a file named `plugin.log` or similar in the plugin's own folder).
- The log directory can be opened from the settings page of OpenDeck, or alternatively located manually at the paths below:
- Linux: `~/.local/share/opendeck/logs/`
- Flatpak: `~/.var/app/me.amankhanna.opendeck/data/opendeck/logs/`
- Windows: `%appdata%\opendeck\logs\`
- macOS: `~/Library/Logs/opendeck/`
- You can also run OpenDeck from the terminal to see the logs directly if it's easier than finding the log file or if the log file is empty or missing details.
- For issues with plugins, you can also check the plugin's logs (in the same folder, sometimes as well as a file named `plugin.log` or similar in the plugin's own folder).
- The log directory can be opened from the settings page of OpenDeck, or alternatively located manually at the paths below:
- Linux: `~/.local/share/opendeck/logs/`
- Flatpak: `~/.var/app/me.amankhanna.opendeck/data/opendeck/logs/`
- Windows: `%appdata%\opendeck\logs\`
- macOS: `~/Library/Logs/opendeck/`
- When trying to run compiled plugins built for Windows on Linux or macOS, please ensure you have the latest version of Wine (and Wine Mono) installed on your system.
- If your device isn't showing up, ensure you have the correct permissions to access it (e.g. on Linux, installing udev subsystem rules and restarting your system), and that you have restarted OpenDeck since connecting it.
@@ -101,11 +103,12 @@ To change other options, open Settings. From here, you can also view information
You'll need to ensure that all of the [prerequisites for building a Tauri application](https://tauri.app/start/prerequisites) are satisfied to build OpenDeck, as well as making sure that [Deno](https://deno.com/) is installed. On Linux, you'll also need `libudev` and `libdbus` installed for your distribution. After running `deno install`, you can use `deno task tauri dev` and `deno task tauri build` to work with OpenDeck.
Before each commit, please ensure that all of the following are completed:
1. Rust code has been linted using `cargo clippy` and it discovers no violations
2. Rust code has been formatted using `cargo fmt`
3. TypeScript code has been checked using `deno check` and linted using `deno lint` and they discover no violations
4. Svelte code has been linted using `deno task check` and it discovers no violations
5. Frontend code has been formatted using `deno fmt --unstable-component`
5. Frontend code has been formatted using `deno task format`
When submitting contributions, please adhere to the [Conventional Commits specification](https://conventionalcommits.org/) for commit messages. You will also need to [sign your commits](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). Feel free to reach out on the support channels above for guidance when contributing!
-5
View File
@@ -1,10 +1,5 @@
{
"exclude": ["src-tauri", "node_modules", "build", ".svelte-kit"],
"fmt": {
"exclude": ["**/*.md", "**/.*/**/*.md"],
"useTabs": true,
"lineWidth": 200
},
"lint": {
"rules": {
"exclude": ["no-explicit-any"]
Generated
+15
View File
@@ -19,6 +19,8 @@
"npm:marked-base-url@^1.1.0": "1.1.8_marked@17.0.1",
"npm:marked@17": "17.0.1",
"npm:phosphor-svelte@2": "2.0.1_svelte@4.2.20",
"npm:prettier-plugin-svelte@^3.5.0": "3.5.2_prettier@3.8.4_svelte@4.2.20",
"npm:prettier@^3.8.0": "3.8.4",
"npm:svelte-check@^4.3.0": "4.3.6_svelte@4.2.20_typescript@5.9.3",
"npm:svelte@^4.2.0": "4.2.20",
"npm:tailwindcss@^4.1.0": "4.1.18",
@@ -919,6 +921,17 @@
"source-map-js"
]
},
"prettier-plugin-svelte@3.5.2_prettier@3.8.4_svelte@4.2.20": {
"integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==",
"dependencies": [
"prettier",
"svelte"
]
},
"prettier@3.8.4": {
"integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==",
"bin": true
},
"readdirp@4.1.2": {
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="
},
@@ -1071,6 +1084,8 @@
"npm:marked-base-url@^1.1.0",
"npm:marked@17",
"npm:phosphor-svelte@2",
"npm:prettier-plugin-svelte@^3.5.0",
"npm:prettier@^3.8.0",
"npm:svelte-check@^4.3.0",
"npm:svelte@^4.2.0",
"npm:tailwindcss@^4.1.0",
+11 -1
View File
@@ -8,7 +8,8 @@
"dev": "vite dev",
"build": "vite build",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"tauri": "tauri"
"tauri": "tauri",
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-static": "^3.0",
@@ -17,6 +18,8 @@
"@tailwindcss/vite": "^4.1",
"@tauri-apps/cli": "^2.10",
"lightningcss": "^1.31",
"prettier": "^3.8",
"prettier-plugin-svelte": "^3.5",
"svelte": "^4.2",
"svelte-check": "^4.3",
"tailwindcss": "^4.1",
@@ -33,5 +36,12 @@
"marked-alert": "^2.1",
"marked-base-url": "^1.1",
"phosphor-svelte": "^2.0"
},
"prettier": {
"useTabs": true,
"printWidth": 160,
"plugins": [
"prettier-plugin-svelte"
]
}
}
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<script>
@@ -14,10 +14,12 @@
const websocket = new WebSocket("ws://localhost:" + inPort);
websocket.onopen = () => {
websocket.send(JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}));
websocket.send(
JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}),
);
};
const action = document.getElementById("action");
@@ -29,14 +31,16 @@
update = () => {
valueDisplay.innerText = value.value;
websocket.send(JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
action: action.value,
value: parseInt(value.value),
},
}));
websocket.send(
JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
action: action.value,
value: parseInt(value.value),
},
}),
);
};
}
</script>
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<script>
@@ -8,10 +8,12 @@
const websocket = new WebSocket("ws://localhost:" + inPort);
inActionInfo = JSON.parse(inActionInfo);
websocket.onopen = () => {
websocket.send(JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}));
websocket.send(
JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}),
);
};
const down = document.getElementById("down");
@@ -32,16 +34,18 @@
}
update = () => {
websocket.send(JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
down: down.value,
up: up.value,
anticlockwise: anticlockwise.value,
clockwise: clockwise.value,
},
}));
websocket.send(
JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
down: down.value,
up: up.value,
anticlockwise: anticlockwise.value,
clockwise: clockwise.value,
},
}),
);
};
websocket.onmessage = (event) => {
@@ -72,18 +76,30 @@
<div id="encoder" style="display: none">
<div class="sdpi-item">
<div class="sdpi-item-label">Dial rotate anticlockwise</div>
<input id="anticlockwise" oninput="update()" class="sdpi-item-value" style="font-family: monospace" placeholder="Input to be executed on dial rotate anticlockwise" />
<input
id="anticlockwise"
oninput="update()"
class="sdpi-item-value"
style="font-family: monospace"
placeholder="Input to be executed on dial rotate anticlockwise"
/>
</div>
<div class="sdpi-item">
<div class="sdpi-item-label">Dial rotate clockwise</div>
<input id="clockwise" oninput="update()" class="sdpi-item-value" style="font-family: monospace" placeholder="Input to be executed on dial rotate clockwise" />
<input
id="clockwise"
oninput="update()"
class="sdpi-item-value"
style="font-family: monospace"
placeholder="Input to be executed on dial rotate clockwise"
/>
</div>
</div>
<details open>
<summary style="margin-bottom: 0.25rem; font-size: 1.25rem">Details</summary>
<code style="word-break: break-all">[t("hello world!"),m(10,10,r),s(5),b(l),k(ctrl,p),k(uni('a')),k(ctrl,r)]</code>
<p>The example above becomes something like this:</p>
<!-- deno-fmt-ignore -->
<!-- prettier-ignore -->
<code style="white-space: pre;">[
Text("hello world!"),
MoveMouse(10, 10, Relative),
@@ -94,10 +110,10 @@
Key(Control, Release),
]</code>
<p>
As you can see, <code>Vertical</code> is the default for the <code>Scroll</code> action if an axis is not specified,
<code>Click</code> (press followed by release) is the default for the <code>Button</code> and <code>Key</code> actions if a direction is not specified, and so on.
As you can see, <code>Vertical</code> is the default for the <code>Scroll</code> action if an axis is not specified, <code>Click</code> (press followed
by release) is the default for the <code>Button</code> and <code>Key</code> actions if a direction is not specified, and so on.
</p>
<!-- deno-fmt-ignore -->
<!-- prettier-ignore -->
<p>
You can find a list of all of the keycodes
<span style="color: dodgerblue; cursor: pointer;" onclick="window.open('https://github.com/enigo-rs/enigo/blob/v0.5.0/src/keycodes.rs', '_blank');">here</span>.
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<script>
@@ -8,10 +8,12 @@
const websocket = new WebSocket("ws://localhost:" + inPort);
inActionInfo = JSON.parse(inActionInfo);
websocket.onopen = () => {
websocket.send(JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}));
websocket.send(
JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}),
);
};
const down = document.getElementById("down");
@@ -30,16 +32,18 @@
}
update = () => {
websocket.send(JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
down: down.value,
up: up.value,
anticlockwise: anticlockwise.value,
clockwise: clockwise.value,
},
}));
websocket.send(
JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
down: down.value,
up: up.value,
anticlockwise: anticlockwise.value,
clockwise: clockwise.value,
},
}),
);
};
}
</script>
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<script>
@@ -8,10 +8,12 @@
const websocket = new WebSocket("ws://localhost:" + inPort);
inActionInfo = JSON.parse(inActionInfo);
websocket.onopen = () => {
websocket.send(JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}));
websocket.send(
JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}),
);
};
if (inActionInfo.payload.controller == "Encoder") {
@@ -32,17 +34,19 @@
show.checked = inActionInfo.payload.settings.show ?? false;
update = () => {
websocket.send(JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
down: down.value,
up: up.value,
rotate: rotate.value,
file: file.value,
show: show.checked,
},
}));
websocket.send(
JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
down: down.value,
up: up.value,
rotate: rotate.value,
file: file.value,
show: show.checked,
},
}),
);
};
}
</script>
@@ -63,7 +67,8 @@
<input id="rotate" oninput="update()" class="sdpi-item-value" style="font-family: monospace" />
</div>
<p style="padding-left: 0.75rem; padding-right: 0.75rem">
Usages of <code>%d</code> will be substituted with the number of ticks turned, where negative values signify counterclockwise rotation and positive values signify clockwise rotation.
Usages of <code>%d</code> will be substituted with the number of ticks turned, where negative values signify counterclockwise rotation and positive
values signify clockwise rotation.
</p>
</div>
<br />
@@ -19,7 +19,18 @@ html {
html,
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
"Segoe UI",
Roboto,
Helvetica,
Arial,
sans-serif,
"Apple Color Emoji",
"Segoe UI Emoji",
"Segoe UI Symbol";
font-size: 9pt;
background-color: var(--sdpi-bgcolor);
color: #9a9a9a;
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<script>
@@ -8,10 +8,12 @@
const websocket = new WebSocket("ws://localhost:" + inPort);
inActionInfo = JSON.parse(inActionInfo);
websocket.onopen = () => {
websocket.send(JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}));
websocket.send(
JSON.stringify({
event: inRegisterEvent,
uuid: inPropertyInspectorUUID,
}),
);
};
const device = document.getElementById("device");
@@ -29,16 +31,18 @@
}
update = () => {
websocket.send(JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
device: device.value ?? inActionInfo.device,
profile: profile.value,
anticlockwise: anticlockwise.value,
clockwise: clockwise.value,
},
}));
websocket.send(
JSON.stringify({
event: "setSettings",
context: inActionInfo.context,
payload: {
device: device.value ?? inActionInfo.device,
profile: profile.value,
anticlockwise: anticlockwise.value,
clockwise: clockwise.value,
},
}),
);
};
websocket.onmessage = (event) => {
@@ -17,4 +17,5 @@ try {
}
await copy("assets", outDir);
if (!(await new Deno.Command("cargo", { args: ["install", "--path", ".", "--target", target, "--root", join(outDir, target)] }).spawn().status).success) Deno.exit(1);
if (!(await new Deno.Command("cargo", { args: ["install", "--path", ".", "--target", target, "--root", join(outDir, target)] }).spawn().status).success)
Deno.exit(1);
+4 -1
View File
@@ -54,7 +54,10 @@ async function bumpStarterPackManifestVersion(version: string) {
async function prependMetainfoRelease(version: string) {
const fromRef = await run("git", ["describe", "--tags", "--abbrev=0"]);
const subjectsRaw = await run("git", ["log", "--reverse", "--pretty=format:%s", `${fromRef}..HEAD`]);
const subjects = subjectsRaw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
const subjects = subjectsRaw
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean);
const xml = await Deno.readTextFile(METAINFO);
if (xml.includes(`version="${version}"`)) throw new Error(`opendeck.metainfo.xml already contains ${version}`);
+2 -10
View File
@@ -1,14 +1,6 @@
{
"identifier": "main",
"local": true,
"windows": [
"main"
],
"permissions": [
"core:default",
"core:window:allow-set-min-size",
"core:window:allow-set-size",
"dialog:default",
"deep-link:default"
]
"windows": ["main"],
"permissions": ["core:default", "core:window:allow-set-min-size", "core:window:allow-set-size", "dialog:default", "deep-link:default"]
}
+1 -5
View File
@@ -30,11 +30,7 @@
"shortDescription": "Use stream controllers",
"longDescription": "OpenDeck is a desktop application for using stream controller devices like the Elgato Stream Deck. OpenDeck supports plugins made for the original Stream Deck SDK, allowing many plugins made for the Elgato software ecosystem to be used.",
"category": "Productivity",
"icon": [
"icons/icon.png",
"icons/icon.icns",
"icons/icon.ico"
],
"icon": ["icons/icon.png", "icons/icon.icns", "icons/icon.ico"],
"macOS": {
"exceptionDomain": "localhost",
"signingIdentity": "-"
+26 -6
View File
@@ -4,15 +4,26 @@
@import "tailwindcss/preflight.css";
@import "tailwindcss/utilities.css";
*, ::after, ::before, ::backdrop, ::file-selector-button {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
button:focus:not(:focus-visible), select:focus:not(:focus-visible), input:focus, textarea:focus {
button:focus:not(:focus-visible),
select:focus:not(:focus-visible),
input:focus,
textarea:focus {
@apply outline-hidden;
}
button, input[type="radio"], input[type="checkbox"], input[type="range"], input[type="color"] {
button,
input[type="radio"],
input[type="checkbox"],
input[type="range"],
input[type="color"] {
@apply cursor-pointer;
}
@@ -70,13 +81,22 @@ button, input[type="radio"], input[type="checkbox"], input[type="range"], input[
@apply pointer-events-none content-["_"];
}
.plugin-readme h1, .plugin-readme h2, .plugin-readme h3, .plugin-readme h4, .plugin-readme h5, .plugin-readme h6 {
.plugin-readme h1,
.plugin-readme h2,
.plugin-readme h3,
.plugin-readme h4,
.plugin-readme h5,
.plugin-readme h6 {
@apply font-medium text-neutral-200;
}
.plugin-readme h1:not(:first-child), .plugin-readme h2:not(:first-child), .plugin-readme h3:not(:first-child) {
.plugin-readme h1:not(:first-child),
.plugin-readme h2:not(:first-child),
.plugin-readme h3:not(:first-child) {
@apply mt-5;
}
.plugin-readme h1, .plugin-readme h2, .plugin-readme h3 {
.plugin-readme h1,
.plugin-readme h2,
.plugin-readme h3 {
@apply mb-3 pb-1.5 border-b border-neutral-600;
}
.plugin-readme h1 {
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
+8 -17
View File
@@ -24,7 +24,7 @@
$: {
let lowerCaseQuery = query.toLowerCase().trim();
filteredCategories = Object.entries(categories)
.sort((a, b) => a[0] == PRODUCT_NAME ? -1 : b[0] == PRODUCT_NAME ? 1 : a[0].localeCompare(b[0]))
.sort((a, b) => (a[0] == PRODUCT_NAME ? -1 : b[0] == PRODUCT_NAME ? 1 : a[0].localeCompare(b[0])))
.map(([categoryName, { icon, actions }]): [string, { icon?: string; actions: Action[] }] => {
if (!categoryName.toLowerCase().includes(lowerCaseQuery)) {
actions = actions.filter((action) => action.name.toLowerCase().includes(lowerCaseQuery));
@@ -79,13 +79,7 @@
<div class="flex flex-col w-[18rem] h-full bg-neutral-900 border-l border-neutral-700">
<div class="flex flex-row items-center m-2 bg-neutral-700 border border-neutral-600 rounded-lg">
<MagnifyingGlass size="13" class="ml-2 mr-1 text-neutral-300" />
<input
bind:value={query}
class="w-full p-1 text-sm text-neutral-300"
placeholder={$t("action_list.search_placeholder")}
type="search"
spellcheck="false"
/>
<input bind:value={query} class="w-full p-1 text-sm text-neutral-300" placeholder={$t("action_list.search_placeholder")} type="search" spellcheck="false" />
</div>
<span id="action-list-hint" class="sr-only">{$t("action_list.hint")}</span>
@@ -95,21 +89,18 @@
<summary class="pl-4 py-3 text-lg font-semibold text-neutral-300 hover:bg-neutral-800 transition-colors cursor-pointer">
{#if icon || (actions[0] && plugins.find((x) => x.id == actions[0].plugin) && categories[name].actions.every((x) => x.plugin == actions[0].plugin))}
<img
src={icon ? (!icon.startsWith("opendeck/") ? getWebserverUrl(icon) : icon.replace("opendeck", "")) : getWebserverUrl(plugins.find((x) => x.id == actions[0].plugin).icon)}
src={icon
? !icon.startsWith("opendeck/")
? getWebserverUrl(icon)
: icon.replace("opendeck", "")
: getWebserverUrl(plugins.find((x) => x.id == actions[0].plugin).icon)}
alt={name}
class="w-5 h-5 rounded-xs ml-1 -mt-1 inline"
/>
{/if}
<span class="ml-1">{name}</span>
</summary>
<div
role="listbox"
aria-label={name}
aria-describedby="action-list-hint"
tabindex="-1"
on:keydown={handleListKeydown}
on:focusin={handleListFocusin}
>
<div role="listbox" aria-label={name} aria-describedby="action-list-hint" tabindex="-1" on:keydown={handleListKeydown} on:focusin={handleListFocusin}>
{#each actions as action, i}
<div
class="flex flex-row items-center p-2 pl-6 bg-neutral-950 hover:bg-neutral-900 transition-colors border-t border-neutral-800 cursor-grab active:cursor-grabbing"
+5 -5
View File
@@ -41,19 +41,19 @@
}
});
(async () => devices = await invoke("get_devices"))();
listen("devices", ({ payload }: { payload: { [id: string]: DeviceInfo } }) => devices = payload);
(async () => (devices = await invoke("get_devices")))();
listen("devices", ({ payload }: { payload: { [id: string]: DeviceInfo } }) => (devices = payload));
let buildInfo: string;
(async () => buildInfo = await invoke("get_build_info"))();
(async () => (buildInfo = await invoke("get_build_info")))();
const window = getCurrentWindow();
$: {
if (devices[value]) {
const effectiveCols = Math.min(Math.max(devices[value].columns, devices[value].encoders, devices[value].touchpoints), 8);
const effectiveRows = Math.min(devices[value].rows + Math.min(devices[value].encoders, 1) + Math.min(devices[value].touchpoints, 1), 4);
const idealWidth = (effectiveCols * 132) + 416;
const idealHeight = (effectiveRows * 132) + 384 + (buildInfo?.split("</summary>")[0]?.includes("darwin") ? 28 : 0);
const idealWidth = effectiveCols * 132 + 416;
const idealHeight = effectiveRows * 132 + 384 + (buildInfo?.split("</summary>")[0]?.includes("darwin") ? 28 : 0);
(async () => {
const width = Math.min(idealWidth, screen.availWidth);
const height = Math.min(idealHeight, screen.availHeight);
+25 -25
View File
@@ -76,7 +76,7 @@
}
$: overflowsX = Math.max(device.columns, device.encoders, device.touchpoints) > 8;
$: overflowsY = (device.rows + Math.min(device.encoders, 1) + Math.min(device.touchpoints, 1)) > 4;
$: overflowsY = device.rows + Math.min(device.encoders, 1) + Math.min(device.touchpoints, 1) > 4;
// Grid navigation: track focused cell and compute row lengths for arrow key movement.
let focusedRow = 0;
@@ -85,7 +85,7 @@
$: gridRowLengths = [
...Array(device.rows).fill(device.columns),
...(device.encoders > 0 ? [device.encoders] : []),
...((device.touchpoints > 0 || device.infobars > 0) ? [device.touchpoints + device.infobars] : []),
...(device.touchpoints > 0 || device.infobars > 0 ? [device.touchpoints + device.infobars] : []),
];
$: encoderRowIndex = device.rows;
$: touchpointRowIndex = device.rows + (device.encoders > 0 ? 1 : 0);
@@ -158,21 +158,6 @@
}
</script>
<style>
.device-fade-x {
mask-image: linear-gradient(to right, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
}
.device-fade-y {
mask-image: linear-gradient(to bottom, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
}
.device-fade-xy {
mask-image:
linear-gradient(to right, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent),
linear-gradient(to bottom, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
mask-composite: intersect;
}
</style>
{#key device}
<span id="grid-description" class="sr-only">{$t("device_view.grid_description")}</span>
<div
@@ -196,11 +181,11 @@
<div class="flex flex-row" role="row">
{#each { length: device.columns } as _, c}
<Key
context={{ device: device.id, profile: profile.id, controller: "Keypad", position: (r * device.columns) + c }}
bind:inslot={profile.keys[(r * device.columns) + c]}
context={{ device: device.id, profile: profile.id, controller: "Keypad", position: r * device.columns + c }}
bind:inslot={profile.keys[r * device.columns + c]}
on:dragover={handleDragOver}
on:drop={(event) => handleDrop(event, "Keypad", (r * device.columns) + c)}
on:dragstart={(event) => handleDragStart(event, "Keypad", (r * device.columns) + c)}
on:drop={(event) => handleDrop(event, "Keypad", r * device.columns + c)}
on:dragstart={(event) => handleDragStart(event, "Keypad", r * device.columns + c)}
{handlePaste}
size={device.id.startsWith("sd-") && device.rows == 4 && device.columns == 8 ? 192 : 144}
label="{$t('device_view.key')} {String.fromCharCode(65 + r)}{c + 1}"
@@ -248,11 +233,11 @@
{/each}
{/if}
<Key
context={{ device: device.id, profile: profile.id, controller: "Keypad", position: (device.rows * device.columns) + i }}
bind:inslot={profile.keys[(device.rows * device.columns) + i]}
context={{ device: device.id, profile: profile.id, controller: "Keypad", position: device.rows * device.columns + i }}
bind:inslot={profile.keys[device.rows * device.columns + i]}
on:dragover={handleDragOver}
on:drop={(event) => handleDrop(event, "Keypad", (device.rows * device.columns) + i)}
on:dragstart={(event) => handleDragStart(event, "Keypad", (device.rows * device.columns) + i)}
on:drop={(event) => handleDrop(event, "Keypad", device.rows * device.columns + i)}
on:dragstart={(event) => handleDragStart(event, "Keypad", device.rows * device.columns + i)}
{handlePaste}
size={device.id.startsWith("sd-") && device.rows == 4 && device.columns == 8 ? 192 : 144}
isTouchPoint
@@ -263,3 +248,18 @@
</div>
</div>
{/key}
<style>
.device-fade-x {
mask-image: linear-gradient(to right, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
}
.device-fade-y {
mask-image: linear-gradient(to bottom, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
}
.device-fade-xy {
mask-image:
linear-gradient(to right, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent),
linear-gradient(to bottom, transparent, black 7.5rem, black calc(100% - 7.5rem), transparent);
mask-composite: intersect;
}
</style>
+7 -26
View File
@@ -73,7 +73,7 @@
{/each}
</select>
</div>
<button class="ml-2 mr-1 float-right text-xl text-neutral-300" on:click={() => showEditor = false} aria-label={$t("settings.close")}>✕</button>
<button class="ml-2 mr-1 float-right text-xl text-neutral-300" on:click={() => (showEditor = false)} aria-label={$t("settings.close")}>✕</button>
</div>
<div class="flex flex-row mx-1">
<div class="flex flex-col justify-center items-center mt-2 mb-1">
@@ -141,12 +141,7 @@
class="mt-1 px-0.5 text-sm text-neutral-300 bg-neutral-600 hover:bg-neutral-500 transition-colors border border-neutral-500 rounded-lg"
>
{$t("instance_editor.background")}
<input
bind:this={backgroundColourInput}
type="color"
bind:value={instance.states[state].background_colour}
class="absolute invisible w-0 h-0"
/>
<input bind:this={backgroundColourInput} type="color" bind:value={instance.states[state].background_colour} class="absolute invisible w-0 h-0" />
</button>
<button
on:click={() => solidColourInput.click()}
@@ -225,17 +220,8 @@
id="editor-colour"
/>
<label for="editor-show" class="mr-2">{$t("instance_editor.show")}</label>
<input
type="checkbox"
bind:checked={instance.states[state].show}
class="mr-4 mt-1 scale-125"
id="editor-show"
/>
<select
bind:value={instance.states[state].alignment}
class="px-1! py-0.5!"
aria-label={$t("instance_editor.alignment")}
>
<input type="checkbox" bind:checked={instance.states[state].show} class="mr-4 mt-1 scale-125" id="editor-show" />
<select bind:value={instance.states[state].alignment} class="px-1! py-0.5!" aria-label={$t("instance_editor.alignment")}>
<option value="top">{$t("instance_editor.alignment.top")}</option>
<option value="middle">{$t("instance_editor.alignment.middle")}</option>
<option value="bottom">{$t("instance_editor.alignment.bottom")}</option>
@@ -287,7 +273,7 @@
<input
type="checkbox"
bind:checked={bold}
on:change={() => instance.states[state].style = bold && italic ? "Bold Italic" : bold ? "Bold" : italic ? "Italic" : "Regular"}
on:change={() => (instance.states[state].style = bold && italic ? "Bold Italic" : bold ? "Bold" : italic ? "Italic" : "Regular")}
class="mr-4 mt-1 scale-125"
id="editor-bold"
/>
@@ -295,17 +281,12 @@
<input
type="checkbox"
bind:checked={italic}
on:change={() => instance.states[state].style = bold && italic ? "Bold Italic" : bold ? "Bold" : italic ? "Italic" : "Regular"}
on:change={() => (instance.states[state].style = bold && italic ? "Bold Italic" : bold ? "Bold" : italic ? "Italic" : "Regular")}
class="mr-4 mt-1 scale-125"
id="editor-italic"
/>
<label for="editor-underline" class="mr-3 underline">U</label>
<input
type="checkbox"
bind:checked={instance.states[state].underline}
class="mr-4 mt-1 scale-125"
id="editor-underline"
/>
<input type="checkbox" bind:checked={instance.states[state].underline} class="mr-4 mt-1 scale-125" id="editor-underline" />
<label for="editor-size" class="mr-2">{$t("instance_editor.font.size")}</label>
<input
type="number"
+12 -27
View File
@@ -87,8 +87,8 @@
event.preventDefault();
if (!active || !context) return;
const rect = canvas.getBoundingClientRect();
let x = (event instanceof MouseEvent && event.x) ? event.x : rect.left;
let y = (event instanceof MouseEvent && event.y) ? event.y : rect.bottom;
let x = event instanceof MouseEvent && event.x ? event.x : rect.left;
let y = event instanceof MouseEvent && event.y ? event.y : rect.bottom;
$openContextMenu = { context, x, y };
await tick();
contextMenuEl?.querySelector("button")?.focus();
@@ -134,14 +134,14 @@
timeouts.forEach(clearTimeout);
showOk = false;
showAlert = true;
timeouts.push(setTimeout(() => showAlert = false, 1.5e3));
timeouts.push(setTimeout(() => (showAlert = false), 1.5e3));
});
listen("show_ok", ({ payload }: { payload: string }) => {
if (!slot || payload != slot.context) return;
timeouts.forEach(clearTimeout);
showAlert = false;
showOk = true;
timeouts.push(setTimeout(() => showOk = false, 1.5e3));
timeouts.push(setTimeout(() => (showOk = false), 1.5e3));
});
let canvas: HTMLCanvasElement;
@@ -188,20 +188,17 @@
$: accessibleLabel = label + (slot ? ": " + slot.action.name + (state?.show && state?.text ? " - " + state.text : "") : "");
</script>
<div
class="relative"
style={`transform: scale(${(112 /* desired inner size */ / size) * scale});`}
>
<div class="relative" style={`transform: scale(${(112 /* desired inner size */ / size) * scale});`}>
<canvas
bind:this={canvas}
class="relative border-3 border-neutral-700 rounded-3xl outline-none outline-offset-2 outline-blue-500"
style={`margin: ${-((size + 3 * 2 /* border */ - 132 /* desired outer size */) / 2)}px;`}
style={`margin: ${-((size + 3 * 2 /* border */ - 132) /* desired outer size */ / 2)}px;`}
class:outline-solid={active && ((slot && $inspectedInstance == slot.context) || (context && $inspectedInstance == context))}
class:rounded-full!={context?.controller == "Encoder"}
class:rounded-lg!={context?.controller == "Infobar"}
class:bg-black={slot != null}
width={width}
height={height}
{width}
{height}
draggable={slot != null}
{tabindex}
{role}
@@ -239,32 +236,20 @@
style={`left: ${$openContextMenu.x}px; top: ${$openContextMenu.y}px;`}
>
{#if !slot}
<button
class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-lg cursor-pointer"
on:click|stopPropagation={paste}
>
<button class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-lg cursor-pointer" on:click|stopPropagation={paste}>
<Clipboard size="18" class="text-neutral-300" />
<span class="ml-2">{$t("key.paste")}</span>
</button>
{:else}
<button
class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-t-lg cursor-pointer"
on:click|stopPropagation={edit}
>
<button class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-t-lg cursor-pointer" on:click|stopPropagation={edit}>
<Pencil size="18" class="text-neutral-300" />
<span class="ml-2">{$t("key.edit")}</span>
</button>
<button
class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors cursor-pointer"
on:click|stopPropagation={copy}
>
<button class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors cursor-pointer" on:click|stopPropagation={copy}>
<Copy size="18" class="text-neutral-300" />
<span class="ml-2">{$t("key.copy")}</span>
</button>
<button
class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-b-lg cursor-pointer"
on:click|stopPropagation={clear}
>
<button class="flex flex-row items-center w-full p-2 hover:bg-neutral-600 transition-colors rounded-b-lg cursor-pointer" on:click|stopPropagation={clear}>
<Trash size="18" class="text-red-400" />
<span class="ml-2">{$t("key.delete")}</span>
</button>
+1 -4
View File
@@ -10,10 +10,7 @@
export let secondaryActionLabel: string = "";
</script>
<div
class="flex flex-row items-center m-2 p-2 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
class:hidden
>
<div class="flex flex-row items-center m-2 p-2 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg" class:hidden>
<img src={icon} class="w-24 h-24 rounded-lg" class:opacity-75={disconnected} alt={name} loading="lazy" />
<div class="ml-4 mr-2 text-neutral-300 wrap-anywhere" class:opacity-75={disconnected}>
<p class="font-semibold">{name}</p>
+1 -1
View File
@@ -5,7 +5,7 @@
import { invoke } from "@tauri-apps/api/core";
let buildInfo: string;
(async () => buildInfo = await invoke("get_build_info"))();
(async () => (buildInfo = await invoke("get_build_info")))();
</script>
<div class="flex flex-col justify-center items-center w-full h-full text-center text-neutral-300">
+7 -10
View File
@@ -33,10 +33,7 @@
async function addAction(action: Action) {
if (
(parentUuid == "opendeck.multiaction" && !action.supported_in_multi_actions) ||
(
parentUuid == "opendeck.toggleaction" &&
(action.uuid == "opendeck.multiaction" || action.uuid == "opendeck.toggleaction")
)
(parentUuid == "opendeck.toggleaction" && (action.uuid == "opendeck.multiaction" || action.uuid == "opendeck.toggleaction"))
) {
return;
}
@@ -113,7 +110,7 @@
/>
<div class="px-6 pt-6 pb-4 text-neutral-300">
<button class="float-right text-xl" on:click={() => $inspectedParentAction = null} aria-label={$t("settings.close")}>✕</button>
<button class="float-right text-xl" on:click={() => ($inspectedParentAction = null)} aria-label={$t("settings.close")}>✕</button>
<h1 class="font-semibold text-2xl">{parentUuid == "opendeck.toggleaction" ? $t("parent_action.toggle") : $t("parent_action.multi")}</h1>
</div>
@@ -121,7 +118,7 @@
<div
bind:this={listEl}
class="flex flex-col h-128 overflow-auto"
on:click={() => $inspectedInstance = null}
on:click={() => ($inspectedInstance = null)}
role="list"
aria-label="{parentUuid == 'opendeck.toggleaction' ? $t('parent_action.toggle') : $t('parent_action.multi')} {$t('parent_action.children')}"
on:keydown={handleListKeydown}
@@ -130,8 +127,8 @@
<!-- svelte-ignore a11y-no-noninteractive-tabindex a11y-no-noninteractive-element-interactions -->
<div
class="flex flex-row items-center mx-4 my-2 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg focus-within:outline-solid focus-within:outline-offset-2 focus-within:outline-blue-500"
on:click|stopPropagation={() => $inspectedInstance = instance.context}
on:focus|stopPropagation={() => $inspectedInstance = instance.context}
on:click|stopPropagation={() => ($inspectedInstance = instance.context)}
on:focus|stopPropagation={() => ($inspectedInstance = instance.context)}
on:keydown={(e) => {
if (e.key == "Delete") removeInstance(index, true);
}}
@@ -163,8 +160,8 @@
class="flex flex-row items-center mx-4 mt-2 mb-4 p-3 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-dashed border-neutral-600 rounded-lg focus-within:outline-solid focus-within:outline-offset-2 focus-within:outline-blue-500"
on:dragover={handleDragOver}
on:drop={handleDrop}
on:click={() => $inspectedInstance = null}
on:focus={() => $inspectedInstance = null}
on:click={() => ($inspectedInstance = null)}
on:focus={() => ($inspectedInstance = null)}
on:keydown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key == "v") handlePaste();
}}
+2 -10
View File
@@ -74,20 +74,12 @@
<Popup show label={$t("plugin_details.title").replaceAll("{details.name}", details.name)}>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={close} aria-label={$t("settings.close")}>✕</button>
<div class="flex flex-row items-start">
<img
src={"https://openactionapi.github.io/plugins/icons/" + id + ".png"}
alt={details.name}
class="size-48 rounded-2xl"
/>
<img src={"https://openactionapi.github.io/plugins/icons/" + id + ".png"} alt={details.name} class="size-48 rounded-2xl" />
<div class="flex flex-col justify-center h-48 ml-8">
<div class="text-3xl text-neutral-200">{details.name}</div>
<div class="flex items-center mt-2 text-lg text-neutral-400">
<span class="mr-2">{$t("plugin_details.by")}</span>
<img
src={"https://avatars.githubusercontent.com/" + details.repository.split("/")[3]}
alt="Author avatar"
class="size-7 mr-1.5 rounded-full"
/>
<img src={"https://avatars.githubusercontent.com/" + details.repository.split("/")[3]} alt="Author avatar" class="size-7 mr-1.5 rounded-full" />
<a
target="_blank"
href={"https://github.com/" + details.repository.split("/")[3]}
+32 -16
View File
@@ -30,10 +30,21 @@
}, 1e3);
async function installPlugin(name: string, url: string | null, file: string | null, fallback_id: string | null) {
if (!file && !await ask($t("plugin_manager.install.prompt"), { title: $t("plugin_manager.install.title", { name }), okLabel: $t("dialog.yes"), cancelLabel: $t("dialog.no") })) return;
if (
!file &&
!(await ask($t("plugin_manager.install.prompt"), {
title: $t("plugin_manager.install.title", { name }),
okLabel: $t("dialog.yes"),
cancelLabel: $t("dialog.no"),
}))
)
return;
try {
await invoke("install_plugin", { url, file, fallback_id });
message($t("plugin_manager.install.success", { name }), { title: $t("plugin_manager.install.success.title", { name }), buttons: { ok: $t("dialog.ok") } });
message($t("plugin_manager.install.success", { name }), {
title: $t("plugin_manager.install.success.title", { name }),
buttons: { ok: $t("dialog.ok") },
});
$actionList?.reload();
installed = await invoke("list_plugins");
} catch (error: any) {
@@ -121,15 +132,19 @@
async function removePlugin(plugin: any) {
if (
!await ask($t("plugin_manager.remove.prompt", { name: plugin.name }), {
!(await ask($t("plugin_manager.remove.prompt", { name: plugin.name }), {
title: $t("plugin_manager.remove.title", { name: plugin.name }),
okLabel: $t("dialog.yes"),
cancelLabel: $t("dialog.no"),
})
) return;
}))
)
return;
try {
await invoke("remove_plugin", { id: plugin.id });
message($t("plugin_manager.remove.success", { name: plugin.name }), { title: $t("plugin_manager.remove.success.title", { name: plugin.name }), buttons: { ok: $t("dialog.ok") } });
message($t("plugin_manager.remove.success", { name: plugin.name }), {
title: $t("plugin_manager.remove.success.title", { name: plugin.name }),
buttons: { ok: $t("dialog.ok") },
});
$actionList?.reload();
$deviceSelector?.reloadProfiles();
installed = await invoke("list_plugins");
@@ -165,10 +180,10 @@
}
let installed: any[] = [];
(async () => installed = await invoke("list_plugins"))();
(async () => (installed = await invoke("list_plugins")))();
let plugins: { [id: string]: GitHubPlugin };
(async () => plugins = await (await fetch("https://openactionapi.github.io/plugins/catalogue.json")).json())();
(async () => (plugins = await (await fetch("https://openactionapi.github.io/plugins/catalogue.json")).json()))();
let showArchive: boolean = false;
let archivePlugins: any[] | null = null;
@@ -179,8 +194,7 @@
for (const plugin of installed) {
if (!checkedPlugins.has(plugin.id)) {
checkedPlugins.add(plugin.id);
isUpdateAvailable(plugin)
.then((version) => availableUpdates = { ...availableUpdates, [plugin.id]: version });
isUpdateAvailable(plugin).then((version) => (availableUpdates = { ...availableUpdates, [plugin.id]: version }));
}
}
}
@@ -207,7 +221,7 @@
<button
class="px-3 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => showPopup = true}
on:click={() => (showPopup = true)}
>
{$t("plugin_manager.button")}
</button>
@@ -223,12 +237,12 @@
/>
<Popup show={showPopup} label={$t("plugin_manager.title")}>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label={$t("settings.close")}>✕</button>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => (showPopup = false)} aria-label={$t("settings.close")}>✕</button>
<h2 class="m-2 font-semibold text-xl text-neutral-300">{$t("plugin_manager.title")}</h2>
<h2 class="mx-2 mt-6 mb-2 text-lg text-neutral-400">{$t("plugin_manager.installed")}</h2>
<div class="grid grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<!-- deno-fmt-ignore -->
<!-- prettier-ignore -->
{#each installed.sort((a, b) =>
(a.builtin && !b.builtin) ? -1 :
(b.builtin && !a.builtin) ? 1 :
@@ -324,7 +338,7 @@
name={plugin.name}
subtitle={plugin.author}
hidden={!plugin.name.toLowerCase().includes(query.toLowerCase())}
action={() => openDetailsView = id}
action={() => (openDetailsView = id)}
actionLabel={$t("plugin_manager.view_details")}
>
<ArrowSquareOut size="24" class="text-neutral-400" />
@@ -406,12 +420,14 @@
// @ts-expect-error
installPluginGitHub(openDetailsView, plugins[openDetailsView]);
}}
close={() => openDetailsView = null}
close={() => (openDetailsView = null)}
/>
{/if}
{#if choices}
<div class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 mt-2 p-2 w-96 text-xs text-neutral-300 bg-neutral-700 border border-neutral-600 rounded-lg z-40">
<div
class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 mt-2 p-2 w-96 text-xs text-neutral-300 bg-neutral-700 border border-neutral-600 rounded-lg z-40"
>
<h3 class="mb-2 font-semibold text-lg text-center">{$t("plugin_manager.choose_asset")}</h3>
<div class="select-wrapper">
<select class="w-full bg-neutral-800!" bind:value={choice} aria-label={$t("plugin_manager.choose_asset.label")}>
+13 -12
View File
@@ -161,7 +161,7 @@
applications = await invoke("get_applications");
applicationProfiles = await invoke("get_application_profiles");
})();
listen("applications", ({ payload }: { payload: string[] }) => applications = payload);
listen("applications", ({ payload }: { payload: string[] }) => (applications = payload));
let applicationsAddAppName: string = "opendeck_select_application";
let applicationsAddProfile: string = "opendeck_select_profile";
$: {
@@ -174,7 +174,9 @@
}
$: {
if (applicationProfiles) {
applicationProfiles = Object.fromEntries(Object.entries(applicationProfiles).filter(([_, devices]) => Object.values(devices).filter((v) => v).length != 0));
applicationProfiles = Object.fromEntries(
Object.entries(applicationProfiles).filter(([_, devices]) => Object.values(devices).filter((v) => v).length != 0),
);
invoke("set_application_profiles", { value: applicationProfiles });
}
}
@@ -218,7 +220,7 @@
/>
<Popup show={showPopup} label="{device.name} {$t('profile_manager.profiles')}">
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label={$t("settings.close")}>✕</button>
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => (showPopup = false)} aria-label={$t("settings.close")}>✕</button>
<h2 class="text-xl font-semibold text-neutral-300">{device.name}</h2>
<div class="flex flex-row mt-2 mb-1">
@@ -245,7 +247,7 @@
<button
class="ml-2 px-4 flex items-center text-neutral-300 bg-neutral-900 hover:bg-neutral-800 transition-colors border border-neutral-600 rounded-lg"
on:click={() => showApplicationManager = true}
on:click={() => (showApplicationManager = true)}
aria-label={$t("profile_manager.application_profiles")}
>
<Browsers size={24} />
@@ -268,6 +270,7 @@
aria-label={id ? profile.split("/")[1] : profile}
/>
{#if profile == renamingProfile}
<!-- prettier-ignore -->
<input
bind:this={renameInput}
bind:value={newId}
@@ -287,11 +290,7 @@
<Copy size="20" class="text-neutral-400" />
</button>
{#if profile != value}
<button
on:click={() => renamingProfile = newId = profile}
title={$t("profile_manager.rename")}
aria-label={$t("profile_manager.rename")}
>
<button on:click={() => (renamingProfile = newId = profile)} title={$t("profile_manager.rename")} aria-label={$t("profile_manager.rename")}>
<Pencil size="20" class="text-neutral-400" />
</button>
<button on:click={() => deleteProfile(profile)} title={$t("profile_manager.delete")} aria-label={$t("profile_manager.delete")}>
@@ -306,13 +305,13 @@
</Popup>
<Popup show={showApplicationManager} label={$t("profile_manager.application_profiles")}>
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => showApplicationManager = false} aria-label={$t("settings.close")}>✕</button>
<button class="mr-1 float-right text-xl text-neutral-300" on:click={() => (showApplicationManager = false)} aria-label={$t("settings.close")}>✕</button>
<h2 class="text-xl font-semibold text-neutral-300">{device.name}</h2>
<span class="text-sm text-neutral-400">{$t("profile_manager.application_profiles.hint.1")}</span>
<span class="text-sm text-neutral-400">{$t("profile_manager.application_profiles.hint.2")}</span>
<table class="w-full text-neutral-300 divide-y divide-neutral-500!">
{#each Object.entries(applicationProfiles).sort((a, b) => a[0] == "opendeck_default" ? -1 : b[0] == "opendeck_default" ? 1 : a[0].localeCompare(b[0])) as [appName, devices]}
{#each Object.entries(applicationProfiles).sort( (a, b) => (a[0] == "opendeck_default" ? -1 : b[0] == "opendeck_default" ? 1 : a[0].localeCompare(b[0])), ) as [appName, devices]}
{#if devices[device.id]}
<tr class="h-12">
<td>{appName == "opendeck_default" ? $t("profile_manager.default_profile") : appName}:</td>
@@ -320,7 +319,9 @@
<select
bind:value={applicationProfiles[appName][device.id]}
class="w-full"
aria-label={$t("profile_manager.application_profiles.aria", { name: appName == "opendeck_default" ? $t("profile_manager.default_profile") : appName })}
aria-label={$t("profile_manager.application_profiles.aria", {
name: appName == "opendeck_default" ? $t("profile_manager.default_profile") : appName,
})}
>
{#each Object.entries(folders) as [id, profiles]}
{#if id && profiles.length}
+58 -49
View File
@@ -33,27 +33,30 @@
if (instance == null || !iframe.src || !iframe.src.startsWith(getWebserverUrl())) return;
const info = JSON.stringify(await invoke("make_info", { plugin: instance.action.plugin }));
iframe?.contentWindow?.postMessage({
event: "connect",
payload: [
getWebSocketPort(),
instance.context,
"registerPropertyInspector",
info,
JSON.stringify({
action: instance.action.uuid,
context: instance.context,
device: split[0],
payload: {
settings: instance.settings,
coordinates,
controller: split[2],
state: instance.current_state,
isInMultiAction: parseInt(split[4]) != 0,
},
}),
],
}, getWebserverUrl());
iframe?.contentWindow?.postMessage(
{
event: "connect",
payload: [
getWebSocketPort(),
instance.context,
"registerPropertyInspector",
info,
JSON.stringify({
action: instance.action.uuid,
context: instance.context,
device: split[0],
payload: {
settings: instance.settings,
coordinates,
controller: split[2],
state: instance.current_state,
isInMultiAction: parseInt(split[4]) != 0,
},
}),
],
},
getWebserverUrl(),
);
}
const closePopup = (context: string) => {
@@ -121,40 +124,46 @@
}
// @ts-expect-error
window.fetchCORS(...data.payload.args).then(async (response: Response) => {
const chunks = [];
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
window
.fetchCORS(...data.payload.args)
.then(async (response: Response) => {
const chunks = [];
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
}
}
const body = combineUint8Arrays(chunks);
const body = combineUint8Arrays(chunks);
iframes[data.payload.context]?.contentWindow?.postMessage({
event: "fetchResponse",
payload: {
id: data.payload.id,
response: {
url: response.url,
body,
headers: response.headers.entries().toArray(),
status: response.status,
statusText: response.statusText,
iframes[data.payload.context]?.contentWindow?.postMessage(
{
event: "fetchResponse",
payload: {
id: data.payload.id,
response: {
url: response.url,
body,
headers: response.headers.entries().toArray(),
status: response.status,
statusText: response.statusText,
},
},
},
},
}, getWebserverUrl());
}).catch((error: any) => {
iframes[data.payload.context]?.contentWindow?.postMessage({ event: "fetchError", payload: { id: data.payload.id, error } }, getWebserverUrl());
});
getWebserverUrl(),
);
})
.catch((error: any) => {
iframes[data.payload.context]?.contentWindow?.postMessage({ event: "fetchError", payload: { id: data.payload.id, error } }, getWebserverUrl());
});
}
});
const nonNull = <T>(o: T | null): o is T => o != null;
$: instances = profile
.keys.filter(nonNull)
const nonNull = <T,>(o: T | null): o is T => o != null;
$: instances = profile.keys
.filter(nonNull)
.reduce((prev, current) => prev.concat(current.children ? [current, ...current.children] : current), [] as ActionInstance[])
.concat(profile.sliders.filter(nonNull))
.concat(profile.infobars.filter(nonNull));
+15 -18
View File
@@ -18,7 +18,7 @@
let showPopup: boolean;
let buildInfo: string;
(async () => buildInfo = await invoke("get_build_info"))();
(async () => (buildInfo = await invoke("get_build_info")))();
listen("device_brightness", ({ payload }: { payload: { action: string; value: number } }) => {
if (!$settings) return;
@@ -38,30 +38,21 @@
});
async function backupConfig() {
await message(
$t("settings.backup_config.prompt"),
{ title: $t("settings.backup_config.title"), buttons: { ok: $t("dialog.ok") } },
);
await message($t("settings.backup_config.prompt"), { title: $t("settings.backup_config.title"), buttons: { ok: $t("dialog.ok") } });
if (await invoke("backup_config_directory")) {
await message(
$t("settings.backup_config.success.prompt"),
{ title: $t("settings.backup_config.success.title"), buttons: { ok: $t("dialog.ok") } },
);
await message($t("settings.backup_config.success.prompt"), { title: $t("settings.backup_config.success.title"), buttons: { ok: $t("dialog.ok") } });
}
}
async function restoreConfig() {
await message(
$t("settings.restore_config.prompt"),
{ title: $t("settings.restore_config.title"), buttons: { ok: $t("dialog.ok") } },
);
await message($t("settings.restore_config.prompt"), { title: $t("settings.restore_config.title"), buttons: { ok: $t("dialog.ok") } });
await invoke("restore_config_directory");
}
</script>
<button
class="px-3 py-1 text-sm text-neutral-300 bg-neutral-700 hover:bg-neutral-600 transition-colors border border-neutral-600 rounded-lg"
on:click={() => showPopup = true}
on:click={() => (showPopup = true)}
>
{$t("settings.button")}
</button>
@@ -73,7 +64,7 @@
/>
<Popup show={showPopup} label={$t("settings.button")}>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => showPopup = false} aria-label={$t("settings.close")}>✕</button>
<button class="mr-2 my-1 float-right text-xl text-neutral-300" on:click={() => (showPopup = false)} aria-label={$t("settings.close")}>✕</button>
<h2 class="m-2 font-semibold text-xl text-neutral-300">{$t("settings.button")}</h2>
{#if $settings}
<div class="flex flex-row items-center m-2 space-x-2">
@@ -101,15 +92,21 @@
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-sleep_timeout_minutes" class="text-neutral-400">{$t("settings.sleep_timeout_minutes")}</label>
<input type="number" min="0" bind:value={$settings.sleep_timeout_minutes} class="w-12 px-1 text-neutral-300 border border-neutral-600 rounded-lg" id="settings-sleep_timeout_minutes" />
<input
type="number"
min="0"
bind:value={$settings.sleep_timeout_minutes}
class="w-12 px-1 text-neutral-300 border border-neutral-600 rounded-lg"
id="settings-sleep_timeout_minutes"
/>
<span class="text-neutral-400">{$t("settings.sleep_timeout_minutes.minutes")}</span>
<Tooltip> {$t("settings.sleep_timeout_minutes.tooltip")} </Tooltip>
<Tooltip>{$t("settings.sleep_timeout_minutes.tooltip")}</Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
<label for="settings-sleep_when_computer_locked" class="text-neutral-400">{$t("settings.sleep_when_computer_locked")}</label>
<input type="checkbox" bind:checked={$settings.sleep_when_computer_locked} id="settings-sleep_when_computer_locked" />
<Tooltip> {$t("settings.sleep_when_computer_locked.tooltip")} </Tooltip>
<Tooltip>{$t("settings.sleep_when_computer_locked.tooltip")}</Tooltip>
</div>
<div class="flex flex-row items-center m-2 space-x-2">
+1 -5
View File
@@ -5,11 +5,7 @@
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="inline-block cursor-help"
on:mouseenter={() => show = true}
on:mouseleave={() => show = false}
>
<div class="inline-block cursor-help" on:mouseenter={() => (show = true)} on:mouseleave={() => (show = false)}>
<Question size="24" class="text-neutral-500" />
{#if show}
<div class="absolute mt-2 p-2 w-96 text-xs text-neutral-300 bg-neutral-700 border border-neutral-600 rounded-lg">
+6 -1
View File
@@ -40,4 +40,9 @@ function translate(locale: string, key: string, vars: { [x: string]: any }) {
return text;
}
export const t = derived(locale, ($locale) => (key: string, vars = {}) => translate($locale, key, vars));
export const t = derived(
locale,
($locale) =>
(key: string, vars = {}) =>
translate($locale, key, vars),
);
+1 -3
View File
@@ -24,7 +24,5 @@ document.addEventListener("keydown", (event) => {
});
globalThis.addEventListener("blur", () => openContextMenu.set(null));
export type CopiedItem =
| { type: "instance"; source: Context }
| { type: "action"; action: Action };
export type CopiedItem = { type: "instance"; source: Context } | { type: "action"; action: Action };
export const copiedItem: Writable<CopiedItem | null> = writable(null);
+16 -20
View File
@@ -31,7 +31,7 @@ export class CanvasLock {
currentLock = Promise.resolve();
async lock() {
let unlockNext: () => void;
const willLock = new Promise<void>((resolve) => unlockNext = resolve);
const willLock = new Promise<void>((resolve) => (unlockNext = resolve));
const previousLock = this.currentLock;
this.currentLock = willLock;
await previousLock;
@@ -68,7 +68,7 @@ export async function renderImage(
context.save();
if (rotation) {
context.translate(canvas.width / 2, canvas.height / 2);
context.rotate(rotation * Math.PI / 180);
context.rotate((rotation * Math.PI) / 180);
context.translate(-canvas.width / 2, -canvas.height / 2);
}
@@ -109,33 +109,33 @@ export async function renderImage(
if (state.show) {
const size = state.size * 2 * scale;
context.textAlign = "center";
context.font = (state.style.includes("Bold") ? "bold " : "") + (state.style.includes("Italic") ? "italic " : "") +
`${size}px "${state.family}", sans-serif`;
context.font =
(state.style.includes("Bold") ? "bold " : "") + (state.style.includes("Italic") ? "italic " : "") + `${size}px "${state.family}", sans-serif`;
context.fillStyle = state.colour;
context.strokeStyle = state.stroke_colour;
context.lineWidth = state.stroke_size * scale;
context.textBaseline = "top";
const x = canvas.width / 2;
let y = canvas.height / 2 - (size * state.text.split("\n").length * 0.5);
let y = canvas.height / 2 - size * state.text.split("\n").length * 0.5;
switch (state.alignment) {
case "top":
y = context.lineWidth;
break;
case "bottom":
y = canvas.height - (size * state.text.split("\n").length) - context.lineWidth;
y = canvas.height - size * state.text.split("\n").length - context.lineWidth;
break;
}
for (const [index, line] of Object.entries(state.text.split("\n"))) {
context.strokeText(line, x, y + (size * parseInt(index)));
context.fillText(line, x, y + (size * parseInt(index)));
context.strokeText(line, x, y + size * parseInt(index));
context.fillText(line, x, y + size * parseInt(index));
if (state.underline) {
const width = context.measureText(line).width;
// Set to black for the outline, since it uses the same fill style info as the text colour.
context.fillStyle = "black";
context.fillRect(x - (width / 2) - 3, y + (size * parseInt(index)) + size, width + 6, 9);
context.fillRect(x - width / 2 - 3, y + size * parseInt(index) + size, width + 6, 9);
// Reset to the user's choice of text colour.
context.fillStyle = state.colour;
context.fillRect(x - (width / 2), y + (size * parseInt(index)) + size + 4, width, 3);
context.fillRect(x - width / 2, y + size * parseInt(index) + size + 4, width, 3);
}
}
}
@@ -168,13 +168,7 @@ export async function renderImage(
const newContext = smallCanvas.getContext("2d");
const margin = 0.1;
if (newContext) {
newContext.drawImage(
canvas,
canvas.width * margin,
canvas.height * margin,
canvas.width * (1 - (margin * 2)),
canvas.height * (1 - (margin * 2)),
);
newContext.drawImage(canvas, canvas.width * margin, canvas.height * margin, canvas.width * (1 - margin * 2), canvas.height * (1 - margin * 2));
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(smallCanvas, 0, 0);
}
@@ -195,10 +189,12 @@ export async function resizeImage(source: string): Promise<string | undefined> {
const image = document.createElement("img");
image.crossOrigin = "anonymous";
image.src = source;
await new Promise((resolve) => image.onload = resolve);
await new Promise((resolve) => (image.onload = resolve));
let xOffset = 0, yOffset = 0;
let xScaled = canvas.width, yScaled = canvas.height;
let xOffset = 0,
yOffset = 0;
let xScaled = canvas.width,
yScaled = canvas.height;
if (image.width > image.height) {
const ratio = image.height / image.width;
yScaled = canvas.height * ratio;
+2 -11
View File
@@ -29,19 +29,10 @@
<div class="flex flex-col grow min-w-0">
<nav class="flex flex-row justify-between items-center p-3" class:hidden={$inspectedParentAction}>
<div class="flex flex-col items-start space-y-1">
<DeviceSelector
bind:devices
bind:value={selectedDevice}
bind:selectedProfiles
bind:this={$deviceSelector}
/>
<DeviceSelector bind:devices bind:value={selectedDevice} bind:selectedProfiles bind:this={$deviceSelector} />
{#key selectedDevice}
{#if selectedDevice && devices[selectedDevice]}
<ProfileManager
bind:device={devices[selectedDevice]}
bind:profile={selectedProfiles[selectedDevice]}
bind:this={$profileManager}
/>
<ProfileManager bind:device={devices[selectedDevice]} bind:profile={selectedProfiles[selectedDevice]} bind:this={$profileManager} />
{/if}
{/key}
</div>