aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rwxr-xr-xdeck-it9
-rw-r--r--deckit/modules/obs.py9
-rw-r--r--static/deck.css2
-rw-r--r--static/modules/obs.js35
-rw-r--r--tests/test_obs.py4
5 files changed, 48 insertions, 11 deletions
diff --git a/deck-it b/deck-it
index 56927e5..2e603ef 100755
--- a/deck-it
+++ b/deck-it
@@ -24,7 +24,7 @@ env_out=$(cd "$DIR" && python3 -m deckit env 2>&1) && eval "$env_out" || echo "C
teardown() {
pkill -f "^bash $SELF watch" 2>/dev/null
pkill -x wayvnc
- pkill -f '^python3 -m deckit' 2>/dev/null
+ pkill -f '[p]ython3 -m deckit' 2>/dev/null
pkill -f "^[^ ]*swaybg -o $MON( |$)" 2>/dev/null
hyprctl output remove "$MON" >/dev/null 2>&1
local s
@@ -176,16 +176,17 @@ mkdir -p -m 700 "$RUN"
vnc_setup
# wayvnc leaves its socket behind when killed and then refuses to bind, so clear it first.
pgrep -x wayvnc >/dev/null || { rm -f "$RUN/vnc.sock"; setsid wayvnc -C "$VNC/config" -u -o "$MON" "$RUN/vnc.sock" >/dev/null 2>&1 & }
-pgrep -f '^python3 -m deckit' >/dev/null || (cd "$DIR" && setsid python3 -m deckit >server.log 2>&1 &)
+pgrep -f '[p]ython3 -m deckit' >/dev/null || (cd "$DIR" && setsid python3 -m deckit >server.log 2>&1 &)
sleep 1
tunnel "$ANDROID_SERIAL" || { echo "Could not set up the tunnel to the tablet"; exit 1; }
# Opening the page with the secret stores it as a cookie in the tablet's browser, which unlocks the API.
token="$CONF/token"
for _ in {1..10}; do [[ -s "$token" ]] && break; sleep 0.5; done
-adb shell am start -a android.intent.action.VIEW -d "http://127.0.0.1:$PORT/?t=$(cat "$token")" >/dev/null 2>&1
-sleep 1
open_avnc "$ANDROID_SERIAL"
+sleep 1
+# The dashboard last, so it is the foreground app; AVNC is one switch away.
+adb shell am start -a android.intent.action.VIEW -d "http://127.0.0.1:$PORT/?t=$(cat "$token")" >/dev/null 2>&1
if [[ "${1:-}" == wifi ]]; then
echo "$ANDROID_SERIAL" > "$RUN/serial"
diff --git a/deckit/modules/obs.py b/deckit/modules/obs.py
index 04c49ef..07ec5d9 100644
--- a/deckit/modules/obs.py
+++ b/deckit/modules/obs.py
@@ -125,10 +125,17 @@ def make(cfg: dict) -> Module:
"sceneItemId": int(body.get("id")),
"sceneItemEnabled": bool(body.get("on"))})
+ def properties(body):
+ name = str(body.get("source") or "")
+ if not name:
+ raise ModuleError("source required")
+ req("OpenInputPropertiesDialog", {"inputName": name})
+
return Module(
name="obs", title="OBS", state=state,
actions={"scene": scene, "record": record, "stream": stream, "mute": mute,
- "volume": volume, "transition": transition, "trigger": trigger, "source": source},
+ "volume": volume, "transition": transition, "trigger": trigger,
+ "source": source, "properties": properties},
start=lambda: threading.Thread(target=connect_loop, daemon=True).start(),
stop=lambda: (stop.set(), client.close()),
)
diff --git a/static/deck.css b/static/deck.css
index ee2be1f..6f3fa97 100644
--- a/static/deck.css
+++ b/static/deck.css
@@ -379,7 +379,9 @@ select {
border: 1px solid var(--udt-surface2);
color: var(--udt-subtext0);
}
+.tile { position: relative; }
.tile .ico { width: 22px; height: 22px; flex: none; }
+.tile .gear { position: absolute; right: 6px; top: 4px; font-style: normal; font-size: 11px; opacity: .6; }
.tile span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tile.on {
background: var(--udt-accent);
diff --git a/static/modules/obs.js b/static/modules/obs.js
index 9430ee5..ae9e31b 100644
--- a/static/modules/obs.js
+++ b/static/modules/obs.js
@@ -52,14 +52,34 @@ function renderTransport(box, s) {
box.append(rec, stream, trigger);
}
-function renderTiles(box, items, onPick) {
+function renderTiles(box, items, onPick, onLong) {
box.textContent = "";
for (const item of items) {
const b = document.createElement("button");
b.className = `tile${item.on ? " on" : ""}`;
- b.innerHTML = `${item.icon ? icon(item.icon) : ""}<span></span>`;
+ b.innerHTML = `${item.icon ? icon(item.icon) : ""}<span></span>${onLong ? '<i class="gear">⚙</i>' : ""}`;
b.querySelector("span").textContent = item.label;
- b.onclick = () => onPick(item);
+ let timer = null;
+ let longFired = false;
+ if (onLong) {
+ const host = () => b.closest(".tab-content");
+ b.addEventListener("pointerdown", () => {
+ if (host()) host()._pressing = true;
+ longFired = false;
+ timer = setTimeout(() => { longFired = true; onLong(item); }, 600);
+ });
+ const cancel = () => {
+ if (timer) { clearTimeout(timer); timer = null; }
+ if (host()) host()._pressing = false;
+ };
+ b.addEventListener("pointerup", cancel);
+ b.addEventListener("pointercancel", cancel);
+ b.addEventListener("pointerleave", cancel);
+ }
+ b.onclick = () => {
+ if (longFired) { longFired = false; return; }
+ onPick(item);
+ };
box.append(b);
}
}
@@ -129,9 +149,12 @@ export default {
renderTiles(el.querySelector(".scenes"),
(state.scenes ?? []).map((name) => ({ label: name, icon: "scene", on: name === state.currentScene })),
(item) => post("scene", { scene: item.label }));
- renderTiles(el.querySelector(".sources"),
- (state.sources ?? []).map((src) => ({ label: src.name, icon: src.on ? "eye" : "eyeOff", on: src.on, id: src.id })),
- (item) => post("source", { scene: state.currentScene, id: item.id, on: !item.on }));
+ if (!el._pressing) {
+ renderTiles(el.querySelector(".sources"),
+ (state.sources ?? []).map((src) => ({ label: src.name, icon: src.on ? "eye" : "eyeOff", on: src.on, id: src.id })),
+ (item) => post("source", { scene: state.currentScene, id: item.id, on: !item.on }),
+ (item) => post("properties", { source: item.label }));
+ }
renderTiles(el.querySelector(".transitions"),
(state.transitions ?? []).map((name) => ({ label: name, on: name === state.transition })),
(item) => post("transition", { name: item.label }));
diff --git a/tests/test_obs.py b/tests/test_obs.py
index cd317ed..ecb7cc0 100644
--- a/tests/test_obs.py
+++ b/tests/test_obs.py
@@ -39,6 +39,10 @@ class Actions(unittest.TestCase):
self.m.actions["source"]({"scene": "S", "id": 2, "on": False})
self.assertEqual(Fake.last[0], "SetSceneItemEnabled")
+ def test_properties_opens_the_source_dialog(self):
+ self.m.actions["properties"]({"source": "Single Window"})
+ self.assertEqual(Fake.last, ("OpenInputPropertiesDialog", {"inputName": "Single Window"}))
+
def test_state_offline(self):
class Off:
connected = False