mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 17:07:08 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd6ec88d23 | |||
| 10a09bc45e | |||
| bf08ec946a | |||
| e0f7f36383 | |||
| cf8eeacb5e | |||
| 2168873d3a | |||
| 9fd81821f1 | |||
| 9487fdd13e | |||
| aeea501a05 | |||
| 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:
|
||||
push:
|
||||
paths:
|
||||
- go.mod
|
||||
- go.sum
|
||||
- "**/*.go"
|
||||
- "**/*.tmpl"
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.yml"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
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
|
||||
@@ -20,21 +27,25 @@ jobs:
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
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
|
||||
working-directory: display
|
||||
run: go mod download
|
||||
|
||||
- name: Generate Templates
|
||||
working-directory: display
|
||||
run: go tool templ generate
|
||||
- name: Generate
|
||||
run: go generate ./...
|
||||
|
||||
- name: Build
|
||||
working-directory: display
|
||||
run: go build -o plg-mudics-display .
|
||||
run: go build -o plg-mudics-${{ matrix.block }} .
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: display/plg-mudics-display
|
||||
name: plg-mudics-${{ matrix.block }}
|
||||
path: ${{matrix.block}}/plg-mudics-${{ matrix.block }}
|
||||
|
||||
@@ -10,6 +10,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -21,3 +22,48 @@ jobs:
|
||||
with:
|
||||
name: PLG-MuDiCS ${{ github.ref_name }}
|
||||
body_path: CHANGELOG.md
|
||||
|
||||
build:
|
||||
name: Build and Upload Assets
|
||||
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: ${{ matrix.block }}/plg-mudics-${{ matrix.block }}
|
||||
tag: ${{ github.ref_name }}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
# PLG-MuDICS
|
||||
# PLG-MuDiCS
|
||||
|
||||

|
||||
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
# PLG-MuDICS Control
|
||||
# PLG-MuDiCS Control
|
||||
|
||||
## 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:@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:@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-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: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@^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-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: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",
|
||||
@@ -596,6 +599,30 @@
|
||||
"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": {
|
||||
"integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==",
|
||||
"dependencies": [
|
||||
@@ -1259,6 +1286,12 @@
|
||||
"lodash.merge@4.6.2": {
|
||||
"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": {
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"dependencies": [
|
||||
@@ -1525,6 +1558,12 @@
|
||||
],
|
||||
"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": {
|
||||
"integrity": "sha512-0Iztj5vcOVOVkhy1pbo5uA9r+d3yaVoE5XPc9eABIWDOSJZ2mOsZ4D+t45rphWCOr0uMw3jtSG2fh2e7GvKnPg==",
|
||||
"dependencies": [
|
||||
@@ -1689,13 +1728,16 @@
|
||||
"npm:@sveltejs/vite-plugin-svelte@6",
|
||||
"npm:@tailwindcss/typography@~0.5.15",
|
||||
"npm:@tailwindcss/vite@4",
|
||||
"npm:@thisux/sveltednd@^0.0.20",
|
||||
"npm:eslint-config-prettier@^10.0.1",
|
||||
"npm:eslint-plugin-svelte@3",
|
||||
"npm:eslint@^9.18.0",
|
||||
"npm:globals@16",
|
||||
"npm:lucide-svelte@0.545",
|
||||
"npm:prettier-plugin-svelte@^3.3.3",
|
||||
"npm:prettier@^3.4.2",
|
||||
"npm:svelte-check@4",
|
||||
"npm:svelte-dnd-action@~0.9.65",
|
||||
"npm:svelte@5",
|
||||
"npm:tailwindcss@4",
|
||||
"npm:typescript-eslint@^8.20.0",
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:generate deno install
|
||||
//go:generate deno task build
|
||||
|
||||
//go:embed all:build
|
||||
var buildDir embed.FS
|
||||
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
<script lang="ts">
|
||||
import { ArrowBigLeft, ArrowBigRight, ChevronDown, Keyboard, Power, PowerOff, Presentation, SquareTerminal, TextAlignStart, TrafficCone } from "lucide-svelte";
|
||||
import { selected_display_ids } from "../ts/stores/select";
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
|
||||
import {
|
||||
ArrowBigLeft,
|
||||
ArrowBigRight,
|
||||
ChevronDown,
|
||||
Keyboard,
|
||||
Power,
|
||||
PowerOff,
|
||||
Presentation,
|
||||
SquareTerminal,
|
||||
TextAlignStart,
|
||||
TrafficCone
|
||||
} from 'lucide-svelte';
|
||||
import Button from './Button.svelte';
|
||||
</script>
|
||||
|
||||
<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">
|
||||
{'Bildschirme steuern (' + $selected_display_ids.length + ' ausgewählt)'}
|
||||
Bildschirme steuern
|
||||
</div>
|
||||
<div class="flex flex-col gap-2 p-2 overflow-auto">
|
||||
<div class="flex flex-row justify-between gap-2">
|
||||
|
||||
@@ -13,11 +13,12 @@
|
||||
} from '../ts/stores/displays';
|
||||
import DNDGrip from './DNDGrip.svelte';
|
||||
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';
|
||||
|
||||
let { display_group } = $props<{
|
||||
let { display_group, get_display_menu_options } = $props<{
|
||||
display_group: DisplayGroup;
|
||||
get_display_menu_options: (display_id: string) => MenuOption[]
|
||||
}>();
|
||||
|
||||
let hovering_selectable = $state(false);
|
||||
@@ -76,7 +77,7 @@
|
||||
class="outline-none"
|
||||
role="figure"
|
||||
>
|
||||
<DisplayObject {display} />
|
||||
<DisplayObject {display} {get_display_menu_options} />
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
import DNDGrip from './DNDGrip.svelte';
|
||||
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-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';
|
||||
|
||||
let { display } = $props<{
|
||||
let { display, get_display_menu_options } = $props<{
|
||||
display: Display;
|
||||
get_display_menu_options: (display_id: string) => MenuOption[];
|
||||
}>();
|
||||
|
||||
let hovering_unselectable = $state(false);
|
||||
@@ -121,17 +122,7 @@
|
||||
click_function={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
menu_options={[
|
||||
{
|
||||
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_options={get_display_menu_options(display.id)}
|
||||
>
|
||||
<Menu />
|
||||
</Button>
|
||||
|
||||
@@ -16,18 +16,42 @@
|
||||
next_height_step_size,
|
||||
pinned_display_id
|
||||
} 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 OnlineState from './OnlineState.svelte';
|
||||
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 { dragHandleZone } from 'svelte-dnd-action';
|
||||
import { flip } from 'svelte/animate';
|
||||
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 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[]) {
|
||||
const new_value = !all_selected(current_displays, current_selected_display_ids);
|
||||
@@ -87,17 +111,7 @@
|
||||
click_function={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
menu_options={[
|
||||
{
|
||||
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_options={get_display_menu_options($pinned_display_id)}
|
||||
>
|
||||
<Menu />
|
||||
</Button>
|
||||
@@ -136,7 +150,7 @@
|
||||
<!-- Normal Heading Left -->
|
||||
<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">
|
||||
Bereits verbundene Displays
|
||||
Verbundene Bildschirme
|
||||
</span>
|
||||
<div class="flex flex-row gap-1">
|
||||
<button
|
||||
@@ -157,7 +171,7 @@
|
||||
: 'Alle auswählen'}</span
|
||||
>
|
||||
</button>
|
||||
<div class="flex flex-ro">
|
||||
<div class="flex flex-row">
|
||||
<Button
|
||||
title="Bildschirme größer darstellen"
|
||||
className="aspect-square !p-1 rounded-r-none"
|
||||
@@ -202,6 +216,13 @@
|
||||
$is_group_drag = false;
|
||||
}}
|
||||
>
|
||||
{#if $displays.length === 1 && $displays[0].data.length === 0}
|
||||
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
|
||||
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf <Settings
|
||||
class="inline pb-1"
|
||||
/> und "Neuen Bildschirm hinzufügen".
|
||||
</div>
|
||||
{:else}
|
||||
{#each $displays as display_group (display_group.id)}
|
||||
<!-- Each Group -->
|
||||
<section
|
||||
@@ -209,9 +230,10 @@
|
||||
animate:flip={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
||||
class="outline-none"
|
||||
>
|
||||
<DisplayGroupObject {display_group} />
|
||||
<DisplayGroupObject {display_group} {get_display_menu_options} />
|
||||
</section>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { fade } from "svelte/transition";
|
||||
import Button from "./Button.svelte";
|
||||
import { X } from "lucide-svelte";
|
||||
import { notifications } from "../ts/stores/notification";
|
||||
import { get_shifted_color } from "../ts/stores/ui_behavior";
|
||||
</script>
|
||||
|
||||
<div class="fixed flex flex-col gap-2 {true ? "top-[41%]": "top-2"} right-2 left-2 md:top-auto md:left-auto md:bottom-2 md:w-100 z-50">
|
||||
{#each $notifications as n (n.id)}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="p-2 pl-4 pb-3 rounded-lg shadow-xl/30 text-white flex flex-col gap-2 overflow-hidden relative border-1 border-black/20 {n.className}"
|
||||
class:bg-red-900={n.type === "error"}
|
||||
class:bg-green-900={n.type === "success"}
|
||||
class:bg-sky-900={n.type === "info"}
|
||||
style="--dur: {n.duration}ms"
|
||||
>
|
||||
<div class="flex flex-row justify-between">
|
||||
<span class="text-xl font-bold flex items-center">{n.title}</span>
|
||||
<Button click_function={() => notifications.remove(n.id)} className="p-2" bg="bg-stone-900/50" hover_bg="bg-stone-600/70" active_bg="bg-stone-500/80"><X/></Button>
|
||||
</div>
|
||||
|
||||
<span class="whitespace-break-spaces">{n.message}</span>
|
||||
|
||||
<div class="absolute inset-x-0 bottom-0 h-1 bg-white/25">
|
||||
<div
|
||||
class="block h-full w-full bg-white/80 origin-left animate-progress-bar"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes progress {
|
||||
from {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
to {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
.animate-progress-bar {
|
||||
animation: progress var(--dur) linear forwards;
|
||||
transform-origin: left;
|
||||
}
|
||||
</style>
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { display_status_to_info, type DisplayStatus } from "../ts/types";
|
||||
|
||||
let { selected, status, className = "" } = $props<{
|
||||
selected: boolean;
|
||||
status: string;
|
||||
status: DisplayStatus;
|
||||
className?: string;
|
||||
}>();
|
||||
|
||||
function get_text_color(selected: boolean, status: string) {
|
||||
function get_text_color(selected: boolean, status: DisplayStatus) {
|
||||
switch (status) {
|
||||
case 'Online':
|
||||
case 'app_online':
|
||||
return selected ? 'text-green-700' : 'text-green-400';
|
||||
case 'Lädt':
|
||||
case 'app_offline':
|
||||
return selected ? 'text-amber-700' : 'text-amber-400';
|
||||
case 'Offline':
|
||||
case 'host_offline':
|
||||
return selected ? 'text-red-700' : 'text-red-400';
|
||||
default:
|
||||
return selected ? 'text-stone-700' : 'text-stone-400';
|
||||
@@ -20,5 +22,5 @@
|
||||
</script>
|
||||
|
||||
<div class="{get_text_color(selected, status)} {className} transition-colors duration-100">
|
||||
{status}
|
||||
{display_status_to_info(status)}
|
||||
</div>
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<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()}
|
||||
>
|
||||
<div
|
||||
@@ -62,7 +62,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import Notification from '../components/Notification.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
{@render children()}
|
||||
|
||||
<Notification />
|
||||
@@ -1,15 +1,26 @@
|
||||
<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 FileView from '../components/FileView.svelte';
|
||||
import ControlView from '../components/ControlView.svelte';
|
||||
import DisplayView from '../components/DisplayView.svelte';
|
||||
import SplashScreen from './../../../../shared/splash_screen.html?raw';
|
||||
import PopUp from '../components/PopUp.svelte';
|
||||
import type { PopupContent } from '../ts/types';
|
||||
import { display_status_to_info, type PopupContent } from '../ts/types';
|
||||
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 { notifications } from '../ts/stores/notification';
|
||||
import { ping_ip } from '../ts/api_handler';
|
||||
import { onMount } from 'svelte';
|
||||
import { on_start } from '../ts/main';
|
||||
|
||||
const ip_regex =
|
||||
/^(?:(?:10|127)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)|192\.168\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d)|172\.(?:1[6-9]|2\d|3[0-1])\.(?:25[0-5]|2[0-4]\d|1?\d?\d)\.(?:25[0-5]|2[0-4]\d|1?\d?\d))$/;
|
||||
@@ -22,11 +33,12 @@
|
||||
closable: true
|
||||
});
|
||||
|
||||
let text_inputs_valid = $state({
|
||||
const text_inputs_valid_null_values = {
|
||||
name: { valid: false, value: '' },
|
||||
ip: { valid: false, value: '' },
|
||||
mac: { valid: false, value: '' }
|
||||
});
|
||||
};
|
||||
let text_inputs_valid = $state(text_inputs_valid_null_values);
|
||||
|
||||
function all_text_inputs_valid(): boolean {
|
||||
for (const entry of Object.values(text_inputs_valid)) {
|
||||
@@ -37,17 +49,17 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
function finalize_add_new_display() {
|
||||
async function finalize_add_edit_display(existing_display_id: string | null) {
|
||||
const ip = text_inputs_valid.ip.value;
|
||||
const mac = text_inputs_valid.mac.value === '' ? null : text_inputs_valid.mac.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 {
|
||||
const status = await ping_ip(text_inputs_valid.ip.value);
|
||||
add_display(ip, mac, name, status);
|
||||
}
|
||||
popup_close_function();
|
||||
text_inputs_valid = {
|
||||
name: { valid: false, value: '' },
|
||||
ip: { valid: false, value: '' },
|
||||
mac: { valid: false, value: '' }
|
||||
};
|
||||
}
|
||||
|
||||
function popup_close_function() {
|
||||
@@ -55,28 +67,85 @@
|
||||
}
|
||||
|
||||
const show_new_display_popup = () => {
|
||||
text_inputs_valid = text_inputs_valid_null_values;
|
||||
popup_content = {
|
||||
open: true,
|
||||
snippet: add_new_display,
|
||||
snippet: add_new_display_popup,
|
||||
title: 'Neuen Bildschirm hinzufügen',
|
||||
title_icon: Monitor,
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
on_start();
|
||||
});
|
||||
</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
|
||||
bind:current_value={text_inputs_valid.name.value}
|
||||
bind:current_valid={text_inputs_valid.name.valid}
|
||||
title="Anzeigename"
|
||||
placeholder="z.B. Beamer vorne links"
|
||||
is_valid_function={(input: string) => {
|
||||
return input.length === 0 || input.length > 50
|
||||
? [false, 'Ungültige Länge']
|
||||
: is_display_name_taken(input)
|
||||
? [false, 'Name bereits verwendet']
|
||||
: [true, 'Gültiger Name'];
|
||||
if (!!existing_display_id) {
|
||||
if (input === get_display_by_id(existing_display_id, $displays)?.name)
|
||||
return [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">
|
||||
@@ -93,8 +162,18 @@
|
||||
className="grow"
|
||||
/>
|
||||
<div class="flex items-end shrink-0">
|
||||
<Button disabled={!text_inputs_valid.ip.valid} className="px-4 gap-2" bg="bg-stone-750"
|
||||
><Radio /> Ping</Button
|
||||
<Button
|
||||
disabled={!text_inputs_valid.ip.valid}
|
||||
className="px-4 gap-2"
|
||||
bg="bg-stone-750"
|
||||
click_function={async () => {
|
||||
const status = await ping_ip(text_inputs_valid.ip.value);
|
||||
notifications.push(
|
||||
'info',
|
||||
`Ping '${text_inputs_valid.ip.value}'`,
|
||||
`Aktueller Zustand: ${display_status_to_info(status)}`
|
||||
);
|
||||
}}><Radio /> Ping</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,13 +190,24 @@
|
||||
: [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
|
||||
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"
|
||||
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>
|
||||
{/snippet}
|
||||
|
||||
@@ -146,7 +236,10 @@
|
||||
</Button>
|
||||
</div>
|
||||
<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
|
||||
class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2"
|
||||
>
|
||||
|
||||
@@ -213,5 +213,8 @@ module.exports = {
|
||||
|
||||
'hover:bg-red-400',
|
||||
'active:bg-red-500',
|
||||
|
||||
'hover:bg-stone-600/70',
|
||||
'active:bg-stone-500/80',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import type { FolderElement } from "./types";
|
||||
import { notifications } from "./stores/notification";
|
||||
import { to_display_status, type DisplayStatus, type FolderElement } from "./types";
|
||||
import { get_uuid } from "./utils";
|
||||
|
||||
export async function get_screenshot(ip: string) {
|
||||
const options = { method: 'PATCH' };
|
||||
return await request(ip, '/takeScreenshot', options);
|
||||
return await request_display(ip, '/takeScreenshot', options);
|
||||
}
|
||||
|
||||
export async function open_file(ip: string, path_to_file: string) {
|
||||
export async function open_file(ip: string, path_to_file: string): Promise<boolean> {
|
||||
const options = { method: 'PATCH', headers: { 'content-type': 'application/octet-stream' } };
|
||||
const raw_response = await request(ip, `/file${path_to_file}`, options);
|
||||
const raw_response = await request_display(ip, `/file${path_to_file}`, options);
|
||||
return !!raw_response;
|
||||
}
|
||||
|
||||
export async function get_file_data(ip: string, path: string): Promise<FolderElement[]> {
|
||||
@@ -34,7 +36,8 @@ export async function get_file_data(ip: string, path: string): Promise<FolderEle
|
||||
done
|
||||
` })
|
||||
};
|
||||
const raw_response = await request(ip, '/shellCommand', options);
|
||||
const raw_response = await request_display(ip, '/shellCommand', options);
|
||||
if (!raw_response) return [];
|
||||
const response: FileInfo[] = raw_response.stdout.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
@@ -62,22 +65,53 @@ done
|
||||
|
||||
|
||||
|
||||
export async function ping_ip(ip: string): Promise<DisplayStatus> {
|
||||
const raw_response = await request_control(`/ping?ip=${ip}`, { method: 'GET' });
|
||||
if (!raw_response) return null;
|
||||
return raw_response.status ? to_display_status(raw_response.status) : null;
|
||||
}
|
||||
|
||||
async function request(ip: string, api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }) {
|
||||
|
||||
|
||||
|
||||
async function request_display(ip: string, api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }): Promise<null | any> {
|
||||
const url = `http://${ip}:1323/api${api_route}`;
|
||||
return await raw_request(url, options);
|
||||
}
|
||||
|
||||
async function request_control(api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }): Promise<null | any> {
|
||||
const url = `${window.location.origin}/api${api_route}`;
|
||||
return await raw_request(url, options);
|
||||
}
|
||||
|
||||
|
||||
async function raw_request(url: string, options: { method: string, headers?: Record<string, string>, body?: any }): Promise<null | any> {
|
||||
try {
|
||||
const url = `http://${ip}:1323/api${api_route}?t=${Date.now()}`;
|
||||
console.log(url)
|
||||
const response = await fetch(url, options);
|
||||
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
|
||||
console.log(url + cache_buster)
|
||||
const response = await fetch(url + cache_buster, options);
|
||||
if (!response.ok) {
|
||||
console.error(`HTTP error! Status: ${response.status}`);
|
||||
notifications.push("error", `HTTP-Fehler bei API-Anfrage`, `${url}\nHTTP-Status: ${response.status}`);
|
||||
}
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
if (!contentType.includes("application/json")) {
|
||||
return await response.blob();
|
||||
} else {
|
||||
return await response.json();
|
||||
const json = await response.json();
|
||||
if (json.error && json.error !== '') {
|
||||
notifications.push("error", `Interner Fehler bei API-Anfrage`, `${url}\nJSON: ${JSON.stringify(json)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return json;
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error instanceof TypeError && /fetch|NetworkError/i.test(error.message)) {
|
||||
console.log("Request failed - Is the targeted device online?")
|
||||
} else {
|
||||
console.log(typeof error, error instanceof TypeError, error.message);
|
||||
console.error(error);
|
||||
notifications.push("error", `Fehler bei API-Anfrage`, `${url}\nFehler: ${error}`);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,23 @@
|
||||
export function on_start() {
|
||||
import { get } from "svelte/store";
|
||||
import { displays, update_displays_with_map } from "./stores/displays"
|
||||
import { ping_ip } from "./api_handler";
|
||||
import type { Display } from "./types";
|
||||
import { change_file_path, update_all_display_files } from "./stores/files";
|
||||
|
||||
const update_display_status_interval_seconds = 20;
|
||||
|
||||
export function on_start() {
|
||||
update_all_display_status();
|
||||
setInterval(update_all_display_status, update_display_status_interval_seconds * 1000);
|
||||
update_all_display_files('/');
|
||||
}
|
||||
|
||||
|
||||
async function update_all_display_status() {
|
||||
update_displays_with_map(async (display: Display) => {
|
||||
const new_status = await ping_ip(display.ip);
|
||||
if (new_status === null && display.status !== null) return display;
|
||||
return { ...display, status: new_status, };
|
||||
});
|
||||
console.log("Display Status updated")
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import type { Display, DisplayGroup } from "../types";
|
||||
import type { Display, DisplayGroup, DisplayStatus } from "../types";
|
||||
import { is_selected, select, selected_display_ids } from "./select";
|
||||
import { get_uuid, image_content_hash } from "../utils";
|
||||
import { get_screenshot } from "../api_handler";
|
||||
import DisplayGroupObject from "../../components/DisplayGroupObject.svelte";
|
||||
|
||||
export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
|
||||
id: get_uuid(),
|
||||
@@ -17,13 +18,33 @@ export function is_display_name_taken(name: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
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: DisplayStatus) {
|
||||
displays.update((displays: DisplayGroup[]) => {
|
||||
displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status });
|
||||
return displays;
|
||||
});
|
||||
}
|
||||
|
||||
export function edit_display_data(display_id: string, ip: string, mac: string | null, name: string) {
|
||||
update_displays_with_map((display: 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[]) {
|
||||
if (display_group.data.length === 0) return false;
|
||||
for (const display of display_group.data) {
|
||||
@@ -112,23 +133,29 @@ export async function update_screenshot(display_id: string, check_type: "first_c
|
||||
}
|
||||
|
||||
if (update_needed) {
|
||||
displays.update((display_groups) =>
|
||||
display_groups.map((group) => ({
|
||||
...group,
|
||||
data: group.data.map((display) => {
|
||||
update_displays_with_map((display: Display) => {
|
||||
if (display.id !== display_id) return display;
|
||||
if (display.preview_url) {
|
||||
URL.revokeObjectURL(display.preview_url);
|
||||
}
|
||||
const new_url = URL.createObjectURL(new_blob);
|
||||
return { ...display, preview_url: new_url, preview_timeout_id: new_preview_timeout_id };
|
||||
}),
|
||||
}))
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function update_displays_with_map(update_function: (display: Display) => Display | Promise<Display>) {
|
||||
const display_groups = get(displays);
|
||||
const updated_groups = await Promise.all(
|
||||
display_groups.map(async (group: DisplayGroup) => ({
|
||||
...group,
|
||||
data: await Promise.all(group.data.map(update_function)),
|
||||
}))
|
||||
);
|
||||
displays.set(updated_groups);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -139,6 +166,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", "PC", "Offline");
|
||||
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "Online");
|
||||
add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "host_offline");
|
||||
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "host_offline");
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import type { FolderElement } from "../types";
|
||||
import { displays } from "./displays";
|
||||
import type { Display, FolderElement } from "../types";
|
||||
import { displays, update_displays_with_map } from "./displays";
|
||||
import { selected_display_ids, selected_file_ids } from "./select";
|
||||
import { get_file_data } from "../api_handler";
|
||||
import { get_uuid } from "../utils";
|
||||
import type { Folder } from "lucide-svelte";
|
||||
import { notifications } from "./notification";
|
||||
|
||||
export const all_files: Writable<Record<string, Record<string, FolderElement[]>>> = writable<Record<string, Record<string, FolderElement[]>>>({});
|
||||
// {
|
||||
@@ -28,6 +30,7 @@ export function change_file_path(new_path: string) {
|
||||
selected_file_ids.update(() => {
|
||||
return [];
|
||||
})
|
||||
setTimeout(async () => { await update_all_display_files(new_path) }, 0);
|
||||
}
|
||||
|
||||
export function get_display_ids_where_file_is_missing(path: string, file: FolderElement, selected_display_ids: string[], all_files: Record<string, Record<string, FolderElement[]>>): string[][] {
|
||||
@@ -53,36 +56,111 @@ export function get_display_ids_where_file_is_missing(path: string, file: Folder
|
||||
return [missing, colliding];
|
||||
}
|
||||
|
||||
export async function update_all_display_files(path: string) {
|
||||
for (const display_group of get(displays)) {
|
||||
for (const display of display_group.data) {
|
||||
await update_folder_elements_recursively(display, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function updates_files_on_display(display_id: string, new_folder_elements: FolderElement[], file_path: string) {
|
||||
all_files.update((files) => {
|
||||
async function update_folder_elements_recursively(display: Display, file_path: string = '/'): Promise<number> {
|
||||
const new_folder_elements = await get_file_data(display.ip, file_path);
|
||||
all_files.update((files: Record<string, Record<string, FolderElement[]>>) => {
|
||||
if (!files.hasOwnProperty(file_path)) {
|
||||
files[file_path] = {};
|
||||
}
|
||||
for (const new_folder_element of new_folder_elements) {
|
||||
new_folder_element.id = get_uuid();
|
||||
}
|
||||
files[file_path][display_id] = new_folder_elements;
|
||||
return files;
|
||||
});
|
||||
if (!files[file_path].hasOwnProperty(display.id)) {
|
||||
files[file_path][display.id] = [];
|
||||
}
|
||||
|
||||
async function get_files_on_all_displays(current_file_path: string) {
|
||||
for (const display_group of get(displays)) {
|
||||
for (const display of display_group.data) {
|
||||
const new_folder_elements = await get_file_data(display.ip, current_file_path);
|
||||
updates_files_on_display(display.id, new_folder_elements, current_file_path)
|
||||
console.log(new_folder_elements)
|
||||
const existing_folder_elements = files[file_path].hasOwnProperty(display.id) ? files[file_path][display.id] : [];
|
||||
|
||||
const diff = get_folder_elements_difference(existing_folder_elements, new_folder_elements);
|
||||
files[file_path][display.id].push(...diff.new);
|
||||
return remove_folder_elements_recursively(files, display, diff.deleted, file_path);
|
||||
})
|
||||
|
||||
let folder_size = 0;
|
||||
for (const element of new_folder_elements) {
|
||||
if (element.type === 'inode/directory') {
|
||||
const folder_content_size = await update_folder_elements_recursively(display, file_path + element.name + '/');
|
||||
folder_size += folder_content_size;
|
||||
// Update foldersize
|
||||
all_files.update((files: Record<string, Record<string, FolderElement[]>>) => {
|
||||
for (const current_folder_element of files[file_path][display.id]) {
|
||||
if (current_folder_element.id === element.id) {
|
||||
current_folder_element.size = folder_content_size;
|
||||
}
|
||||
}
|
||||
return files;
|
||||
})
|
||||
} else {
|
||||
folder_size += element.size;
|
||||
}
|
||||
}
|
||||
return folder_size;
|
||||
}
|
||||
|
||||
function remove_folder_elements_recursively(files: Record<string, Record<string, FolderElement[]>>, display: Display, folder_elements: FolderElement[], file_path: string): Record<string, Record<string, FolderElement[]>> {
|
||||
if (!files.hasOwnProperty(file_path) || !files[file_path].hasOwnProperty(display.id)) {
|
||||
console.error("File remove path does not exist:", files, display, folder_elements, file_path);
|
||||
notifications.push("error", "Fehler beim Aktualisieren der Dateien", `File remove path does not exist: ${file_path} display_ip: ${display.ip}`);
|
||||
return {};
|
||||
}
|
||||
for (const folder_element of folder_elements) {
|
||||
files[file_path][display.id] = files[file_path][display.id].filter((f) => f.id !== folder_element.id);
|
||||
|
||||
if (folder_element.type === 'inode/directory') {
|
||||
const new_file_path = file_path + folder_element.name + '/';
|
||||
if (!files.hasOwnProperty(new_file_path) || !files[new_file_path].hasOwnProperty(display.id)) {
|
||||
console.error("File remove path does not exist (recursion):", files, display, folder_elements, file_path, new_file_path);
|
||||
notifications.push("error", "Fehler beim Aktualisieren der Dateien", `File remove path does not exist (recursion): ${new_file_path} display_ip: ${display.ip}`);
|
||||
return {};
|
||||
}
|
||||
const sub_folder = files[new_file_path][display.id];
|
||||
remove_folder_elements_recursively(files, display, sub_folder, new_file_path);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
function get_folder_elements_difference(old_elements: FolderElement[], new_elements: FolderElement[]): { deleted: FolderElement[], new: FolderElement[] } {
|
||||
const old_hashes = new Set(old_elements.map(e => e.hash));
|
||||
const new_hashes = new Set(new_elements.map(e => e.hash));
|
||||
|
||||
const only_in_old = old_elements.filter(e => !new_hashes.has(e.hash));
|
||||
const only_in_new = new_elements.filter(e => !old_hashes.has(e.hash));
|
||||
return { deleted: only_in_old, new: only_in_new };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// export function updates_files_on_display(display_id: string, new_folder_elements: FolderElement[], file_path: string) {
|
||||
// all_files.update((files) => {
|
||||
// if (!files.hasOwnProperty(file_path)) {
|
||||
// files[file_path] = {};
|
||||
// }
|
||||
// files[file_path][display_id] = new_folder_elements;
|
||||
// return files;
|
||||
// });
|
||||
// }
|
||||
|
||||
// async function get_files_on_all_displays(current_file_path: string) {
|
||||
// for (const display_group of get(displays)) {
|
||||
// for (const display of display_group.data) {
|
||||
// const new_folder_elements = await get_file_data(display.ip, current_file_path);
|
||||
// updates_files_on_display(display.id, new_folder_elements, current_file_path)
|
||||
// console.log(new_folder_elements)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
export function get_current_folder_elements(all_files: Record<string, Record<string, FolderElement[]>>, current_file_path: string, selected_display_ids: string[]) {
|
||||
if (!all_files.hasOwnProperty(current_file_path)) {
|
||||
setTimeout(async () => { await get_files_on_all_displays(current_file_path) }, 0);
|
||||
return [];
|
||||
}
|
||||
if (!all_files.hasOwnProperty(current_file_path)) return [];
|
||||
|
||||
const files_on_display_array = all_files[current_file_path];
|
||||
const files: FolderElement[] = [];
|
||||
for (const key of Object.keys(files_on_display_array)) {
|
||||
@@ -123,120 +201,3 @@ function sort_files(files: FolderElement[]) {
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
add_test_files();
|
||||
export function add_test_files() {
|
||||
// updates_files_on_display(get(displays)[0].data[0].id, [
|
||||
// {
|
||||
// hash: "ok",
|
||||
// thumbnail: null,
|
||||
// name: "ok.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2022-09-10"),
|
||||
// size: "32 KB"
|
||||
// },
|
||||
// {
|
||||
// hash: "schön",
|
||||
// thumbnail: null,
|
||||
// name: "schön.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2023-09-10"),
|
||||
// size: "2 GB"
|
||||
// },
|
||||
// {
|
||||
// hash: "lang",
|
||||
// thumbnail: null,
|
||||
// name: "langer Bildname der wirklich gar nicht mehr aufhört, sodass man mal gut testen kann, wie die UI darauf reagiert.odp",
|
||||
// type: "application/vnd.oasis.opendocument.presentation",
|
||||
// date_created: new Date("2028-09-10"),
|
||||
// size: "324 KB"
|
||||
// },
|
||||
// {
|
||||
// hash: "Schön hier aber waren Sie mal in Baden Würtemberg?",
|
||||
// thumbnail: null,
|
||||
// name: "Schön hier aber waren Sie mal in Baden Würtemberg?.mp4",
|
||||
// type: "video/mp4",
|
||||
// date_created: new Date("2024-09-10"),
|
||||
// size: "32 KB"
|
||||
// },
|
||||
// {
|
||||
// hash: "lan4234g",
|
||||
// thumbnail: null,
|
||||
// name: "Ein schöner Ordner",
|
||||
// type: 'inode/directory',
|
||||
// date_created: new Date("2025-01-02"),
|
||||
// size: "324 TB"
|
||||
// },
|
||||
// {
|
||||
// hash: "Schön hi23424er aber waren Sie mal in Baden Würtemberg?",
|
||||
// thumbnail: null,
|
||||
// name: "Ein hässlicher Ordner",
|
||||
// type: 'inode/directory',
|
||||
// date_created: new Date("2025-10-22"),
|
||||
// size: "1 B"
|
||||
// },
|
||||
// ], '/');
|
||||
|
||||
|
||||
// updates_files_on_display(get(displays)[0].data[0].id, [
|
||||
// {
|
||||
// hash: "ok",
|
||||
// thumbnail: null,
|
||||
// name: "ok.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2022-09-10"),
|
||||
// size: "32 KB"
|
||||
// },
|
||||
// {
|
||||
// hash: "schön",
|
||||
// thumbnail: null,
|
||||
// name: "schön.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2023-09-10"),
|
||||
// size: "2 GB"
|
||||
// },
|
||||
// ], '/Ein hässlicher Ordner/');
|
||||
|
||||
// updates_files_on_display(get(displays)[0].data[0].id, [
|
||||
// {
|
||||
// hash: "nö",
|
||||
// thumbnail: null,
|
||||
// name: "nö.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2022-09-10"),
|
||||
// size: "32 KB"
|
||||
// },
|
||||
// {
|
||||
// hash: "na gut",
|
||||
// thumbnail: null,
|
||||
// name: "na gut.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2023-09-10"),
|
||||
// size: "2 GB"
|
||||
// },
|
||||
// ], '/Ein schöner Ordner/');
|
||||
|
||||
|
||||
|
||||
// updates_files_on_display(get(displays)[0].data[1].id, [
|
||||
// {
|
||||
// hash: "okk",
|
||||
// thumbnail: null,
|
||||
// name: "ok.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2022-09-10"),
|
||||
// size: "32 KB"
|
||||
// },
|
||||
// {
|
||||
// hash: "schön",
|
||||
// thumbnail: null,
|
||||
// name: "schön.png",
|
||||
// type: "image/png",
|
||||
// date_created: new Date("2023-09-10"),
|
||||
// size: "2 GB"
|
||||
// },], '/');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
|
||||
export type Notification = {
|
||||
id: number;
|
||||
title: string;
|
||||
message: string;
|
||||
duration: number;
|
||||
className: string;
|
||||
type?: "error" | "success" | "info";
|
||||
};
|
||||
|
||||
function createNotifications() {
|
||||
const { subscribe, update } = writable<Notification[]>([]);
|
||||
|
||||
function push(type: "error" | "success" | "info", title: string, message: string = "", className: string = "") {
|
||||
const id = Date.now();
|
||||
const duration = type === "error" ? 8000 : 4000;
|
||||
update((n) => [...n, { id, title, message, duration, className, type }]);
|
||||
setTimeout(() => {
|
||||
update((n) => n.filter((x) => x.id !== id));
|
||||
}, duration);
|
||||
}
|
||||
const remove = (id: number) => update(n => n.filter(x => x.id !== id));
|
||||
|
||||
return { subscribe, push, remove };
|
||||
}
|
||||
|
||||
export const notifications = createNotifications();
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FileBox, FileImage, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
export type SupportedFileType = {
|
||||
display_name:
|
||||
@@ -62,7 +63,7 @@ export type Display = {
|
||||
preview_timeout_id: number | null;
|
||||
mac: string | null;
|
||||
name: string;
|
||||
status: string;
|
||||
status: DisplayStatus;
|
||||
}
|
||||
|
||||
export type DisplayGroup = {
|
||||
@@ -81,9 +82,31 @@ export type MenuOption = {
|
||||
|
||||
export type PopupContent = {
|
||||
open: boolean;
|
||||
snippet: any;
|
||||
snippet: Snippet<[string]> | null;
|
||||
snippet_arg?: string;
|
||||
title: string;
|
||||
title_class?: string;
|
||||
title_icon?: typeof X | null;
|
||||
closable?: boolean;
|
||||
}
|
||||
|
||||
export type DisplayStatus = "host_offline" | "app_offline" | "app_online" | null;
|
||||
|
||||
export function to_display_status(value: string): DisplayStatus {
|
||||
return ["host_offline", "app_offline", "app_online"].includes(value)
|
||||
? (value as DisplayStatus)
|
||||
: null;
|
||||
}
|
||||
|
||||
export function display_status_to_info(status: DisplayStatus): string {
|
||||
switch (status) {
|
||||
case 'app_online':
|
||||
return 'Online';
|
||||
case 'app_offline':
|
||||
return 'Lädt';
|
||||
case 'host_offline':
|
||||
return 'Offline';
|
||||
case null:
|
||||
return '???';
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -2,9 +2,7 @@ module plg-mudics/control
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
)
|
||||
require github.com/labstack/echo/v4 v4.13.4
|
||||
|
||||
require (
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
@@ -6,6 +8,10 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
@@ -21,3 +27,5 @@ golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+36
-4
@@ -2,8 +2,12 @@ package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"plg-mudics/control/frontend"
|
||||
"plg-mudics/shared"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
@@ -20,12 +24,40 @@ func main() {
|
||||
|
||||
// Servers all API endpoints, e.g. our custom logic
|
||||
apiGroup := e.Group("/api")
|
||||
apiGroup.GET("/ping", func(ctx echo.Context) error {
|
||||
return ctx.String(http.StatusOK, "pong")
|
||||
})
|
||||
apiGroup.GET("/ping", pingRoute)
|
||||
|
||||
err := e.Start(":1323")
|
||||
port := "8080"
|
||||
|
||||
go shared.OpenBrowserWindow("http://localhost:"+port, false, false)
|
||||
|
||||
err := e.Start(":" + port)
|
||||
if err != nil {
|
||||
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
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
|
||||
## Development Setup
|
||||
|
||||
- install Go (at least version v1.24)
|
||||
- build template: `go tool templ generate`
|
||||
- install Go (at least version v1.25)
|
||||
- build template: `go generate ./..`
|
||||
- run code: `go run .`
|
||||
|
||||
+9
-8
@@ -3,13 +3,13 @@ module plg-mudics/display
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/a-h/templ v0.3.960
|
||||
github.com/labstack/echo/v4 v4.13.4
|
||||
github.com/micmonay/keybd_event v1.1.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e // indirect
|
||||
github.com/a-h/templ v0.3.924 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cli/browser v1.3.0 // indirect
|
||||
@@ -21,13 +21,14 @@ require (
|
||||
github.com/natefinch/atomic v1.0.1 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.40.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/tools v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.40.0 // indirect
|
||||
golang.org/x/mod v0.26.0 // indirect
|
||||
golang.org/x/net v0.42.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
)
|
||||
|
||||
tool github.com/a-h/templ/cmd/templ
|
||||
|
||||
+20
-16
@@ -1,7 +1,7 @@
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.924 h1:t5gZqTneXqvehpNZsgtnlOscnBboNh9aASBH2MgV/0k=
|
||||
github.com/a-h/templ v0.3.924/go.mod h1:FFAu4dI//ESmEN7PQkJ7E7QfnSEMdcnu7QrAY8Dn334=
|
||||
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
@@ -14,6 +14,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA=
|
||||
github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
@@ -34,20 +36,22 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
|
||||
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
+4
-3
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"plg-mudics/display/pkg"
|
||||
"plg-mudics/display/web"
|
||||
"plg-mudics/shared"
|
||||
)
|
||||
|
||||
const VERSION = "0.1.0"
|
||||
@@ -22,10 +23,10 @@ func main() {
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
port := "1323"
|
||||
go web.StartWebServer(VERSION, port)
|
||||
|
||||
go web.StartWebServer(VERSION)
|
||||
|
||||
err = pkg.OpenBrowserWindow("http://127.0.0.1:1323")
|
||||
err = shared.OpenBrowserWindow("http://localhost:"+port, true, true)
|
||||
if err != nil {
|
||||
slog.Error("Failed to open browser window", "error", err)
|
||||
os.Exit(1)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"plg-mudics/shared"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -36,7 +37,7 @@ func GenerateFilePreview(inputPath string) (string, error) {
|
||||
|
||||
func generateImagePreview(inputPath string, outputPath string) error {
|
||||
cmd := exec.Command("magick", inputPath, "-thumbnail", "100x100", "-quality", "50", outputPath)
|
||||
result := RunShellCommand(cmd)
|
||||
result := shared.RunShellCommand(cmd)
|
||||
if result.ExitCode != 0 {
|
||||
if result.ExitCode == 127 {
|
||||
return ErrFilePreviewToolsMissing
|
||||
@@ -48,12 +49,12 @@ func generateImagePreview(inputPath string, outputPath string) error {
|
||||
|
||||
func generatePDFPreview(inputPath string, outputPath string) error {
|
||||
testCmd := exec.Command("which", "gs")
|
||||
if result := RunShellCommand(testCmd); result.ExitCode != 0 {
|
||||
if result := shared.RunShellCommand(testCmd); result.ExitCode != 0 {
|
||||
return ErrFilePreviewToolsMissing
|
||||
}
|
||||
|
||||
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 == 127 {
|
||||
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()))
|
||||
|
||||
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 == 127 {
|
||||
return ErrFilePreviewToolsMissing
|
||||
|
||||
+5
-47
@@ -1,7 +1,6 @@
|
||||
package pkg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -12,14 +11,12 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"plg-mudics/shared"
|
||||
|
||||
"github.com/micmonay/keybd_event"
|
||||
)
|
||||
|
||||
type CommandResponse struct {
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exitCode"`
|
||||
}
|
||||
|
||||
|
||||
func GetDeviceIp() (string, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
@@ -54,7 +51,7 @@ func GetDeviceMac() (string, error) {
|
||||
|
||||
func OpenPresentation(path string) error {
|
||||
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 {
|
||||
return errors.New(result.Stderr)
|
||||
}
|
||||
@@ -84,7 +81,7 @@ func TakeScreenshot() (string, error) {
|
||||
exec.Command("spectacle", "--fullscreen", "--nonotify", "--background", "--output", tempFilePath),
|
||||
}
|
||||
for _, cmd := range cmds {
|
||||
commandOutput := RunShellCommand(cmd)
|
||||
commandOutput := shared.RunShellCommand(cmd)
|
||||
if commandOutput.ExitCode == 0 {
|
||||
return tempFilePath, nil
|
||||
}
|
||||
@@ -93,45 +90,6 @@ func TakeScreenshot() (string, error) {
|
||||
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) {
|
||||
var storagePath string
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"version": "1",
|
||||
"name": "PLG-MuDICS",
|
||||
"name": "PLG-MuDiCS Display",
|
||||
"type": "collection",
|
||||
"ignore": ["node_modules", ".git"]
|
||||
"ignore": [
|
||||
"node_modules",
|
||||
".git"
|
||||
]
|
||||
}
|
||||
+3
-3
@@ -37,7 +37,7 @@ var supportedExtensions = map[string]bool{
|
||||
".odp": true,
|
||||
}
|
||||
|
||||
func StartWebServer(v string) {
|
||||
func StartWebServer(v string, port string) {
|
||||
version = v
|
||||
|
||||
e := echo.New()
|
||||
@@ -63,7 +63,7 @@ func StartWebServer(v string) {
|
||||
fileGroup.PATCH("/:path", openFileRoute)
|
||||
fileGroup.GET("/preview/:path", previewRoute)
|
||||
|
||||
err := e.Start(":1323")
|
||||
err := e.Start(":" + port)
|
||||
if err != nil {
|
||||
slog.Error("Failed to start server", "error", err)
|
||||
}
|
||||
@@ -155,7 +155,7 @@ func shellCommandRoute(ctx echo.Context) error {
|
||||
}
|
||||
cmd.Dir = storagePath
|
||||
|
||||
commandOutput := pkg.RunShellCommand(cmd)
|
||||
commandOutput := shared.RunShellCommand(cmd)
|
||||
if commandOutput.ExitCode != 0 {
|
||||
slog.Error("Shell command execution error", "error", commandOutput.Stderr)
|
||||
}
|
||||
|
||||
+18
-1
@@ -1,7 +1,24 @@
|
||||
github.com/a-h/htmlformat v0.0.0-20250209131833-673be874c677/go.mod h1:FMIm5afKmEfarNbIXOaPHFY8X7fo+fRQB6I9MPG2nB0=
|
||||
github.com/a-h/templ v0.3.960 h1:trshEpGa8clF5cdI39iY4ZrZG8Z/QixyzEyUnA7feTM=
|
||||
github.com/a-h/templ v0.3.960/go.mod h1:oCZcnKRf5jjsGpf2yELzQfodLphd2mwecwG4Crk5HBo=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
|
||||
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
|
||||
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
|
||||
golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
|
||||
golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
|
||||
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0=
|
||||
golang.org/x/telemetry v0.0.0-20250710130107-8d8967aff50b/go.mod h1:4ZwOYna0/zsOKwuR5X/m0QFOJpSZvAxFfkQT+Erd9D4=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0=
|
||||
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
|
||||
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
|
||||
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
|
||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed splash_screen.html
|
||||
var SplashScreenTemplate string
|
||||
+33
-3
@@ -1,8 +1,38 @@
|
||||
package shared
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"bytes"
|
||||
"errors"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
//go:embed splash_screen.html
|
||||
var SplashScreenTemplate string
|
||||
type CommandResponse struct {
|
||||
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,37 @@
|
||||
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)
|
||||
}
|
||||
|
||||
errs := []string{}
|
||||
for _, bin := range bins {
|
||||
cmd := exec.Command(bin, args...)
|
||||
commandOutput := RunShellCommand(cmd)
|
||||
if commandOutput.ExitCode == 0 {
|
||||
return nil
|
||||
}
|
||||
errs = append(errs, commandOutput.Stderr)
|
||||
}
|
||||
|
||||
return errors.New("failed to open browser window: " + fmt.Sprint(errs))
|
||||
}
|
||||
Reference in New Issue
Block a user