mirror of
https://github.com/nekename/OpenDeck.git
synced 2026-10-28 09:44:01 +00:00
chore: reformat codebase using Prettier due to frequent instability in the Deno formatter
This commit is contained in:
38 files changed
+330
-302
No files matched your search
@@ -0,0 +1 @@
|
||||
LICENSE.md
|
||||
Vendored
+3
-6
@@ -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,
|
||||
},
|
||||
}
|
||||
Vendored
+2
-10
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,9 +30,11 @@ 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).
|
||||
@@ -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.
|
||||
|
||||
@@ -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!
|
||||
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
+9
-5
@@ -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({
|
||||
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({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: "setSettings",
|
||||
context: inActionInfo.context,
|
||||
payload: {
|
||||
action: action.value,
|
||||
value: parseInt(value.value),
|
||||
},
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
+27
-11
@@ -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({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: inRegisterEvent,
|
||||
uuid: inPropertyInspectorUUID,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const down = document.getElementById("down");
|
||||
@@ -32,7 +34,8 @@
|
||||
}
|
||||
|
||||
update = () => {
|
||||
websocket.send(JSON.stringify({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: "setSettings",
|
||||
context: inActionInfo.context,
|
||||
payload: {
|
||||
@@ -41,7 +44,8 @@
|
||||
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>.
|
||||
|
||||
+9
-5
@@ -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({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: inRegisterEvent,
|
||||
uuid: inPropertyInspectorUUID,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const down = document.getElementById("down");
|
||||
@@ -30,7 +32,8 @@
|
||||
}
|
||||
|
||||
update = () => {
|
||||
websocket.send(JSON.stringify({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: "setSettings",
|
||||
context: inActionInfo.context,
|
||||
payload: {
|
||||
@@ -39,7 +42,8 @@
|
||||
anticlockwise: anticlockwise.value,
|
||||
clockwise: clockwise.value,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
+11
-6
@@ -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({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: inRegisterEvent,
|
||||
uuid: inPropertyInspectorUUID,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (inActionInfo.payload.controller == "Encoder") {
|
||||
@@ -32,7 +34,8 @@
|
||||
show.checked = inActionInfo.payload.settings.show ?? false;
|
||||
|
||||
update = () => {
|
||||
websocket.send(JSON.stringify({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: "setSettings",
|
||||
context: inActionInfo.context,
|
||||
payload: {
|
||||
@@ -42,7 +45,8 @@
|
||||
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;
|
||||
|
||||
+9
-5
@@ -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({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: inRegisterEvent,
|
||||
uuid: inPropertyInspectorUUID,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const device = document.getElementById("device");
|
||||
@@ -29,7 +31,8 @@
|
||||
}
|
||||
|
||||
update = () => {
|
||||
websocket.send(JSON.stringify({
|
||||
websocket.send(
|
||||
JSON.stringify({
|
||||
event: "setSettings",
|
||||
context: inActionInfo.context,
|
||||
payload: {
|
||||
@@ -38,7 +41,8 @@
|
||||
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);
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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();
|
||||
}}
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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")}>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
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({
|
||||
iframe?.contentWindow?.postMessage(
|
||||
{
|
||||
event: "connect",
|
||||
payload: [
|
||||
getWebSocketPort(),
|
||||
@@ -53,7 +54,9 @@
|
||||
},
|
||||
}),
|
||||
],
|
||||
}, getWebserverUrl());
|
||||
},
|
||||
getWebserverUrl(),
|
||||
);
|
||||
}
|
||||
|
||||
const closePopup = (context: string) => {
|
||||
@@ -121,7 +124,9 @@
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
window.fetchCORS(...data.payload.args).then(async (response: Response) => {
|
||||
window
|
||||
.fetchCORS(...data.payload.args)
|
||||
.then(async (response: Response) => {
|
||||
const chunks = [];
|
||||
if (response.body) {
|
||||
const reader = response.body.getReader();
|
||||
@@ -133,7 +138,8 @@
|
||||
}
|
||||
const body = combineUint8Arrays(chunks);
|
||||
|
||||
iframes[data.payload.context]?.contentWindow?.postMessage({
|
||||
iframes[data.payload.context]?.contentWindow?.postMessage(
|
||||
{
|
||||
event: "fetchResponse",
|
||||
payload: {
|
||||
id: data.payload.id,
|
||||
@@ -145,16 +151,19 @@
|
||||
statusText: response.statusText,
|
||||
},
|
||||
},
|
||||
}, getWebserverUrl());
|
||||
}).catch((error: any) => {
|
||||
},
|
||||
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));
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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),
|
||||
);
|
||||
@@ -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
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user