TTs whisper
This commit is contained in:
67
quibot-web/README.md
Normal file
67
quibot-web/README.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# Quibot Web
|
||||
|
||||
Interfície web per controlar el robit de la Quibot. Construida amb [Nuxt 3](https://nuxt.com/) i [Vue 3](https://vuejs.org/).
|
||||
|
||||
## Requisits previs
|
||||
|
||||
- [Node.js](https://nodejs.org/) (versió 20 o superior)
|
||||
- npm, yarn o pnpm
|
||||
|
||||
## Instal·lació
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
## Execució en mode desenvolupament
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
L'aplicació estarà disponible a `http://localhost:3000`.
|
||||
|
||||
## Configuració
|
||||
|
||||
L'aplicació es configura mitjançant variables d'entorn:
|
||||
|
||||
| Variable | Descripció | Valor per defecte |
|
||||
|-------------------|----------------------------------|------------------------|
|
||||
| `QUIBOT_BASE_URL` | URL del backend del Quibot | `http://quibot:8000` |
|
||||
| `QUIBOT_TOKEN` | Token d'autenticació del backend | `MY_SECRET_TOKEN` |
|
||||
|
||||
## Comandos disponibles
|
||||
|
||||
| Comando | Descripció |
|
||||
|-----------------|--------------------------------------------|
|
||||
| `npm run dev` | Inicia el servidor de desenvolupament |
|
||||
| `npm run build` | Compila l'aplicació per a producció |
|
||||
| `npm run generate` | Genera l'aplicació estàtica |
|
||||
| `npm run preview` | Previsualitza la versió de producció |
|
||||
|
||||
## Funcionalitat
|
||||
|
||||
L'interfície permet enviar comandes al motor del Quibot:
|
||||
|
||||
- **Step Forward** – Mou el motor cap endavant
|
||||
- **Step Backwards** – Mou el motor cap enrere
|
||||
- **Stop** – Atura el motor
|
||||
|
||||
## Estructura del projecte
|
||||
|
||||
```
|
||||
quibot-web/
|
||||
├── app/
|
||||
│ └── app.vue # Pàgina principal amb els controls del motor
|
||||
├── server/
|
||||
│ └── api/motor/ # Rutes API del servidor
|
||||
│ ├── stop.post.ts # Comanda d'aturada
|
||||
│ └── step/[direction].post.ts # Comandes de moviment
|
||||
├── public/
|
||||
│ └── favicon.ico # Icona del navegador
|
||||
├── nuxt.config.ts # Configuració del Nuxt
|
||||
├── package.json
|
||||
└── tsconfig.json # Configuració de TypeScript
|
||||
```
|
||||
|
||||
## Llicència
|
||||
File diff suppressed because it is too large
Load Diff
149
quibot-web/app/locales/ca.ts
Normal file
149
quibot-web/app/locales/ca.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export default {
|
||||
header: {
|
||||
subtitle: 'Monitor de Control del Robot',
|
||||
online: 'EN LÍNIA',
|
||||
offline: 'DESCONNECTAT',
|
||||
},
|
||||
panels: {
|
||||
blockQueue: 'Cua de Blocs',
|
||||
motionControls: 'Controls de Moviment',
|
||||
eyeControls: 'Controls dels Ulls',
|
||||
gestureSensor: 'Sensor de Gestos',
|
||||
},
|
||||
blocks: {
|
||||
empty: 'No hi ha blocs a la cua',
|
||||
clear: 'Buidar Cua',
|
||||
queued: 'A l\'espera',
|
||||
processing: 'Procesant',
|
||||
},
|
||||
blockActions: {
|
||||
rd: 'Avançar fins al creuament',
|
||||
gn: 'Girar a la dreta 90\u00B0',
|
||||
bu: 'Girar a l\'esquerra 90\u00B0',
|
||||
ye: 'Agafar / Extrair líquid',
|
||||
og: 'Deixar / Dispensar líquid',
|
||||
vt: 'Pausa sorpresa',
|
||||
},
|
||||
colors: {
|
||||
red: 'Vermell',
|
||||
green: 'Verd',
|
||||
blue: 'Blau',
|
||||
yellow: 'Groc',
|
||||
orange: 'Taronja',
|
||||
violet: 'Violeta',
|
||||
},
|
||||
motion: {
|
||||
forward: 'Endavant',
|
||||
backward: 'Enrere',
|
||||
left: 'Esquerra',
|
||||
right: 'Dreta',
|
||||
sending: 'Enviant: {dir}',
|
||||
sent: 'Comanda de motor enviada: {dir}',
|
||||
failed: 'La comanda ha fallat',
|
||||
},
|
||||
motionToast: {
|
||||
success: 'Moviment {dir}',
|
||||
},
|
||||
eyes: {
|
||||
shapeLabel: 'Color dels Ulls',
|
||||
actionsLabel: 'Accions',
|
||||
applyShape: 'Aplicar Forma',
|
||||
applyColor: 'Aplicar Color',
|
||||
eyeOn: 'Encendre',
|
||||
eyeOff: 'Apagar',
|
||||
toastSetShape: 'Forma dels ulls establerta: {shape}',
|
||||
toastFailedShape: 'No s\'ha pogut establir la forma dels ulls',
|
||||
toastSetColor: 'Color dels ulls establert: {color}',
|
||||
toastFailedColor: 'No s\'ha pogut establir el color dels ulls',
|
||||
toastOn: 'Ulls encesos',
|
||||
toastOff: 'Ulls apagats',
|
||||
toastFailedOn: 'No s\'han pogut encendre els ulls',
|
||||
toastFailedOff: 'No s\'han pogut apagar els ulls',
|
||||
},
|
||||
eyeShapes: {
|
||||
open: 'Oberts',
|
||||
fw: 'Endavant',
|
||||
down: 'Baix',
|
||||
gesture: 'Gest',
|
||||
},
|
||||
eyeColors: {
|
||||
white: 'Blanc',
|
||||
red: 'Vermell',
|
||||
green: 'Verd',
|
||||
blue: 'Blau',
|
||||
yellow: 'Groc',
|
||||
orange: 'Taronja',
|
||||
purple: 'Lila',
|
||||
cyan: 'Cian',
|
||||
black: 'Apagat',
|
||||
},
|
||||
gestures: {
|
||||
modeLabel: 'Mode de Funcionament',
|
||||
blockMode: 'Mode Blocs',
|
||||
gestureMode: 'Mode Gestos',
|
||||
detectedLabel: 'Gestos Detectats',
|
||||
empty: 'Encara no s\'han detectat gestos',
|
||||
reference: 'Referència de Gestos',
|
||||
clearLog: 'Buidar Registre',
|
||||
toggleToast: 'Canviar mode',
|
||||
},
|
||||
gestureNames: {
|
||||
forward: 'Empènyer Endavant',
|
||||
left: 'Ona Esquerra',
|
||||
right: 'Ona Dreta',
|
||||
up: 'Ona Amunt',
|
||||
down: 'Ona Avall',
|
||||
clockwise: 'Cercle CW',
|
||||
anticlockwise: 'Cercle CCW',
|
||||
wave: 'Ona (Commute)',
|
||||
},
|
||||
gestureActions: {
|
||||
forward: 'Avançar creuament',
|
||||
right: 'Girar a la dreta 90\u00B0',
|
||||
left: 'Girar a l\'esquerra 90\u00B0',
|
||||
up: 'Agafar / Extreure',
|
||||
down: 'Deixar / Dispensar',
|
||||
clockwise: 'Inactiu',
|
||||
anticlockwise: 'Inactiu',
|
||||
wave: 'Canviar mode',
|
||||
},
|
||||
gestureRef: {
|
||||
forward: 'Empènyer Endavant',
|
||||
left: 'Ona Esquerra',
|
||||
right: 'Ona Dreta',
|
||||
up: 'Ona Amunt',
|
||||
down: 'Ona Avall',
|
||||
clockwise: 'Cercle CW',
|
||||
anticlockwise: 'Cercle CCW',
|
||||
wave: 'Ona (Ambdues)',
|
||||
},
|
||||
modes: {
|
||||
block: 'Mode Blocs',
|
||||
gesture: 'Mode Gestos',
|
||||
},
|
||||
toast: {
|
||||
cleared: 'Cua buidada',
|
||||
switched: 'Canviat a mode {mode}',
|
||||
},
|
||||
theme: {
|
||||
light: 'Canviar a mode clar',
|
||||
dark: 'Canviar a mode fosc',
|
||||
},
|
||||
settings: {
|
||||
title: 'Configuració',
|
||||
save: 'Desar',
|
||||
saved: 'Configuració desada',
|
||||
theme: {
|
||||
label: 'Tema',
|
||||
dark: 'Fosc',
|
||||
light: 'Clar',
|
||||
},
|
||||
language: {
|
||||
label: 'Idioma',
|
||||
},
|
||||
piUrl: {
|
||||
label: 'URL del Raspberry Pi',
|
||||
placeholder: 'http://raspberrypi.local:8000',
|
||||
},
|
||||
},
|
||||
}
|
||||
149
quibot-web/app/locales/en.ts
Normal file
149
quibot-web/app/locales/en.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export default {
|
||||
header: {
|
||||
subtitle: 'Robot Control Monitor',
|
||||
online: 'ONLINE',
|
||||
offline: 'OFFLINE',
|
||||
},
|
||||
panels: {
|
||||
blockQueue: 'Block Queue',
|
||||
motionControls: 'Motion Controls',
|
||||
eyeControls: 'Eye Controls',
|
||||
gestureSensor: 'Gesture Sensor',
|
||||
},
|
||||
blocks: {
|
||||
empty: 'No blocks in queue',
|
||||
clear: 'Clear Queue',
|
||||
queued: 'Queued',
|
||||
processing: 'Processing',
|
||||
},
|
||||
blockActions: {
|
||||
rd: 'Advance to crossing',
|
||||
gn: 'Turn right 90\u00B0',
|
||||
bu: 'Turn left 90\u00B0',
|
||||
ye: 'Take / Extract liquid',
|
||||
og: 'Leave / Dispense liquid',
|
||||
vt: 'Surprise pause',
|
||||
},
|
||||
colors: {
|
||||
red: 'Red',
|
||||
green: 'Green',
|
||||
blue: 'Blue',
|
||||
yellow: 'Yellow',
|
||||
orange: 'Orange',
|
||||
violet: 'Violet',
|
||||
},
|
||||
motion: {
|
||||
forward: 'Forward',
|
||||
backward: 'Back',
|
||||
left: 'Left',
|
||||
right: 'Right',
|
||||
sending: 'Sending: {dir}',
|
||||
sent: 'Motor command sent: {dir}',
|
||||
failed: 'Command failed',
|
||||
},
|
||||
motionToast: {
|
||||
success: 'Motion {dir}',
|
||||
},
|
||||
eyes: {
|
||||
shapeLabel: 'Eye Color',
|
||||
actionsLabel: 'Actions',
|
||||
applyShape: 'Apply Shape',
|
||||
applyColor: 'Apply Color',
|
||||
eyeOn: 'Turn On',
|
||||
eyeOff: 'Turn Off',
|
||||
toastSetShape: 'Eye shape set: {shape}',
|
||||
toastFailedShape: 'Failed to set eye shape',
|
||||
toastSetColor: 'Eye color set: {color}',
|
||||
toastFailedColor: 'Failed to set eye color',
|
||||
toastOn: 'Eyes turned on',
|
||||
toastOff: 'Eyes turned off',
|
||||
toastFailedOn: 'Failed to turn eyes on',
|
||||
toastFailedOff: 'Failed to turn eyes off',
|
||||
},
|
||||
eyeShapes: {
|
||||
open: 'Open',
|
||||
fw: 'Forward',
|
||||
down: 'Down',
|
||||
gesture: 'Gesture',
|
||||
},
|
||||
eyeColors: {
|
||||
white: 'White',
|
||||
red: 'Red',
|
||||
green: 'Green',
|
||||
blue: 'Blue',
|
||||
yellow: 'Yellow',
|
||||
orange: 'Orange',
|
||||
purple: 'Purple',
|
||||
cyan: 'Cyan',
|
||||
black: 'Off',
|
||||
},
|
||||
gestures: {
|
||||
modeLabel: 'Operating Mode',
|
||||
blockMode: 'Block Mode',
|
||||
gestureMode: 'Gesture Mode',
|
||||
detectedLabel: 'Detected Gestures',
|
||||
empty: 'No gestures detected yet',
|
||||
reference: 'Gesture Reference',
|
||||
clearLog: 'Clear Log',
|
||||
toggleToast: 'Toggle mode',
|
||||
},
|
||||
gestureNames: {
|
||||
forward: 'Push Forward',
|
||||
left: 'Wave Left',
|
||||
right: 'Wave Right',
|
||||
up: 'Wave Up',
|
||||
down: 'Wave Down',
|
||||
clockwise: 'Circle CW',
|
||||
anticlockwise: 'Circle CCW',
|
||||
wave: 'Wave (Toggle)',
|
||||
},
|
||||
gestureActions: {
|
||||
forward: 'Advance crossing',
|
||||
right: 'Turn right 90\u00B0',
|
||||
left: 'Turn left 90\u00B0',
|
||||
up: 'Take / Extract',
|
||||
down: 'Leave / Dispense',
|
||||
clockwise: 'Idle',
|
||||
anticlockwise: 'Idle',
|
||||
wave: 'Toggle mode',
|
||||
},
|
||||
gestureRef: {
|
||||
forward: 'Push Forward',
|
||||
left: 'Wave Left',
|
||||
right: 'Wave Right',
|
||||
up: 'Wave Up',
|
||||
down: 'Wave Down',
|
||||
clockwise: 'Circle CW',
|
||||
anticlockwise: 'Circle CCW',
|
||||
wave: 'Wave (Both)',
|
||||
},
|
||||
modes: {
|
||||
block: 'Block Mode',
|
||||
gesture: 'Gesture Mode',
|
||||
},
|
||||
toast: {
|
||||
cleared: 'Queue cleared',
|
||||
switched: 'Switched to {mode} mode',
|
||||
},
|
||||
theme: {
|
||||
light: 'Switch to light mode',
|
||||
dark: 'Switch to dark mode',
|
||||
},
|
||||
settings: {
|
||||
title: 'Settings',
|
||||
save: 'Save',
|
||||
saved: 'Settings saved',
|
||||
theme: {
|
||||
label: 'Theme',
|
||||
dark: 'Dark',
|
||||
light: 'Light',
|
||||
},
|
||||
language: {
|
||||
label: 'Language',
|
||||
},
|
||||
piUrl: {
|
||||
label: 'Raspberry Pi URL',
|
||||
placeholder: 'http://raspberrypi.local:8000',
|
||||
},
|
||||
},
|
||||
}
|
||||
149
quibot-web/app/locales/es.ts
Normal file
149
quibot-web/app/locales/es.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
export default {
|
||||
header: {
|
||||
subtitle: 'Monitor de Control del Robot',
|
||||
online: 'EN LÍNEA',
|
||||
offline: 'DESCONNECTADO',
|
||||
},
|
||||
panels: {
|
||||
blockQueue: 'Cola de Bloques',
|
||||
motionControls: 'Controles de Movimiento',
|
||||
eyeControls: 'Controles de los Ojos',
|
||||
gestureSensor: 'Sensor de Gestos',
|
||||
},
|
||||
blocks: {
|
||||
empty: 'No hay bloques en la cola',
|
||||
clear: 'Borrar Cola',
|
||||
queued: 'En espera',
|
||||
processing: 'Procesando',
|
||||
},
|
||||
blockActions: {
|
||||
rd: 'Avanzar hasta el cruce',
|
||||
gn: 'Girar a la derecha 90\u00B0',
|
||||
bu: 'Girar a la izquierda 90\u00B0',
|
||||
ye: 'Tomar / Extraer líquido',
|
||||
og: 'Dejar / Dispensar líquido',
|
||||
vt: 'Pausa sorpresa',
|
||||
},
|
||||
colors: {
|
||||
red: 'Rojo',
|
||||
green: 'Verde',
|
||||
blue: 'Azul',
|
||||
yellow: 'Amarillo',
|
||||
orange: 'Naranja',
|
||||
violet: 'Violeta',
|
||||
},
|
||||
motion: {
|
||||
forward: 'Adelante',
|
||||
backward: 'Atrás',
|
||||
left: 'Izquierda',
|
||||
right: 'Derecha',
|
||||
sending: 'Enviando: {dir}',
|
||||
sent: 'Comando de motor enviado: {dir}',
|
||||
failed: 'El comando falló',
|
||||
},
|
||||
motionToast: {
|
||||
success: 'Movimiento {dir}',
|
||||
},
|
||||
eyes: {
|
||||
shapeLabel: 'Color de los Ojos',
|
||||
actionsLabel: 'Acciones',
|
||||
applyShape: 'Aplicar Forma',
|
||||
applyColor: 'Aplicar Color',
|
||||
eyeOn: 'Encender',
|
||||
eyeOff: 'Apagar',
|
||||
toastSetShape: 'Forma de ojos establecida: {shape}',
|
||||
toastFailedShape: 'No se pudo establecer la forma de los ojos',
|
||||
toastSetColor: 'Color de ojos establecido: {color}',
|
||||
toastFailedColor: 'No se pudo establecer el color de los ojos',
|
||||
toastOn: 'Ojos encendidos',
|
||||
toastOff: 'Ojos apagados',
|
||||
toastFailedOn: 'No se pudieron encender los ojos',
|
||||
toastFailedOff: 'No se pudieron apagar los ojos',
|
||||
},
|
||||
eyeShapes: {
|
||||
open: 'Abiertos',
|
||||
fw: 'Adelante',
|
||||
down: 'Abajo',
|
||||
gesture: 'Gesto',
|
||||
},
|
||||
eyeColors: {
|
||||
white: 'Blanco',
|
||||
red: 'Rojo',
|
||||
green: 'Verde',
|
||||
blue: 'Azul',
|
||||
yellow: 'Amarillo',
|
||||
orange: 'Naranja',
|
||||
purple: 'Morado',
|
||||
cyan: 'Cian',
|
||||
black: 'Apagado',
|
||||
},
|
||||
gestures: {
|
||||
modeLabel: 'Modo de Funcionamiento',
|
||||
blockMode: 'Modo Bloques',
|
||||
gestureMode: 'Modo Gestos',
|
||||
detectedLabel: 'Gestos Detectados',
|
||||
empty: 'Aún no se han detectado gestos',
|
||||
reference: 'Referencia de Gestos',
|
||||
clearLog: 'Borrar Registro',
|
||||
toggleToast: 'Cambiar modo',
|
||||
},
|
||||
gestureNames: {
|
||||
forward: 'Empujar Adelante',
|
||||
left: 'Onda Izquierda',
|
||||
right: 'Onda Derecha',
|
||||
up: 'Onda Arriba',
|
||||
down: 'Onda Abajo',
|
||||
clockwise: 'Círculo CW',
|
||||
anticlockwise: 'Círculo CCW',
|
||||
wave: 'Onda (Conmute)',
|
||||
},
|
||||
gestureActions: {
|
||||
forward: 'Avanzar cruce',
|
||||
right: 'Girar a la derecha 90\u00B0',
|
||||
left: 'Girar a la izquierda 90\u00B0',
|
||||
up: 'Tomar / Extraer',
|
||||
down: 'Dejar / Dispensar',
|
||||
clockwise: 'Inactivo',
|
||||
anticlockwise: 'Inactivo',
|
||||
wave: 'Cambiar modo',
|
||||
},
|
||||
gestureRef: {
|
||||
forward: 'Empujar Adelante',
|
||||
left: 'Onda Izquierda',
|
||||
right: 'Onda Derecha',
|
||||
up: 'Onda Arriba',
|
||||
down: 'Onda Abajo',
|
||||
clockwise: 'Círculo CW',
|
||||
anticlockwise: 'Círculo CCW',
|
||||
wave: 'Onda (Ambas)',
|
||||
},
|
||||
modes: {
|
||||
block: 'Modo Bloques',
|
||||
gesture: 'Modo Gestos',
|
||||
},
|
||||
toast: {
|
||||
cleared: 'Cola borrada',
|
||||
switched: 'Cambiado a modo {mode}',
|
||||
},
|
||||
theme: {
|
||||
light: 'Cambiar a modo claro',
|
||||
dark: 'Cambiar a modo oscuro',
|
||||
},
|
||||
settings: {
|
||||
title: 'Configuración',
|
||||
save: 'Guardar',
|
||||
saved: 'Configuración guardada',
|
||||
theme: {
|
||||
label: 'Tema',
|
||||
dark: 'Oscuro',
|
||||
light: 'Claro',
|
||||
},
|
||||
language: {
|
||||
label: 'Idioma',
|
||||
},
|
||||
piUrl: {
|
||||
label: 'URL de Raspberry Pi',
|
||||
placeholder: 'http://raspberrypi.local:8000',
|
||||
},
|
||||
},
|
||||
}
|
||||
38
quibot-web/app/plugins/i18n.ts
Normal file
38
quibot-web/app/plugins/i18n.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { defineNuxtPlugin, useRuntimeConfig } from '#app'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
import en from '~/locales/en'
|
||||
import ca from '~/locales/ca'
|
||||
import es from '~/locales/es'
|
||||
|
||||
const locales = ['en', 'ca', 'es'] as const
|
||||
type LocaleType = (typeof locales)[number]
|
||||
|
||||
function getSavedLocale(): LocaleType {
|
||||
if (typeof window !== 'undefined') {
|
||||
const saved = localStorage.getItem('quibot-locale')
|
||||
if (saved && locales.includes(saved as LocaleType)) {
|
||||
return saved as LocaleType
|
||||
}
|
||||
}
|
||||
return 'en'
|
||||
}
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
const locale = getSavedLocale()
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale,
|
||||
fallbackLocale: 'en',
|
||||
messages: { en, ca, es },
|
||||
})
|
||||
|
||||
nuxtApp.vueApp.use(i18n)
|
||||
|
||||
// Expose setLocale for global access
|
||||
nuxtApp.provide('setLocale', (l: LocaleType) => {
|
||||
if (locales.includes(l)) {
|
||||
i18n.global.locale.value = l
|
||||
localStorage.setItem('quibot-locale', l)
|
||||
}
|
||||
})
|
||||
})
|
||||
269
quibot-web/components/SettingsModal.vue
Normal file
269
quibot-web/components/SettingsModal.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { locale } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:show': [value: boolean]
|
||||
toast: [msg: string, type: 'success' | 'error']
|
||||
}>()
|
||||
|
||||
const localShow = ref(props.show)
|
||||
|
||||
watch(() => props.show, (val) => {
|
||||
localShow.value = val
|
||||
})
|
||||
|
||||
const selectedTheme = ref(localStorage.getItem('quibot-theme') || 'dark')
|
||||
const selectedLanguage = ref(localStorage.getItem('quibot-locale') || 'en')
|
||||
const piUrl = ref(localStorage.getItem('quibot-pi-url') || 'http://raspberrypi.local:8000')
|
||||
|
||||
function applyTheme(theme: string) {
|
||||
document.documentElement.setAttribute('data-theme', theme)
|
||||
localStorage.setItem('quibot-theme', theme)
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
applyTheme(selectedTheme.value)
|
||||
locale.value = selectedLanguage.value as any
|
||||
localStorage.setItem('quibot-locale', selectedLanguage.value)
|
||||
localStorage.setItem('quibot-pi-url', piUrl.value)
|
||||
document.cookie = `quibot-pi-url=${encodeURIComponent(piUrl.value)};path=/`
|
||||
emit('toast', t('settings.saved'), 'success')
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function close() {
|
||||
localShow.value = false
|
||||
emit('update:show', false)
|
||||
}
|
||||
|
||||
function backdropClick(e: MouseEvent) {
|
||||
if ((e.target as HTMLElement).classList.contains('settings-backdrop')) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-show="localShow" class="settings-backdrop" @click="backdropClick">
|
||||
<div class="settings-modal">
|
||||
<div class="settings-header">
|
||||
<h2>{{ $t('settings.title') }}</h2>
|
||||
<button class="close-btn" @click="close">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Theme -->
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">{{ $t('settings.theme.label') }}</span>
|
||||
<div class="theme-options">
|
||||
<button
|
||||
:class="['theme-option', { active: selectedTheme === 'light' }]"
|
||||
@click="selectedTheme = 'light'"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1111.21 3a7 7 0 009.79 9.79z"/></svg>
|
||||
<span>{{ $t('settings.theme.light') }}</span>
|
||||
</button>
|
||||
<button
|
||||
:class="['theme-option', { active: selectedTheme === 'dark' }]"
|
||||
@click="selectedTheme = 'dark'"
|
||||
>
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2m0 18v2m11-11h-2M3 12H1m17.07-7.07l-1.41 1.41M6.34 17.66l-1.41 1.41m14.14 0l-1.41-1.41M6.34 6.34L4.93 4.93"/></svg>
|
||||
<span>{{ $t('settings.theme.dark') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Language -->
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">{{ $t('settings.language.label') }}</span>
|
||||
<select v-model="selectedLanguage" class="lang-select">
|
||||
<option value="en">English</option>
|
||||
<option value="ca">Català</option>
|
||||
<option value="es">Español</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- PI URL -->
|
||||
<div class="setting-row">
|
||||
<span class="setting-label">{{ $t('settings.piUrl.label') }}</span>
|
||||
<input
|
||||
v-model="piUrl"
|
||||
:placeholder="$t('settings.piUrl.placeholder')"
|
||||
type="text"
|
||||
class="url-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Save -->
|
||||
<button class="btn-save" @click="saveSettings">{{ $t('settings.save') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.settings-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.settings-backdrop[style*="display: block"],
|
||||
.settings-backdrop[style*="display:flex"] {
|
||||
display: flex !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-modal {
|
||||
background: var(--bg-panel);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
background: var(--btn-bg);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.5rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
border-color: #ef4444;
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.setting-row {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.setting-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-options {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.theme-option {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-family: inherit;
|
||||
color: var(--text-muted);
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.625rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.theme-option.active {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: var(--active-bg);
|
||||
}
|
||||
|
||||
.theme-option:not(:disabled):hover {
|
||||
border-color: var(--border-subtle);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.lang-select,
|
||||
.url-input {
|
||||
width: 100%;
|
||||
padding: 0.625rem 0.75rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-secondary);
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: 0.625rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.lang-select:focus,
|
||||
.url-input:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.url-input::placeholder {
|
||||
color: var(--text-ghost);
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
font-family: inherit;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--accent), #ea580c);
|
||||
border: none;
|
||||
border-radius: 0.625rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-save:hover {
|
||||
box-shadow: 0 4px 16px var(--accent-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,10 @@ export default defineNuxtConfig({
|
||||
runtimeConfig: {
|
||||
quibotBaseUrl: process.env.QUIBOT_BASE_URL || 'http://quibot:8000',
|
||||
quibotToken: process.env.QUIBOT_TOKEN || 'MY_SECRET_TOKEN',
|
||||
public: {
|
||||
defaultLocale: 'en',
|
||||
supportedLocales: ['en', 'ca', 'es'],
|
||||
}
|
||||
},
|
||||
vite: {
|
||||
optimizeDeps: {
|
||||
|
||||
2233
quibot-web/package-lock.json
generated
2233
quibot-web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,9 +11,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"nuxt": "^4.4.2",
|
||||
"react-native-svg": "^15.15.5",
|
||||
"vue": "^3.5.32",
|
||||
"vue-router": "^5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
const config = useRuntimeConfig()
|
||||
const direction = getRouterParam(event, 'direction')
|
||||
|
||||
if (direction !== 'forward' && direction !== 'backwards') {
|
||||
@@ -9,7 +8,10 @@ export default defineEventHandler(async (event) => {
|
||||
})
|
||||
}
|
||||
|
||||
return await $fetch(`${config.quibotBaseUrl}/motor/step/${direction}`, {
|
||||
const baseUrl = getPiBaseUrl(event)
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
return await $fetch(`${baseUrl}/motor/step/${direction}`, {
|
||||
method: 'POST',
|
||||
query: {
|
||||
token: config.quibotToken,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
export default defineEventHandler(async () => {
|
||||
export default defineEventHandler(async (event) => {
|
||||
const baseUrl = getPiBaseUrl(event)
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
return await $fetch(`${config.quibotBaseUrl}/motor/stop`, {
|
||||
return await $fetch(`${baseUrl}/motor/stop`, {
|
||||
method: 'POST',
|
||||
query: {
|
||||
token: config.quibotToken,
|
||||
|
||||
10
quibot-web/server/plugins/settings.ts
Normal file
10
quibot-web/server/plugins/settings.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export default defineNitroPlugin((nitroApp) => {
|
||||
const base = useRuntimeConfig().quibotBaseUrl
|
||||
nitroApp.hooks.hook('request', (event) => {
|
||||
const url = getCookie(event, 'quibot-pi-url')
|
||||
if (url && url !== decodeURIComponent(base)) {
|
||||
// Override the base URL for this request
|
||||
(event.context as any).__piBaseUrl = url
|
||||
}
|
||||
})
|
||||
})
|
||||
9
quibot-web/server/utils/pi-url.ts
Normal file
9
quibot-web/server/utils/pi-url.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { getCookie } from 'h3'
|
||||
|
||||
export function getPiBaseUrl(event: any): string {
|
||||
const cookieUrl = getCookie(event, 'quibot-pi-url')
|
||||
if (cookieUrl) return decodeURIComponent(cookieUrl)
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
return config.quibotBaseUrl
|
||||
}
|
||||
Reference in New Issue
Block a user