Nice Work
This commit is contained in:
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
Vendored
-3
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ensureSchema } from './index.js';
|
||||
|
||||
ensureSchema()
|
||||
.then(() => {
|
||||
console.log('✅ Datenbankschema abgeglichen.');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('⚠️ Schema-Abgleich fehlgeschlagen:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const dbPath = path.join(__dirname, 'klangkiste.sqlite');
|
||||
const schemaPath = path.join(__dirname, 'schema.sql');
|
||||
|
||||
export const db = new sqlite3.Database(dbPath);
|
||||
|
||||
db.exec('PRAGMA foreign_keys = ON;');
|
||||
|
||||
export function run(query, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(query, params, function onRun(error) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve({ changes: this.changes, lastID: this.lastID });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function get(query, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(query, params, (error, row) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(row || null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function all(query, params = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.all(query, params, (error, rows) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(rows || []);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureSchema() {
|
||||
const schema = fs.readFileSync(schemaPath, 'utf-8');
|
||||
return new Promise((resolve, reject) => {
|
||||
db.exec(schema, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,65 @@
|
||||
PRAGMA journal_mode = WAL;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS boxes (
|
||||
box_id TEXT PRIMARY KEY,
|
||||
fingerprint TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
first_seen INTEGER NOT NULL,
|
||||
last_seen INTEGER NOT NULL,
|
||||
firmware_version TEXT NOT NULL,
|
||||
capabilities_json TEXT NOT NULL,
|
||||
api_token TEXT,
|
||||
last_ip TEXT,
|
||||
api_port INTEGER,
|
||||
setup_port INTEGER,
|
||||
alias TEXT,
|
||||
media_root TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tags (
|
||||
uid TEXT PRIMARY KEY,
|
||||
label TEXT,
|
||||
alias TEXT,
|
||||
status TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
written_at INTEGER,
|
||||
media_path TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS box_tags (
|
||||
box_id TEXT NOT NULL,
|
||||
tag_uid TEXT NOT NULL,
|
||||
media_path TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (box_id, tag_uid),
|
||||
FOREIGN KEY (box_id) REFERENCES boxes(box_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_uid) REFERENCES tags(uid) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS box_tag_blocks (
|
||||
box_id TEXT NOT NULL,
|
||||
tag_uid TEXT NOT NULL,
|
||||
created_at INTEGER NOT NULL,
|
||||
PRIMARY KEY (box_id, tag_uid),
|
||||
FOREIGN KEY (box_id) REFERENCES boxes(box_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (tag_uid) REFERENCES tags(uid) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS box_jobs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
box_id TEXT NOT NULL,
|
||||
job_type TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
attempts INTEGER NOT NULL,
|
||||
last_error TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
FOREIGN KEY (box_id) REFERENCES boxes(box_id) ON DELETE CASCADE
|
||||
);
|
||||
Generated
+2671
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "klangkiste-backend",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node server.js",
|
||||
"db:ensure": "node db/ensure.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "2.8.5",
|
||||
"express": "4.19.2",
|
||||
"music-metadata": "7.14.0",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"sqlite3": "5.1.7"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Klangkiste Control Panel</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+2743
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "klangkiste-frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview --host 0.0.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@phosphor-icons/react": "2.1.7",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "4.3.1",
|
||||
"vite": "5.4.3"
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,239 @@
|
||||
const defaultHost =
|
||||
typeof window !== 'undefined' && window.location ? window.location.hostname : '127.0.0.1';
|
||||
const API_BASE = import.meta.env.VITE_BACKEND_URL || `http://${defaultHost}:5001`;
|
||||
|
||||
async function readJson(response) {
|
||||
const text = await response.text();
|
||||
try {
|
||||
return { ok: response.ok, status: response.status, data: JSON.parse(text) };
|
||||
} catch (error) {
|
||||
return { ok: response.ok, status: response.status, data: { detail: text } };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBoxes() {
|
||||
const response = await fetch(`${API_BASE}/api/boxes`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function pairBox(boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/pair`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ box_id: boxId }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function getStatus(boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/status`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function getMediaTree() {
|
||||
const response = await fetch(`${API_BASE}/api/media-tree`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function createMediaFolder(parentPath, name) {
|
||||
const response = await fetch(`${API_BASE}/api/media/folder`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ parent_path: parentPath, name }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function renameMedia(path, name) {
|
||||
const response = await fetch(`${API_BASE}/api/media/rename`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, name }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function moveMedia(path, targetParent) {
|
||||
const response = await fetch(`${API_BASE}/api/media/move`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path, target_parent: targetParent }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function deleteMedia(path) {
|
||||
const response = await fetch(`${API_BASE}/api/media/delete`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ path }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export function uploadMedia(targetPath, files, onProgress) {
|
||||
const formData = new FormData();
|
||||
formData.append('target_path', targetPath);
|
||||
files.forEach((file) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const request = new XMLHttpRequest();
|
||||
request.open('POST', `${API_BASE}/api/media/upload`);
|
||||
request.upload.onprogress = (event) => {
|
||||
if (!event.lengthComputable || !onProgress) return;
|
||||
const percent = Math.round((event.loaded / event.total) * 100);
|
||||
onProgress(percent);
|
||||
};
|
||||
request.onload = () => {
|
||||
try {
|
||||
const data = JSON.parse(request.responseText || '{}');
|
||||
resolve({ ok: request.status >= 200 && request.status < 300, status: request.status, data });
|
||||
} catch (error) {
|
||||
resolve({
|
||||
ok: request.status >= 200 && request.status < 300,
|
||||
status: request.status,
|
||||
data: { detail: request.responseText || '' },
|
||||
});
|
||||
}
|
||||
};
|
||||
request.onerror = () => {
|
||||
resolve({ ok: false, status: 0, data: { detail: 'network error' } });
|
||||
};
|
||||
request.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTags() {
|
||||
const response = await fetch(`${API_BASE}/api/tags`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function generateTag(label) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/generate`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ label }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function claimTag(uid, label) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/claim`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uid, label }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function markTagWritten(uid) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/write`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function getBoxTags(boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/tags`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function getBoxLocalTags(boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/local-tags`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function getTagBlocks(boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/tag-blocks`);
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function setTagBlock(boxId, uid, blocked) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/tag-blocks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uid, blocked }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function setBoxAlias(boxId, alias) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/alias`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ alias }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function setTagAlias(uid, alias) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/alias`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ alias }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function assignTag(uid, boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/assign`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ box_id: boxId }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function unassignTag(uid, boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/unassign`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ box_id: boxId }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function deleteTag(uid) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/${uid}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function setTagMedia(uid, mediaPath) {
|
||||
const response = await fetch(`${API_BASE}/api/tags/${uid}/media`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ media_path: mediaPath }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function pullTagFromBox(boxId, uid, targetFolder) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/pull-tag`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uid, target_folder: targetFolder }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function sendCommand(boxId, command, payload = {}) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/command`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ command, payload }),
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
|
||||
export async function unpairBox(boxId) {
|
||||
const response = await fetch(`${API_BASE}/api/boxes/${boxId}/unpair`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
return readJson(response);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.jsx';
|
||||
import './styles.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,826 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
|
||||
color: #131522;
|
||||
background: #f4f2ef;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
min-height: 38px;
|
||||
box-shadow: 0 8px 20px rgba(13, 16, 31, 0.12);
|
||||
background: #ffffff;
|
||||
border: 1px solid #e4e0dd;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
border-color: #b9d9b9;
|
||||
background: #eef7ee;
|
||||
color: #205b20;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
border-color: #f2c9c9;
|
||||
background: #ffefef;
|
||||
color: #9a2a2a;
|
||||
}
|
||||
|
||||
.page {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 24px 48px;
|
||||
}
|
||||
|
||||
header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin: 0;
|
||||
color: #525466;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #ffffff;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
box-shadow: 0 8px 24px rgba(13, 16, 31, 0.08);
|
||||
}
|
||||
|
||||
.page > section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.subheading {
|
||||
margin: 16px 0 8px;
|
||||
font-size: 14px;
|
||||
color: #3b3f46;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border: 1px solid #e4e0dd;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
margin-top: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card.selected {
|
||||
border-color: #d28735;
|
||||
box-shadow: 0 0 0 2px rgba(210, 135, 53, 0.2);
|
||||
}
|
||||
|
||||
.stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.stack-inline {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.box-tag-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e0dd;
|
||||
border-radius: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.box-tag-actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
margin: 8px 0 0;
|
||||
padding-left: 18px;
|
||||
color: #3b3f46;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.file-list li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.tag-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.alias-input {
|
||||
border: 1px solid #dde0e6;
|
||||
border-radius: 8px;
|
||||
padding: 6px 8px;
|
||||
font-size: 12px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.alias-input:focus {
|
||||
outline: none;
|
||||
border-color: #cfd4dc;
|
||||
box-shadow: 0 0 0 2px rgba(47, 52, 66, 0.08);
|
||||
}
|
||||
|
||||
.toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #3b3f46;
|
||||
}
|
||||
|
||||
.toggle input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.tag-matrix {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.matrix-row {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-auto-columns: minmax(120px, 1fr);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.matrix-row.header {
|
||||
font-weight: 600;
|
||||
color: #3b3f46;
|
||||
}
|
||||
|
||||
.matrix-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e6e8ec;
|
||||
border-radius: 10px;
|
||||
background: #fbfbfc;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.matrix-cell.label {
|
||||
font-weight: 600;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 12px;
|
||||
color: #6a6c7a;
|
||||
}
|
||||
|
||||
.pill {
|
||||
background: #f0e2d4;
|
||||
color: #7a4b17;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: #1f243d;
|
||||
color: #ffffff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2c3252;
|
||||
}
|
||||
|
||||
select {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d6d1cc;
|
||||
padding: 8px 10px;
|
||||
min-width: 180px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.button-ghost {
|
||||
background: transparent;
|
||||
color: #1f243d;
|
||||
border: 1px solid #d6d1cc;
|
||||
}
|
||||
|
||||
.button-ghost:hover {
|
||||
background: #f6f2ee;
|
||||
}
|
||||
|
||||
.card.compact {
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
input {
|
||||
border-radius: 8px;
|
||||
border: 1px solid #d6d1cc;
|
||||
padding: 8px 10px;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.status {
|
||||
background: #111522;
|
||||
color: #e8e8ef;
|
||||
padding: 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #ffefef;
|
||||
color: #9a2a2a;
|
||||
border: 1px solid #f2c9c9;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #6a6c7a;
|
||||
}
|
||||
|
||||
.tree {
|
||||
border: 1px solid #e6e1dc;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: #faf7f4;
|
||||
}
|
||||
|
||||
.explorer {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 0;
|
||||
border: 1px solid #e2e4e8;
|
||||
border-radius: 12px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
background: #ffffff;
|
||||
min-height: 200px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.explorer-sidebar {
|
||||
grid-row: 1;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: #ffffff;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar-title {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #8a919d;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
border: 1px solid #e6e8ec;
|
||||
background: #fbfbfc;
|
||||
padding: 10px 12px;
|
||||
min-height: 44px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sidebar-tree {
|
||||
border: 1px solid #e6e8ec;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
padding: 8px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sidebar-search {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
border: 1px solid #dde0e6;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
color: #2f3442;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-search:focus {
|
||||
outline: none;
|
||||
border-color: #cfd4dc;
|
||||
box-shadow: 0 0 0 2px rgba(47, 52, 66, 0.08);
|
||||
}
|
||||
|
||||
.explorer-toolbar,
|
||||
.sidebar-toolbar {
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
box-shadow: 0 1px 2px rgba(16, 18, 22, 0.06);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.explorer-actions.footer {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.explorer-main {
|
||||
grid-row: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 12px 14px 14px;
|
||||
background: #ffffff;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.explorer-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e6e8ec;
|
||||
border-radius: 10px;
|
||||
background: #fbfbfc;
|
||||
}
|
||||
|
||||
.explorer-path {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #6a7280;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.breadcrumb-root {
|
||||
font-weight: 600;
|
||||
color: #3b3f46;
|
||||
}
|
||||
|
||||
.path-link {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #3b3f46;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.path-link:hover,
|
||||
.path-link:focus {
|
||||
background: transparent;
|
||||
color: #3b3f46;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
.toolbar-actions {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
border: 1px solid #dde0e6;
|
||||
background: #ffffff;
|
||||
color: #1f243d;
|
||||
padding: 6px 8px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-button:hover {
|
||||
background: #f3f5f8;
|
||||
}
|
||||
|
||||
.icon-button.danger {
|
||||
border-color: #f2c9c9;
|
||||
color: #9a2a2a;
|
||||
}
|
||||
|
||||
.icon-button.upload {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.icon-button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.icon-button.upload input {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.explorer-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.explorer-list {
|
||||
border: 1px solid #e6e8ec;
|
||||
border-radius: 10px;
|
||||
background: #ffffff;
|
||||
overflow: auto;
|
||||
max-height: 420px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.explorer-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 120px 90px;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid #f4f5f7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.explorer-list.has-meta .explorer-row {
|
||||
grid-template-columns: 1fr 160px 200px 80px 90px;
|
||||
}
|
||||
|
||||
.explorer-footer {
|
||||
grid-row: 2;
|
||||
grid-column: 1 / -1;
|
||||
margin: 0 12px 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e6e8ec;
|
||||
border-radius: 10px;
|
||||
background: #fbfbfc;
|
||||
font-size: 12px;
|
||||
color: #3b3f46;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.sidebar-tree {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.explorer-row.header {
|
||||
background: #f6f7f9;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.explorer-row.footer {
|
||||
background: #fbfbfc;
|
||||
font-size: 12px;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.footer-count {
|
||||
text-align: right;
|
||||
color: #6a7280;
|
||||
}
|
||||
|
||||
.explorer-row.selected {
|
||||
background: #eef3ff;
|
||||
}
|
||||
|
||||
.tree-row {
|
||||
background: transparent;
|
||||
padding: 4px 6px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.tree-row.folder {
|
||||
background: transparent;
|
||||
font-weight: 500;
|
||||
color: #3b3f46;
|
||||
}
|
||||
|
||||
.tree-row.folder:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tree-row.active {
|
||||
background: #e7edf7;
|
||||
}
|
||||
|
||||
.tree-tag-count {
|
||||
font-size: 11px;
|
||||
color: #6a7280;
|
||||
background: #eef1f5;
|
||||
border-radius: 999px;
|
||||
padding: 0 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.media-tag-count {
|
||||
margin-left: 6px;
|
||||
font-size: 11px;
|
||||
color: #6a7280;
|
||||
background: #eef1f5;
|
||||
border-radius: 999px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
|
||||
.tree-caret {
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin-right: 6px;
|
||||
color: #8a919d;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
width: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tree-caret:hover,
|
||||
.tree-caret:focus {
|
||||
background: transparent;
|
||||
color: #8a919d;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tree-caret.disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tree-icon {
|
||||
margin-right: 6px;
|
||||
color: #8a919d;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.tree-icon.folder {
|
||||
color: #5a7bb6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.row-name {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.row-icon {
|
||||
width: 18px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
color: #5a7bb6;
|
||||
}
|
||||
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(16, 18, 22, 0.35);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
width: min(420px, 90vw);
|
||||
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.18);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.modal-card h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
font-size: 12px;
|
||||
color: #5a5f6b;
|
||||
}
|
||||
|
||||
.modal-card input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.upload-dropzone {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed #d5d9e0;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
background: #fbfbfc;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #6a7280;
|
||||
}
|
||||
|
||||
.upload-dropzone input[type='file'] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.upload-dropzone.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dropzone-title {
|
||||
font-size: 13px;
|
||||
color: #2f3442;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dropzone-hint {
|
||||
font-size: 12px;
|
||||
color: #6a7280;
|
||||
}
|
||||
|
||||
.dropzone-files {
|
||||
font-size: 11px;
|
||||
color: #3b3f46;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.upload-progress {
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #eef1f5;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.upload-progress > div {
|
||||
height: 100%;
|
||||
background: #5b7cfa;
|
||||
width: 0%;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
|
||||
.upload-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #e6e8ec;
|
||||
border-radius: 10px;
|
||||
background: #fbfbfc;
|
||||
font-size: 12px;
|
||||
color: #3b3f46;
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.tree-label {
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.depth-1 .tree-row {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.depth-2 .tree-row {
|
||||
margin-left: 24px;
|
||||
}
|
||||
|
||||
.depth-3 .tree-row {
|
||||
margin-left: 36px;
|
||||
}
|
||||
|
||||
.depth-4 .tree-row {
|
||||
margin-left: 48px;
|
||||
}
|
||||
|
||||
.depth-5 .tree-row {
|
||||
margin-left: 60px;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
host: true,
|
||||
port: 5174,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>gui</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
-1337
File diff suppressed because it is too large
Load Diff
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"name": "gui",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.2.4"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,82 +0,0 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onBeforeUnmount } from "vue";
|
||||
import { getStatus, sendCommand } from "./api.js";
|
||||
|
||||
const status = ref({});
|
||||
const logs = ref([]);
|
||||
const error = ref(null);
|
||||
const apiBase = import.meta.env.VITE_BOX_API_URL || "http://10.10.10.23:8000";
|
||||
let pollId = null;
|
||||
|
||||
async function refresh() {
|
||||
error.value = null;
|
||||
try {
|
||||
status.value = await getStatus();
|
||||
} catch (err) {
|
||||
error.value = err?.message || "Failed to fetch status";
|
||||
}
|
||||
}
|
||||
|
||||
async function runCommand(command, payload = {}) {
|
||||
error.value = null;
|
||||
logs.value.unshift({
|
||||
at: new Date().toLocaleTimeString(),
|
||||
command,
|
||||
payload,
|
||||
});
|
||||
try {
|
||||
await sendCommand(command, payload);
|
||||
await refresh();
|
||||
} catch (err) {
|
||||
error.value = err?.message || "Failed to send command";
|
||||
logs.value.unshift({
|
||||
at: new Date().toLocaleTimeString(),
|
||||
command: "error",
|
||||
payload: { message: error.value },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await refresh();
|
||||
pollId = setInterval(refresh, 1000);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (pollId) {
|
||||
clearInterval(pollId);
|
||||
pollId = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>Klangkiste – Control Panel</h1>
|
||||
|
||||
<p>API: {{ apiBase }}</p>
|
||||
<p v-if="error">Error: {{ error }}</p>
|
||||
<p v-if="status.playback_state?.last_error">
|
||||
last_error: {{ status.playback_state.last_error }}
|
||||
</p>
|
||||
<pre>{{ status }}</pre>
|
||||
<h2>GUI Log</h2>
|
||||
<pre>{{ logs }}</pre>
|
||||
|
||||
<button @click="runCommand('play_pause')">Play / Pause</button>
|
||||
<button @click="runCommand('next')">Next</button>
|
||||
<button @click="runCommand('prev')">Prev</button>
|
||||
<button @click="runCommand('volume_up')">Vol +</button>
|
||||
<button @click="runCommand('volume_down')">Vol -</button>
|
||||
|
||||
<hr />
|
||||
|
||||
<button @click="runCommand('nfc_on', { uid: 'UID_1' })">
|
||||
NFC UID_1 ON
|
||||
</button>
|
||||
<button @click="runCommand('nfc_off', { uid: 'UID_1' })">
|
||||
NFC UID_1 OFF
|
||||
</button>
|
||||
|
||||
<hr />
|
||||
<button @click="refresh">Refresh Status</button>
|
||||
</template>
|
||||
@@ -1,29 +0,0 @@
|
||||
// src/api.js
|
||||
|
||||
const API_BASE = import.meta.env.VITE_BOX_API_URL || "http://10.10.10.23:8000";
|
||||
const API_TOKEN = import.meta.env.VITE_BOX_API_TOKEN || "";
|
||||
|
||||
export async function getStatus() {
|
||||
const response = await fetch(`${API_BASE}/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Status request failed (${response.status})`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
export async function sendCommand(command, payload = {}) {
|
||||
console.log("GUI -> API", command, payload);
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (API_TOKEN) {
|
||||
headers["X-API-Token"] = API_TOKEN;
|
||||
}
|
||||
const response = await fetch(`${API_BASE}/command`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ command, payload }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Command request failed (${response.status})`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 496 B |
@@ -1,43 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
msg: String,
|
||||
})
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Learn more about IDE Support for Vue in the
|
||||
<a
|
||||
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||
target="_blank"
|
||||
>Vue Docs Scaling up Guide</a
|
||||
>.
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -1,79 +0,0 @@
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: [{ find: "@", replacement: "/src" }],
|
||||
},
|
||||
server: {
|
||||
host: true, // Hört auf allen Netzwerk-Interfaces, kein localhost notwendig
|
||||
port: 5174 // optional, Standardport für Vite
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user