mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 17:07:08 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 97274243b8 | |||
| d4f9046f3e | |||
| 78d12cf49f | |||
| bbaf08d6b8 | |||
| 9d2310dace | |||
| ceba7b9e56 | |||
| 01e2441943 | |||
| 3297327321 | |||
| c042ce5ef6 | |||
| 6c042c8129 | |||
| 9f8bf01aba | |||
| d355891517 | |||
| 80271dc8ae | |||
| edc9d312bc | |||
| 74470c3bb5 | |||
| 012934362a | |||
| 8bbacc6ce5 | |||
| 4ebac7d7fc | |||
| fa3d5198d2 |
+24
-13
@@ -2,16 +2,23 @@ name: Build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
paths-ignore:
|
||||||
- go.mod
|
- "**/*.md"
|
||||||
- go.sum
|
- "**/*.yml"
|
||||||
- "**/*.go"
|
|
||||||
- "**/*.tmpl"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
block:
|
||||||
|
- control
|
||||||
|
- display
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ matrix.block }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Repository
|
- name: Checkout Repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -20,21 +27,25 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: "^1.24"
|
go-version: "^1.24"
|
||||||
cache-dependency-path: display/go.sum
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
|
- name: Setup Deno
|
||||||
|
uses: denoland/setup-deno@v2
|
||||||
|
if: matrix.block == 'control'
|
||||||
|
with:
|
||||||
|
deno-version: v2.x
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
working-directory: display
|
|
||||||
run: go mod download
|
run: go mod download
|
||||||
|
|
||||||
- name: Generate Templates
|
- name: Generate
|
||||||
working-directory: display
|
run: go generate ./...
|
||||||
run: go tool templ generate
|
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
working-directory: display
|
run: go build -o plg-mudics-${{ matrix.block }} .
|
||||||
run: go build -o plg-mudics-display .
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
path: display/plg-mudics-display
|
name: plg-mudics-${{ matrix.block }}
|
||||||
|
path: ${{matrix.block}}/plg-mudics-${{ matrix.block }}
|
||||||
|
|||||||
@@ -21,3 +21,47 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: PLG-MuDiCS ${{ github.ref_name }}
|
name: PLG-MuDiCS ${{ github.ref_name }}
|
||||||
body_path: CHANGELOG.md
|
body_path: CHANGELOG.md
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
block:
|
||||||
|
- control
|
||||||
|
- display
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ${{ matrix.block }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: "^1.24"
|
||||||
|
cache-dependency-path: go.sum
|
||||||
|
|
||||||
|
- name: Setup Deno
|
||||||
|
uses: denoland/setup-deno@v2
|
||||||
|
if: matrix.block == 'control'
|
||||||
|
with:
|
||||||
|
deno-version: v2.x
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: go mod download
|
||||||
|
|
||||||
|
- name: Generate
|
||||||
|
run: go generate ./...
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: go build -o plg-mudics-${{ matrix.block }} .
|
||||||
|
|
||||||
|
- name: Upload Release Assets
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
file: plg-mudics-${{ matrix.block }}
|
||||||
|
tag: ${{ github.ref_name }}
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
# PLG-MuDICS Control
|
# PLG-MuDiCS Control
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|||||||
Generated
+42
@@ -9,13 +9,16 @@
|
|||||||
"npm:@sveltejs/vite-plugin-svelte@6": "6.1.0_svelte@5.37.1__acorn@8.15.0_vite@7.0.6__picomatch@4.0.3",
|
"npm:@sveltejs/vite-plugin-svelte@6": "6.1.0_svelte@5.37.1__acorn@8.15.0_vite@7.0.6__picomatch@4.0.3",
|
||||||
"npm:@tailwindcss/typography@~0.5.15": "0.5.16_tailwindcss@4.1.11",
|
"npm:@tailwindcss/typography@~0.5.15": "0.5.16_tailwindcss@4.1.11",
|
||||||
"npm:@tailwindcss/vite@4": "4.1.11_vite@7.0.6__picomatch@4.0.3",
|
"npm:@tailwindcss/vite@4": "4.1.11_vite@7.0.6__picomatch@4.0.3",
|
||||||
|
"npm:@thisux/sveltednd@^0.0.20": "0.0.20_svelte@5.37.1__acorn@8.15.0",
|
||||||
"npm:eslint-config-prettier@^10.0.1": "10.1.8_eslint@9.33.0",
|
"npm:eslint-config-prettier@^10.0.1": "10.1.8_eslint@9.33.0",
|
||||||
"npm:eslint-plugin-svelte@3": "3.11.0_eslint@9.33.0_svelte@5.37.1__acorn@8.15.0_postcss@8.5.6",
|
"npm:eslint-plugin-svelte@3": "3.11.0_eslint@9.33.0_svelte@5.37.1__acorn@8.15.0_postcss@8.5.6",
|
||||||
"npm:eslint@^9.18.0": "9.33.0",
|
"npm:eslint@^9.18.0": "9.33.0",
|
||||||
"npm:globals@16": "16.3.0",
|
"npm:globals@16": "16.3.0",
|
||||||
|
"npm:lucide-svelte@0.545": "0.545.0_svelte@5.37.1__acorn@8.15.0",
|
||||||
"npm:prettier-plugin-svelte@^3.3.3": "3.4.0_prettier@3.6.2_svelte@5.37.1__acorn@8.15.0",
|
"npm:prettier-plugin-svelte@^3.3.3": "3.4.0_prettier@3.6.2_svelte@5.37.1__acorn@8.15.0",
|
||||||
"npm:prettier@^3.4.2": "3.6.2",
|
"npm:prettier@^3.4.2": "3.6.2",
|
||||||
"npm:svelte-check@4": "4.3.0_svelte@5.37.1__acorn@8.15.0_typescript@5.8.3",
|
"npm:svelte-check@4": "4.3.0_svelte@5.37.1__acorn@8.15.0_typescript@5.8.3",
|
||||||
|
"npm:svelte-dnd-action@~0.9.65": "0.9.65_svelte@5.37.1__acorn@8.15.0",
|
||||||
"npm:svelte@5": "5.37.1_acorn@8.15.0",
|
"npm:svelte@5": "5.37.1_acorn@8.15.0",
|
||||||
"npm:tailwindcss@4": "4.1.11",
|
"npm:tailwindcss@4": "4.1.11",
|
||||||
"npm:typescript-eslint@^8.20.0": "8.40.0_eslint@9.33.0_typescript@5.8.3_@typescript-eslint+parser@8.40.0__eslint@9.33.0__typescript@5.8.3",
|
"npm:typescript-eslint@^8.20.0": "8.40.0_eslint@9.33.0_typescript@5.8.3_@typescript-eslint+parser@8.40.0__eslint@9.33.0__typescript@5.8.3",
|
||||||
@@ -596,6 +599,30 @@
|
|||||||
"vite"
|
"vite"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"@thisux/sveltednd@0.0.14": {
|
||||||
|
"integrity": "sha512-Vbq69SU3HUomPg6oCXtb89OG89hka0YIkdaErYibn3waK7tYE66IcQxD/Fzg8YNW3EVsXoA9kc7kW5EUBCSQGg=="
|
||||||
|
},
|
||||||
|
"@thisux/sveltednd@0.0.17_svelte@5.37.1__acorn@8.15.0": {
|
||||||
|
"integrity": "sha512-lRninjw439phhA8xAHqCpMAX0hnwFMdbXW4M0XJgAdnGxeum+QsLiIC4P3HnkNXAygsVKUqxRcbS84CxDZ9hPw==",
|
||||||
|
"dependencies": [
|
||||||
|
"@thisux/sveltednd@0.0.14",
|
||||||
|
"svelte"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@thisux/sveltednd@0.0.18_svelte@5.37.1__acorn@8.15.0": {
|
||||||
|
"integrity": "sha512-MO+iR9ZRHApvtwgWujmqwUbhv9mKs0jLOmeG4tn98GmU1/9wVOi02jrNqeggKg3c8dXz6pMmUHfOfmcq7WwYZg==",
|
||||||
|
"dependencies": [
|
||||||
|
"@thisux/sveltednd@0.0.17_svelte@5.37.1__acorn@8.15.0",
|
||||||
|
"svelte"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@thisux/sveltednd@0.0.20_svelte@5.37.1__acorn@8.15.0": {
|
||||||
|
"integrity": "sha512-VE0HopIlHIvNOfSZ1SsiIzl1AmTU0VSd2Nz2Q2nIdqIUsT/UP4hiLw2ed2jFqTKTbqC+kbCYL9vuBpkYnEw+Kw==",
|
||||||
|
"dependencies": [
|
||||||
|
"@thisux/sveltednd@0.0.18_svelte@5.37.1__acorn@8.15.0",
|
||||||
|
"svelte"
|
||||||
|
]
|
||||||
|
},
|
||||||
"@tybys/wasm-util@0.10.0": {
|
"@tybys/wasm-util@0.10.0": {
|
||||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -1259,6 +1286,12 @@
|
|||||||
"lodash.merge@4.6.2": {
|
"lodash.merge@4.6.2": {
|
||||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
|
||||||
},
|
},
|
||||||
|
"lucide-svelte@0.545.0_svelte@5.37.1__acorn@8.15.0": {
|
||||||
|
"integrity": "sha512-lVza6hIAf1abADTU1ThyNp+M3bQZtzTGQ9EQZ2xo3n9ftkd4QcWbDB2ekP2MqRLTJpeLXX5gaFEgFZtbWCnyqg==",
|
||||||
|
"dependencies": [
|
||||||
|
"svelte"
|
||||||
|
]
|
||||||
|
},
|
||||||
"magic-string@0.30.17": {
|
"magic-string@0.30.17": {
|
||||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -1525,6 +1558,12 @@
|
|||||||
],
|
],
|
||||||
"bin": true
|
"bin": true
|
||||||
},
|
},
|
||||||
|
"svelte-dnd-action@0.9.65_svelte@5.37.1__acorn@8.15.0": {
|
||||||
|
"integrity": "sha512-GKFtrAtYAjcm27aMELoXOhkLtKA1AEoj2njjCReCer6jh1hnRtTHdEO4Kjfpayz+ZAvE0MMwIvLISW3tsiO9Qg==",
|
||||||
|
"dependencies": [
|
||||||
|
"svelte"
|
||||||
|
]
|
||||||
|
},
|
||||||
"svelte-eslint-parser@1.3.1_svelte@5.37.1__acorn@8.15.0_postcss@8.5.6": {
|
"svelte-eslint-parser@1.3.1_svelte@5.37.1__acorn@8.15.0_postcss@8.5.6": {
|
||||||
"integrity": "sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==",
|
"integrity": "sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -1689,13 +1728,16 @@
|
|||||||
"npm:@sveltejs/vite-plugin-svelte@6",
|
"npm:@sveltejs/vite-plugin-svelte@6",
|
||||||
"npm:@tailwindcss/typography@~0.5.15",
|
"npm:@tailwindcss/typography@~0.5.15",
|
||||||
"npm:@tailwindcss/vite@4",
|
"npm:@tailwindcss/vite@4",
|
||||||
|
"npm:@thisux/sveltednd@^0.0.20",
|
||||||
"npm:eslint-config-prettier@^10.0.1",
|
"npm:eslint-config-prettier@^10.0.1",
|
||||||
"npm:eslint-plugin-svelte@3",
|
"npm:eslint-plugin-svelte@3",
|
||||||
"npm:eslint@^9.18.0",
|
"npm:eslint@^9.18.0",
|
||||||
"npm:globals@16",
|
"npm:globals@16",
|
||||||
|
"npm:lucide-svelte@0.545",
|
||||||
"npm:prettier-plugin-svelte@^3.3.3",
|
"npm:prettier-plugin-svelte@^3.3.3",
|
||||||
"npm:prettier@^3.4.2",
|
"npm:prettier@^3.4.2",
|
||||||
"npm:svelte-check@4",
|
"npm:svelte-check@4",
|
||||||
|
"npm:svelte-dnd-action@~0.9.65",
|
||||||
"npm:svelte@5",
|
"npm:svelte@5",
|
||||||
"npm:tailwindcss@4",
|
"npm:tailwindcss@4",
|
||||||
"npm:typescript-eslint@^8.20.0",
|
"npm:typescript-eslint@^8.20.0",
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:generate deno install
|
||||||
//go:generate deno task build
|
//go:generate deno task build
|
||||||
|
|
||||||
//go:embed all:build
|
//go:embed all:build
|
||||||
var buildDir embed.FS
|
var buildDir embed.FS
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ArrowBigLeft, ArrowBigRight, ChevronDown, Keyboard, Power, PowerOff, Presentation, SquareTerminal, TextAlignStart, TrafficCone } from "lucide-svelte";
|
import {
|
||||||
import { selected_display_ids } from "../ts/stores/select";
|
ArrowBigLeft,
|
||||||
import Button from "./Button.svelte";
|
ArrowBigRight,
|
||||||
|
ChevronDown,
|
||||||
|
Keyboard,
|
||||||
|
Power,
|
||||||
|
PowerOff,
|
||||||
|
Presentation,
|
||||||
|
SquareTerminal,
|
||||||
|
TextAlignStart,
|
||||||
|
TrafficCone
|
||||||
|
} from 'lucide-svelte';
|
||||||
|
import Button from './Button.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl min-w-0">
|
<div class="grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl min-w-0">
|
||||||
<div class="text-xl font-bold pl-3 content-center bg-stone-700 rounded-t-2xl truncate min-w-0">
|
<div class="text-xl font-bold pl-3 content-center bg-stone-700 rounded-t-2xl truncate min-w-0">
|
||||||
{'Bildschirme steuern (' + $selected_display_ids.length + ' ausgewählt)'}
|
Bildschirme steuern
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 p-2 overflow-auto">
|
<div class="flex flex-col gap-2 p-2 overflow-auto">
|
||||||
<div class="flex flex-row justify-between gap-2">
|
<div class="flex flex-row justify-between gap-2">
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
} from '../ts/stores/displays';
|
} from '../ts/stores/displays';
|
||||||
import DNDGrip from './DNDGrip.svelte';
|
import DNDGrip from './DNDGrip.svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import type { DisplayGroup } from '../ts/types';
|
import type { DisplayGroup, MenuOption } from '../ts/types';
|
||||||
import { selected_display_ids } from '../ts/stores/select';
|
import { selected_display_ids } from '../ts/stores/select';
|
||||||
|
|
||||||
let { display_group } = $props<{
|
let { display_group, get_display_menu_options } = $props<{
|
||||||
display_group: DisplayGroup;
|
display_group: DisplayGroup;
|
||||||
|
get_display_menu_options: (display_id: string) => MenuOption[]
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let hovering_selectable = $state(false);
|
let hovering_selectable = $state(false);
|
||||||
@@ -76,7 +77,7 @@
|
|||||||
class="outline-none"
|
class="outline-none"
|
||||||
role="figure"
|
role="figure"
|
||||||
>
|
>
|
||||||
<DisplayObject {display} />
|
<DisplayObject {display} {get_display_menu_options} />
|
||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,12 @@
|
|||||||
import DNDGrip from './DNDGrip.svelte';
|
import DNDGrip from './DNDGrip.svelte';
|
||||||
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte';
|
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte';
|
||||||
import OnlineState from './OnlineState.svelte';
|
import OnlineState from './OnlineState.svelte';
|
||||||
import type { Display } from '../ts/types';
|
import type { Display, MenuOption } from '../ts/types';
|
||||||
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
|
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
|
||||||
|
|
||||||
let { display } = $props<{
|
let { display, get_display_menu_options } = $props<{
|
||||||
display: Display;
|
display: Display;
|
||||||
|
get_display_menu_options: (display_id: string) => MenuOption[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let hovering_unselectable = $state(false);
|
let hovering_unselectable = $state(false);
|
||||||
@@ -121,17 +122,7 @@
|
|||||||
click_function={(e) => {
|
click_function={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
menu_options={[
|
menu_options={get_display_menu_options(display.id)}
|
||||||
{
|
|
||||||
icon: Pencil,
|
|
||||||
name: 'Bildschirm bearbeiten'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Trash2,
|
|
||||||
name: 'Bildschirm löschen',
|
|
||||||
class: 'text-red-400 hover:text-stone-200 hover:!bg-red-400 active:!bg-red-500'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -16,18 +16,42 @@
|
|||||||
next_height_step_size,
|
next_height_step_size,
|
||||||
pinned_display_id
|
pinned_display_id
|
||||||
} from '../ts/stores/ui_behavior';
|
} from '../ts/stores/ui_behavior';
|
||||||
import { type Display, type DisplayGroup } from '../ts/types';
|
import { type Display, type DisplayGroup, type MenuOption } from '../ts/types';
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import OnlineState from './OnlineState.svelte';
|
import OnlineState from './OnlineState.svelte';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
import { Menu, Minus, Pencil, PinOff, Plus, Trash2, VideoOff } from 'lucide-svelte';
|
import { Menu, Minus, Pencil, PinOff, Plus, Settings, Trash2, VideoOff } from 'lucide-svelte';
|
||||||
import { selected_display_ids } from '../ts/stores/select';
|
import { selected_display_ids } from '../ts/stores/select';
|
||||||
import { dragHandleZone } from 'svelte-dnd-action';
|
import { dragHandleZone } from 'svelte-dnd-action';
|
||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import DisplayGroupObject from './DisplayGroupObject.svelte';
|
import DisplayGroupObject from './DisplayGroupObject.svelte';
|
||||||
|
|
||||||
|
let { handle_display_deletion, handle_display_editing } = $props<{
|
||||||
|
handle_display_deletion: (display_id: string) => void;
|
||||||
|
handle_display_editing: (display_id: string) => void;
|
||||||
|
}>();
|
||||||
|
|
||||||
let displays_scroll_box: HTMLElement;
|
let displays_scroll_box: HTMLElement;
|
||||||
let pinned_display: Display | null = $derived(get_display_by_id($pinned_display_id || '', $displays));
|
let pinned_display: Display | null = $derived(
|
||||||
|
get_display_by_id($pinned_display_id || '', $displays)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
function get_display_menu_options(display_id: string): MenuOption[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
icon: Pencil,
|
||||||
|
name: 'Bildschirm bearbeiten',
|
||||||
|
on_select: () => {handle_display_editing(display_id)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Trash2,
|
||||||
|
name: 'Bildschirm löschen',
|
||||||
|
class: 'text-red-400 hover:text-stone-200 hover:!bg-red-400 active:!bg-red-500',
|
||||||
|
on_select: () => {handle_display_deletion(display_id)},
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
|
function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
|
||||||
const new_value = !all_selected(current_displays, current_selected_display_ids);
|
const new_value = !all_selected(current_displays, current_selected_display_ids);
|
||||||
@@ -87,17 +111,7 @@
|
|||||||
click_function={(e) => {
|
click_function={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
menu_options={[
|
menu_options={get_display_menu_options($pinned_display_id)}
|
||||||
{
|
|
||||||
icon: Pencil,
|
|
||||||
name: 'Bildschirm bearbeiten'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Trash2,
|
|
||||||
name: 'Bildschirm löschen',
|
|
||||||
class: 'text-red-400 hover:text-stone-200 hover:!bg-red-400 active:!bg-red-500'
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<Menu />
|
<Menu />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -136,7 +150,7 @@
|
|||||||
<!-- Normal Heading Left -->
|
<!-- Normal Heading Left -->
|
||||||
<div class="bg-stone-700 flex justify-between w-full p-1 gap-2 min-w-0">
|
<div class="bg-stone-700 flex justify-between w-full p-1 gap-2 min-w-0">
|
||||||
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
|
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
|
||||||
Bereits verbundene Displays
|
Verbundene Bildschirme
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-row gap-1">
|
<div class="flex flex-row gap-1">
|
||||||
<button
|
<button
|
||||||
@@ -157,7 +171,7 @@
|
|||||||
: 'Alle auswählen'}</span
|
: 'Alle auswählen'}</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-ro">
|
<div class="flex flex-row">
|
||||||
<Button
|
<Button
|
||||||
title="Bildschirme größer darstellen"
|
title="Bildschirme größer darstellen"
|
||||||
className="aspect-square !p-1 rounded-r-none"
|
className="aspect-square !p-1 rounded-r-none"
|
||||||
@@ -202,16 +216,24 @@
|
|||||||
$is_group_drag = false;
|
$is_group_drag = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#each $displays as display_group (display_group.id)}
|
{#if $displays.length === 1 && $displays[0].data.length === 0}
|
||||||
<!-- Each Group -->
|
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
|
||||||
<section
|
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf <Settings
|
||||||
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
class="inline pb-1"
|
||||||
animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
/> und "Neuen Bildschirm hinzufügen".
|
||||||
class="outline-none"
|
</div>
|
||||||
>
|
{:else}
|
||||||
<DisplayGroupObject {display_group} />
|
{#each $displays as display_group (display_group.id)}
|
||||||
</section>
|
<!-- Each Group -->
|
||||||
{/each}
|
<section
|
||||||
|
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
||||||
|
animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
||||||
|
class="outline-none"
|
||||||
|
>
|
||||||
|
<DisplayGroupObject {display_group} {get_display_menu_options} />
|
||||||
|
</section>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div
|
<div
|
||||||
class="bg-stone-800 rounded-2xl min-w-[30%] min-h-[30%] max-w-[80%] max-h-[80%] flex flex-col shadow-2xl/60 overflow-hidden"
|
class="bg-stone-800 rounded-2xl min-w-[30%] max-w-[80%] max-h-[80%] flex flex-col shadow-2xl/60 overflow-hidden"
|
||||||
onclick={(e) => e.stopPropagation()}
|
onclick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-2 min-h-0 overflow-auto flex flex-col gap-2">
|
<div class="p-2 min-h-0 overflow-auto flex flex-col gap-2">
|
||||||
{@render content.snippet()}
|
{@render content.snippet(content.snippet_arg)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Monitor, Plus, Radio, Settings, X } from 'lucide-svelte';
|
import { Monitor, Plus, Radio, Settings, Trash2, X } from 'lucide-svelte';
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../components/Button.svelte';
|
||||||
import FileView from '../components/FileView.svelte';
|
import FileView from '../components/FileView.svelte';
|
||||||
import ControlView from '../components/ControlView.svelte';
|
import ControlView from '../components/ControlView.svelte';
|
||||||
@@ -8,7 +8,14 @@
|
|||||||
import PopUp from '../components/PopUp.svelte';
|
import PopUp from '../components/PopUp.svelte';
|
||||||
import type { PopupContent } from '../ts/types';
|
import type { PopupContent } from '../ts/types';
|
||||||
import TextInput from '../components/TextInput.svelte';
|
import TextInput from '../components/TextInput.svelte';
|
||||||
import { add_display, is_display_name_taken } from '../ts/stores/displays';
|
import {
|
||||||
|
add_display,
|
||||||
|
displays,
|
||||||
|
edit_display_data,
|
||||||
|
get_display_by_id,
|
||||||
|
is_display_name_taken,
|
||||||
|
remove_display
|
||||||
|
} from '../ts/stores/displays';
|
||||||
import { text } from '@sveltejs/kit';
|
import { text } from '@sveltejs/kit';
|
||||||
|
|
||||||
const ip_regex =
|
const ip_regex =
|
||||||
@@ -22,11 +29,12 @@
|
|||||||
closable: true
|
closable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
let text_inputs_valid = $state({
|
const text_inputs_valid_null_values = {
|
||||||
name: { valid: false, value: '' },
|
name: { valid: false, value: '' },
|
||||||
ip: { valid: false, value: '' },
|
ip: { valid: false, value: '' },
|
||||||
mac: { valid: false, value: '' }
|
mac: { valid: false, value: '' }
|
||||||
});
|
};
|
||||||
|
let text_inputs_valid = $state(text_inputs_valid_null_values);
|
||||||
|
|
||||||
function all_text_inputs_valid(): boolean {
|
function all_text_inputs_valid(): boolean {
|
||||||
for (const entry of Object.values(text_inputs_valid)) {
|
for (const entry of Object.values(text_inputs_valid)) {
|
||||||
@@ -37,17 +45,16 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalize_add_new_display() {
|
function finalize_add_edit_display(existing_display_id: string|null) {
|
||||||
const ip = text_inputs_valid.ip.value;
|
const ip = text_inputs_valid.ip.value;
|
||||||
const mac = text_inputs_valid.mac.value === '' ? null : text_inputs_valid.mac.value;
|
const mac = text_inputs_valid.mac.value === '' ? null : text_inputs_valid.mac.value;
|
||||||
const name = text_inputs_valid.name.value;
|
const name = text_inputs_valid.name.value;
|
||||||
add_display(ip, mac, name, 'Online');
|
if (!!existing_display_id) {
|
||||||
|
edit_display_data(existing_display_id, ip, mac, name);
|
||||||
|
} else {
|
||||||
|
add_display(ip, mac, name, 'Online');
|
||||||
|
}
|
||||||
popup_close_function();
|
popup_close_function();
|
||||||
text_inputs_valid = {
|
|
||||||
name: { valid: false, value: '' },
|
|
||||||
ip: { valid: false, value: '' },
|
|
||||||
mac: { valid: false, value: '' }
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function popup_close_function() {
|
function popup_close_function() {
|
||||||
@@ -55,28 +62,81 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const show_new_display_popup = () => {
|
const show_new_display_popup = () => {
|
||||||
|
text_inputs_valid = text_inputs_valid_null_values;
|
||||||
popup_content = {
|
popup_content = {
|
||||||
open: true,
|
open: true,
|
||||||
snippet: add_new_display,
|
snippet: add_new_display_popup,
|
||||||
title: 'Neuen Bildschirm hinzufügen',
|
title: 'Neuen Bildschirm hinzufügen',
|
||||||
title_icon: Monitor,
|
title_icon: Monitor,
|
||||||
closable: true
|
closable: true
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const show_remove_display_popup = (display_id: string) => {
|
||||||
|
popup_content = {
|
||||||
|
open: true,
|
||||||
|
snippet: remove_display_popup,
|
||||||
|
snippet_arg: display_id,
|
||||||
|
title: 'Bildschirm wirklich löschen?',
|
||||||
|
title_class: 'text-red-400',
|
||||||
|
title_icon: Trash2,
|
||||||
|
closable: true
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_edit_display_popup = (display_id: string) => {
|
||||||
|
const display = get_display_by_id(display_id, $displays);
|
||||||
|
if (!display) return;
|
||||||
|
// insert existing values in text_inputs_valid
|
||||||
|
for (const key of Object.keys(text_inputs_valid) as (keyof typeof text_inputs_valid)[]) {
|
||||||
|
text_inputs_valid[key].valid = true;
|
||||||
|
text_inputs_valid[key].value = display[key] || '';
|
||||||
|
}
|
||||||
|
popup_content = {
|
||||||
|
open: true,
|
||||||
|
snippet: add_new_display_popup,
|
||||||
|
snippet_arg: display_id,
|
||||||
|
title: 'Bildschirm bearbeiten',
|
||||||
|
title_icon: Monitor,
|
||||||
|
closable: true
|
||||||
|
};
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet add_new_display()}
|
{#snippet remove_display_popup(display_id: string)}
|
||||||
|
<div class="max-w-prose px-2">
|
||||||
|
Soll der Bildschirm "{get_display_by_id(display_id, $displays)?.name || '?'}" wirklich gelöscht
|
||||||
|
werden? Dadurch wird es von diesem Controller nicht mehr erreichbar. Die Installation auf dem
|
||||||
|
Gerät bleibt bestehen. Mit dem erneuten Hinzufügen des Bildschirms wird er wieder steuerbar.
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row justify-end gap-2">
|
||||||
|
<Button className="px-4 font-bold" click_function={popup_close_function}>Abbrechen</Button>
|
||||||
|
<Button
|
||||||
|
hover_bg="bg-red-400"
|
||||||
|
active_bg="bg-red-500"
|
||||||
|
className="px-4 flex text-red-400 hover:text-stone-100"
|
||||||
|
click_function={() => {
|
||||||
|
remove_display(display_id);
|
||||||
|
popup_close_function();
|
||||||
|
}}>Löschen</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{#snippet add_new_display_popup(existing_display_id: string | null = null)}
|
||||||
<TextInput
|
<TextInput
|
||||||
bind:current_value={text_inputs_valid.name.value}
|
bind:current_value={text_inputs_valid.name.value}
|
||||||
bind:current_valid={text_inputs_valid.name.valid}
|
bind:current_valid={text_inputs_valid.name.valid}
|
||||||
title="Anzeigename"
|
title="Anzeigename"
|
||||||
placeholder="z.B. Beamer vorne links"
|
placeholder="z.B. Beamer vorne links"
|
||||||
is_valid_function={(input: string) => {
|
is_valid_function={(input: string) => {
|
||||||
return input.length === 0 || input.length > 50
|
if (!!existing_display_id) {
|
||||||
? [false, 'Ungültige Länge']
|
if (input === get_display_by_id(existing_display_id, $displays)?.name)
|
||||||
: is_display_name_taken(input)
|
return [true, 'Gültiger Name'];
|
||||||
? [false, 'Name bereits verwendet']
|
}
|
||||||
: [true, 'Gültiger Name'];
|
if (input.length === 0 || input.length > 50) return [false, 'Ungültige Länge'];
|
||||||
|
if (is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
|
||||||
|
return [true, 'Gültiger Name'];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
@@ -111,13 +171,24 @@
|
|||||||
: [false, 'Ungültige MAC-Adresse'];
|
: [false, 'Ungültige MAC-Adresse'];
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="flex justify-end pt-2">
|
<div class="flex flex-row gap-2 justify-end pt-2">
|
||||||
|
{#if !!existing_display_id}
|
||||||
|
<!-- TODO: Ping mit existing_display_id -->
|
||||||
|
<Button className="px-4" click_function={popup_close_function}>Abbrechen</Button>
|
||||||
|
{/if}
|
||||||
<Button
|
<Button
|
||||||
disabled={!all_text_inputs_valid()}
|
disabled={!all_text_inputs_valid()}
|
||||||
className="pl-3 pr-4 gap-2 font-bold"
|
className="{!!existing_display_id ? 'px-4' : 'pl-3 pr-4 gap-2'} font-bold"
|
||||||
bg="bg-stone-650"
|
bg="bg-stone-650"
|
||||||
click_function={finalize_add_new_display}><Plus /> Bildschirm hinzufügen</Button
|
click_function={() => {
|
||||||
>
|
finalize_add_edit_display(existing_display_id);
|
||||||
|
}}
|
||||||
|
>{#if !!existing_display_id}
|
||||||
|
Speichern
|
||||||
|
{:else}
|
||||||
|
<Plus /> Bildschirm hinzufügen
|
||||||
|
{/if}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
@@ -146,7 +217,10 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
|
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
|
||||||
<DisplayView />
|
<DisplayView
|
||||||
|
handle_display_deletion={show_remove_display_popup}
|
||||||
|
handle_display_editing={show_edit_display_popup}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2"
|
class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,19 +11,44 @@ export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
|
|||||||
|
|
||||||
|
|
||||||
export function is_display_name_taken(name: string): boolean {
|
export function is_display_name_taken(name: string): boolean {
|
||||||
const display_groups = get(displays);
|
const display_groups = get(displays);
|
||||||
return display_groups.some(group =>
|
return display_groups.some(group =>
|
||||||
group.data.some(display => display.name.trim().toLowerCase() === name.trim().toLowerCase())
|
group.data.some(display => display.name.trim().toLowerCase() === name.trim().toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function add_display(ip: string, mac: string|null, name: string, status: string) {
|
export function add_display(ip: string, mac: string | null, name: string, status: string) {
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
displays.update((displays: DisplayGroup[]) => {
|
||||||
displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status });
|
displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status });
|
||||||
return displays;
|
return displays;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function edit_display_data(display_id: string, ip: string, mac: string | null, name: string) {
|
||||||
|
displays.update((display_groups) =>
|
||||||
|
display_groups.map((group) => ({
|
||||||
|
...group,
|
||||||
|
data: group.data.map((display) => {
|
||||||
|
if (display.id !== display_id) return display;
|
||||||
|
return { ...display, ip: ip, mac: mac, name: name };
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function remove_display(display_id: string) {
|
||||||
|
console.log(display_id);
|
||||||
|
displays.update((displays: DisplayGroup[]) => {
|
||||||
|
displays = displays.map(display_group => ({
|
||||||
|
...display_group,
|
||||||
|
data: display_group.data.filter(display => display.id !== display_id)
|
||||||
|
}));
|
||||||
|
return displays;
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO remove ID from Files usw.
|
||||||
|
}
|
||||||
|
|
||||||
export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) {
|
export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) {
|
||||||
if (display_group.data.length === 0) return false;
|
if (display_group.data.length === 0) return false;
|
||||||
for (const display of display_group.data) {
|
for (const display of display_group.data) {
|
||||||
@@ -139,6 +164,6 @@ function add_testing_displays() {
|
|||||||
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline");
|
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline");
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "Offline");
|
add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "Offline");
|
||||||
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "Online");
|
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "Online");
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { FileBox, FileImage, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
|
import { FileBox, FileImage, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
|
||||||
|
import type { Snippet } from "svelte";
|
||||||
|
|
||||||
export type SupportedFileType = {
|
export type SupportedFileType = {
|
||||||
display_name:
|
display_name:
|
||||||
@@ -60,7 +61,7 @@ export type Display = {
|
|||||||
ip: string;
|
ip: string;
|
||||||
preview_url: string | null;
|
preview_url: string | null;
|
||||||
preview_timeout_id: number | null;
|
preview_timeout_id: number | null;
|
||||||
mac: string|null;
|
mac: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
status: string;
|
status: string;
|
||||||
}
|
}
|
||||||
@@ -81,7 +82,8 @@ export type MenuOption = {
|
|||||||
|
|
||||||
export type PopupContent = {
|
export type PopupContent = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
snippet: any;
|
snippet: Snippet<[string]> | null;
|
||||||
|
snippet_arg?: string;
|
||||||
title: string;
|
title: string;
|
||||||
title_class?: string;
|
title_class?: string;
|
||||||
title_icon?: typeof X | null;
|
title_icon?: typeof X | null;
|
||||||
|
|||||||
+36
-4
@@ -2,8 +2,12 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
"plg-mudics/control/frontend"
|
"plg-mudics/control/frontend"
|
||||||
|
"plg-mudics/shared"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
@@ -20,12 +24,40 @@ func main() {
|
|||||||
|
|
||||||
// Servers all API endpoints, e.g. our custom logic
|
// Servers all API endpoints, e.g. our custom logic
|
||||||
apiGroup := e.Group("/api")
|
apiGroup := e.Group("/api")
|
||||||
apiGroup.GET("/ping", func(ctx echo.Context) error {
|
apiGroup.GET("/ping", pingRoute)
|
||||||
return ctx.String(http.StatusOK, "pong")
|
|
||||||
})
|
|
||||||
|
|
||||||
err := e.Start(":1323")
|
port := "8080"
|
||||||
|
|
||||||
|
go shared.OpenBrowserWindow("http://localhost:"+port, false, false)
|
||||||
|
|
||||||
|
err := e.Start(":" + port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to start Echo Webserver", "error", err)
|
slog.Error("Failed to start Echo Webserver", "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pingRoute(ctx echo.Context) error {
|
||||||
|
ip := ctx.QueryParam("ip")
|
||||||
|
if ip == "" {
|
||||||
|
return ctx.JSON(http.StatusBadRequest, PingResponse{Error: "missing 'ip' query parameter"})
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("ping", "-c", "1", "-w", "1", ip)
|
||||||
|
result := shared.RunShellCommand(cmd)
|
||||||
|
if result.ExitCode != 0 {
|
||||||
|
return ctx.JSON(http.StatusOK, PingResponse{Status: "host_offline"})
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := net.DialTimeout("tcp", ip+":1323", 1*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return ctx.JSON(http.StatusOK, PingResponse{Status: "app_offline"})
|
||||||
|
}
|
||||||
|
conn.Close()
|
||||||
|
|
||||||
|
return ctx.JSON(http.StatusOK, PingResponse{Status: "app_online"})
|
||||||
|
}
|
||||||
|
|
||||||
|
type PingResponse struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"version": "1",
|
||||||
|
"name": "PLG-MuDiCS Control",
|
||||||
|
"type": "collection",
|
||||||
|
"ignore": [
|
||||||
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
meta {
|
||||||
|
name: ping
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
get {
|
||||||
|
url: http://127.0.0.1:8080/api/ping?ip=127.0.0.1
|
||||||
|
body: none
|
||||||
|
auth: inherit
|
||||||
|
}
|
||||||
|
|
||||||
|
params:query {
|
||||||
|
ip: 127.0.0.1
|
||||||
|
}
|
||||||
|
|
||||||
|
settings {
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
# PLG-MuDICS Display
|
# PLG-MuDiCS Display
|
||||||
|
|
||||||
## Runtime Requirements
|
## Runtime Requirements
|
||||||
|
|
||||||
@@ -11,6 +11,6 @@
|
|||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
- install Go (at least version v1.24)
|
- install Go (at least version v1.25)
|
||||||
- build template: `go tool templ generate`
|
- build template: `go generate ./..`
|
||||||
- run code: `go run .`
|
- run code: `go run .`
|
||||||
|
|||||||
+4
-3
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"plg-mudics/display/pkg"
|
"plg-mudics/display/pkg"
|
||||||
"plg-mudics/display/web"
|
"plg-mudics/display/web"
|
||||||
|
"plg-mudics/shared"
|
||||||
)
|
)
|
||||||
|
|
||||||
const VERSION = "0.1.0"
|
const VERSION = "0.1.0"
|
||||||
@@ -22,10 +23,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
port := "1323"
|
||||||
|
go web.StartWebServer(VERSION, port)
|
||||||
|
|
||||||
go web.StartWebServer(VERSION)
|
err = shared.OpenBrowserWindow("http://localhost:"+port, true, true)
|
||||||
|
|
||||||
err = pkg.OpenBrowserWindow("http://127.0.0.1:1323")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to open browser window", "error", err)
|
slog.Error("Failed to open browser window", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"plg-mudics/shared"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -36,7 +37,7 @@ func GenerateFilePreview(inputPath string) (string, error) {
|
|||||||
|
|
||||||
func generateImagePreview(inputPath string, outputPath string) error {
|
func generateImagePreview(inputPath string, outputPath string) error {
|
||||||
cmd := exec.Command("magick", inputPath, "-thumbnail", "100x100", "-quality", "50", outputPath)
|
cmd := exec.Command("magick", inputPath, "-thumbnail", "100x100", "-quality", "50", outputPath)
|
||||||
result := RunShellCommand(cmd)
|
result := shared.RunShellCommand(cmd)
|
||||||
if result.ExitCode != 0 {
|
if result.ExitCode != 0 {
|
||||||
if result.ExitCode == 127 {
|
if result.ExitCode == 127 {
|
||||||
return ErrFilePreviewToolsMissing
|
return ErrFilePreviewToolsMissing
|
||||||
@@ -48,12 +49,12 @@ func generateImagePreview(inputPath string, outputPath string) error {
|
|||||||
|
|
||||||
func generatePDFPreview(inputPath string, outputPath string) error {
|
func generatePDFPreview(inputPath string, outputPath string) error {
|
||||||
testCmd := exec.Command("which", "gs")
|
testCmd := exec.Command("which", "gs")
|
||||||
if result := RunShellCommand(testCmd); result.ExitCode != 0 {
|
if result := shared.RunShellCommand(testCmd); result.ExitCode != 0 {
|
||||||
return ErrFilePreviewToolsMissing
|
return ErrFilePreviewToolsMissing
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := exec.Command("magick", fmt.Sprintf("%s[0]", inputPath), "-thumbnail", "100x100", "-quality", "50", outputPath)
|
cmd := exec.Command("magick", fmt.Sprintf("%s[0]", inputPath), "-thumbnail", "100x100", "-quality", "50", outputPath)
|
||||||
result := RunShellCommand(cmd)
|
result := shared.RunShellCommand(cmd)
|
||||||
if result.ExitCode != 0 {
|
if result.ExitCode != 0 {
|
||||||
if result.ExitCode == 127 {
|
if result.ExitCode == 127 {
|
||||||
return ErrFilePreviewToolsMissing
|
return ErrFilePreviewToolsMissing
|
||||||
@@ -67,7 +68,7 @@ func generateVideoPreview(inputPath string, outputPath string) error {
|
|||||||
tempFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("preview_temp_%d.webp", time.Now().Unix()))
|
tempFilePath := filepath.Join(os.TempDir(), fmt.Sprintf("preview_temp_%d.webp", time.Now().Unix()))
|
||||||
|
|
||||||
ffmpegCmd := exec.Command("ffmpeg", "-i", inputPath, "-ss", "00:00:01.000", "-vframes", "1", tempFilePath)
|
ffmpegCmd := exec.Command("ffmpeg", "-i", inputPath, "-ss", "00:00:01.000", "-vframes", "1", tempFilePath)
|
||||||
result := RunShellCommand(ffmpegCmd)
|
result := shared.RunShellCommand(ffmpegCmd)
|
||||||
if result.ExitCode != 0 {
|
if result.ExitCode != 0 {
|
||||||
if result.ExitCode == 127 {
|
if result.ExitCode == 127 {
|
||||||
return ErrFilePreviewToolsMissing
|
return ErrFilePreviewToolsMissing
|
||||||
|
|||||||
+5
-47
@@ -1,7 +1,6 @@
|
|||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
@@ -12,14 +11,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"plg-mudics/shared"
|
||||||
|
|
||||||
"github.com/micmonay/keybd_event"
|
"github.com/micmonay/keybd_event"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandResponse struct {
|
|
||||||
Stdout string `json:"stdout"`
|
|
||||||
Stderr string `json:"stderr"`
|
|
||||||
ExitCode int `json:"exitCode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetDeviceIp() (string, error) {
|
func GetDeviceIp() (string, error) {
|
||||||
addrs, err := net.InterfaceAddrs()
|
addrs, err := net.InterfaceAddrs()
|
||||||
@@ -54,7 +51,7 @@ func GetDeviceMac() (string, error) {
|
|||||||
|
|
||||||
func OpenPresentation(path string) error {
|
func OpenPresentation(path string) error {
|
||||||
cmd := exec.Command("bash", "-c", "-r", fmt.Sprintf("soffice --show %s -nologo -norestore", path))
|
cmd := exec.Command("bash", "-c", "-r", fmt.Sprintf("soffice --show %s -nologo -norestore", path))
|
||||||
result := RunShellCommand(cmd)
|
result := shared.RunShellCommand(cmd)
|
||||||
if result.ExitCode != 0 {
|
if result.ExitCode != 0 {
|
||||||
return errors.New(result.Stderr)
|
return errors.New(result.Stderr)
|
||||||
}
|
}
|
||||||
@@ -84,7 +81,7 @@ func TakeScreenshot() (string, error) {
|
|||||||
exec.Command("spectacle", "--fullscreen", "--nonotify", "--background", "--output", tempFilePath),
|
exec.Command("spectacle", "--fullscreen", "--nonotify", "--background", "--output", tempFilePath),
|
||||||
}
|
}
|
||||||
for _, cmd := range cmds {
|
for _, cmd := range cmds {
|
||||||
commandOutput := RunShellCommand(cmd)
|
commandOutput := shared.RunShellCommand(cmd)
|
||||||
if commandOutput.ExitCode == 0 {
|
if commandOutput.ExitCode == 0 {
|
||||||
return tempFilePath, nil
|
return tempFilePath, nil
|
||||||
}
|
}
|
||||||
@@ -93,45 +90,6 @@ func TakeScreenshot() (string, error) {
|
|||||||
return "", errors.New("no screenshot utility found or all failed")
|
return "", errors.New("no screenshot utility found or all failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func RunShellCommand(cmd *exec.Cmd) CommandResponse {
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
cmd.Stdout = &stdout
|
|
||||||
cmd.Stderr = &stderr
|
|
||||||
err := cmd.Run()
|
|
||||||
|
|
||||||
commandOutput := CommandResponse{
|
|
||||||
Stdout: stdout.String(),
|
|
||||||
Stderr: stderr.String(),
|
|
||||||
ExitCode: cmd.ProcessState.ExitCode(),
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
var exitErr *exec.ExitError
|
|
||||||
if errors.As(err, &exitErr) {
|
|
||||||
commandOutput.ExitCode = exitErr.ExitCode()
|
|
||||||
} else {
|
|
||||||
commandOutput.Stderr = err.Error()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return commandOutput
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenBrowserWindow(url string) error {
|
|
||||||
template := "%s --app='%s' --start-fullscreen --user-data-dir=$(mktemp -d) --autoplay-policy=no-user-gesture-required"
|
|
||||||
|
|
||||||
cmds := []*exec.Cmd{
|
|
||||||
exec.Command("bash", "-c", fmt.Sprintf(template, "chromium", url)),
|
|
||||||
exec.Command("bash", "-c", fmt.Sprintf(template, "chromium-browser", url)),
|
|
||||||
}
|
|
||||||
for _, cmd := range cmds {
|
|
||||||
commandOutput := RunShellCommand(cmd)
|
|
||||||
if commandOutput.ExitCode == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("chromium not found in PATH")
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetStoragePath() (string, error) {
|
func GetStoragePath() (string, error) {
|
||||||
var storagePath string
|
var storagePath string
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
{
|
{
|
||||||
"version": "1",
|
"version": "1",
|
||||||
"name": "PLG-MuDICS",
|
"name": "PLG-MuDiCS Display",
|
||||||
"type": "collection",
|
"type": "collection",
|
||||||
"ignore": ["node_modules", ".git"]
|
"ignore": [
|
||||||
}
|
"node_modules",
|
||||||
|
".git"
|
||||||
|
]
|
||||||
|
}
|
||||||
+3
-3
@@ -37,7 +37,7 @@ var supportedExtensions = map[string]bool{
|
|||||||
".odp": true,
|
".odp": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartWebServer(v string) {
|
func StartWebServer(v string, port string) {
|
||||||
version = v
|
version = v
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
@@ -63,7 +63,7 @@ func StartWebServer(v string) {
|
|||||||
fileGroup.PATCH("/:path", openFileRoute)
|
fileGroup.PATCH("/:path", openFileRoute)
|
||||||
fileGroup.GET("/preview/:path", previewRoute)
|
fileGroup.GET("/preview/:path", previewRoute)
|
||||||
|
|
||||||
err := e.Start(":1323")
|
err := e.Start(":" + port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("Failed to start server", "error", err)
|
slog.Error("Failed to start server", "error", err)
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ func shellCommandRoute(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
cmd.Dir = storagePath
|
cmd.Dir = storagePath
|
||||||
|
|
||||||
commandOutput := pkg.RunShellCommand(cmd)
|
commandOutput := shared.RunShellCommand(cmd)
|
||||||
if commandOutput.ExitCode != 0 {
|
if commandOutput.ExitCode != 0 {
|
||||||
slog.Error("Shell command execution error", "error", commandOutput.Stderr)
|
slog.Error("Shell command execution error", "error", commandOutput.Stderr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed splash_screen.html
|
||||||
|
var SplashScreenTemplate string
|
||||||
+33
-3
@@ -1,8 +1,38 @@
|
|||||||
package shared
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed splash_screen.html
|
type CommandResponse struct {
|
||||||
var SplashScreenTemplate string
|
Stdout string `json:"stdout"`
|
||||||
|
Stderr string `json:"stderr"`
|
||||||
|
ExitCode int `json:"exitCode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func RunShellCommand(cmd *exec.Cmd) CommandResponse {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
commandOutput := CommandResponse{
|
||||||
|
Stdout: stdout.String(),
|
||||||
|
Stderr: stderr.String(),
|
||||||
|
ExitCode: cmd.ProcessState.ExitCode(),
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
var exitErr *exec.ExitError
|
||||||
|
if errors.As(err, &exitErr) {
|
||||||
|
commandOutput.ExitCode = exitErr.ExitCode()
|
||||||
|
} else {
|
||||||
|
commandOutput.Stderr = err.Error()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commandOutput
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package shared
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OpenBrowserWindow(url string, fullscreen bool, temp bool) error {
|
||||||
|
bins := []string{"chromium", "chromium-browser"}
|
||||||
|
|
||||||
|
args := []string{fmt.Sprintf("--app=%s", url), "--autoplay-policy=no-user-gesture-required"}
|
||||||
|
if fullscreen {
|
||||||
|
args = append(args, "--start-fullscreen")
|
||||||
|
}
|
||||||
|
if temp {
|
||||||
|
tempDirPath, err := os.MkdirTemp("", "plg-mudics-browser-")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
arg := fmt.Sprintf("--user-data-dir=%s", tempDirPath)
|
||||||
|
args = append(args, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bin := range bins {
|
||||||
|
cmd := exec.Command(bin, args...)
|
||||||
|
commandOutput := RunShellCommand(cmd)
|
||||||
|
if commandOutput.ExitCode == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("chromium not found in PATH")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user