Create test input methods
This commit is contained in:
@@ -3,8 +3,10 @@ import { join } from 'path'
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
|
||||
let mainWindow
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 900,
|
||||
height: 670,
|
||||
show: false,
|
||||
@@ -12,7 +14,9 @@ const createWindow = () => {
|
||||
...(process.platform === 'linux' ? { icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
sandbox: false
|
||||
sandbox: false,
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
})
|
||||
|
||||
@@ -32,6 +36,35 @@ const createWindow = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// IPC handlers for input overlay
|
||||
ipcMain.on('show-input-overlay', (event, data) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('show-input-overlay', data)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('hide-input-overlay', () => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('hide-input-overlay')
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('input-overlay-submit', (event, data) => {
|
||||
console.log('Input overlay submit:', data)
|
||||
// You can handle the submitted input here
|
||||
// For example, send it to other parts of your application
|
||||
})
|
||||
|
||||
// Function to show input overlay (can be called globally)
|
||||
const showInputOverlay = (options = {}) => {
|
||||
if (mainWindow) {
|
||||
mainWindow.webContents.send('show-input-overlay', options)
|
||||
}
|
||||
}
|
||||
|
||||
// Export for potential use by other modules
|
||||
global.showInputOverlay = showInputOverlay
|
||||
|
||||
app.whenReady().then(() => {
|
||||
electronApp.setAppUserModelId('com.electron')
|
||||
|
||||
|
@@ -1,7 +1,17 @@
|
||||
import { contextBridge } from 'electron'
|
||||
import { contextBridge, ipcRenderer } from 'electron'
|
||||
import { electronAPI } from '@electron-toolkit/preload'
|
||||
|
||||
const api = {}
|
||||
const api = {
|
||||
// Input overlay API
|
||||
showInputOverlay: (options) => ipcRenderer.send('show-input-overlay', options),
|
||||
hideInputOverlay: () => ipcRenderer.send('hide-input-overlay'),
|
||||
onInputOverlayShow: (callback) => ipcRenderer.on('show-input-overlay', callback),
|
||||
onInputOverlayHide: (callback) => ipcRenderer.on('hide-input-overlay', callback),
|
||||
removeInputOverlayListeners: () => {
|
||||
ipcRenderer.removeAllListeners('show-input-overlay')
|
||||
ipcRenderer.removeAllListeners('hide-input-overlay')
|
||||
}
|
||||
}
|
||||
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
|
@@ -3,10 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OpenWall</title>
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src *; script-src * 'unsafe-inline' 'unsafe-eval'; style-src * 'unsafe-inline'; img-src * data: blob:; frame-src *; connect-src *; font-src *; media-src *; object-src *; child-src *;"
|
||||
/>
|
||||
<!-- CSP disabled for development -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@@ -4,6 +4,7 @@ import Calendar from './pages/Calendar'
|
||||
import Shopping from './pages/Shopping'
|
||||
import Notes from './pages/Notes'
|
||||
import Search from './pages/Search'
|
||||
import InputOverlay from './components/InputOverlay'
|
||||
|
||||
// Configuration
|
||||
const BACKGROUND_IMAGE_URL = 'https://cdn.pixabay.com/photo/2018/11/19/03/26/iceland-3824494_1280.jpg'
|
||||
@@ -30,6 +31,7 @@ const App = () => {
|
||||
<Route path="/search" element={<Search />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<InputOverlay />
|
||||
</div>
|
||||
</Router>
|
||||
)
|
||||
|
@@ -0,0 +1,97 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { showInputOverlay, setupAutoInputOverlay, onInputOverlaySubmit } from '../../utils/InputOverlayHelper'
|
||||
import './InputDemo.sass'
|
||||
|
||||
const InputDemo = () => {
|
||||
const [manualInput, setManualInput] = useState('')
|
||||
const [autoInput, setAutoInput] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
// Setup automatic input overlay for focused elements
|
||||
const cleanupAutoOverlay = setupAutoInputOverlay()
|
||||
|
||||
// Listen for input overlay submissions
|
||||
const cleanupSubmitListener = onInputOverlaySubmit((data) => {
|
||||
console.log('Input overlay submitted:', data)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cleanupAutoOverlay()
|
||||
cleanupSubmitListener()
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleManualTrigger = () => {
|
||||
showInputOverlay({
|
||||
targetElement: null,
|
||||
initialValue: manualInput,
|
||||
preferredMode: 'keyboard'
|
||||
})
|
||||
}
|
||||
|
||||
const handleDrawingTrigger = () => {
|
||||
showInputOverlay({
|
||||
targetElement: null,
|
||||
initialValue: manualInput,
|
||||
preferredMode: 'drawing'
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="input-demo">
|
||||
<h3>🎯 Intelligente Eingabehilfe</h3>
|
||||
|
||||
<div className="demo-section">
|
||||
<h4>⚡ Automatische Aktivierung</h4>
|
||||
<p>Klicken Sie in die Eingabefelder unten, um die Eingabehilfe automatisch zu öffnen:</p>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Klicken Sie hier für Tastatur-Eingabe..."
|
||||
value={autoInput}
|
||||
onChange={(e) => setAutoInput(e.target.value)}
|
||||
className="demo-input"
|
||||
/>
|
||||
<textarea
|
||||
placeholder="Oder hier für längere Texte..."
|
||||
value={autoInput}
|
||||
onChange={(e) => setAutoInput(e.target.value)}
|
||||
className="demo-textarea"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="demo-section">
|
||||
<h4>🎮 Manuelle Steuerung</h4>
|
||||
<p>Verwenden Sie die Buttons unten, um die Eingabehilfe manuell zu öffnen:</p>
|
||||
<div className="demo-controls">
|
||||
<button onClick={handleManualTrigger} className="demo-btn demo-btn--keyboard">
|
||||
⌨️ Tastatur öffnen
|
||||
</button>
|
||||
<button onClick={handleDrawingTrigger} className="demo-btn demo-btn--drawing">
|
||||
✍️ Handschrift öffnen
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Manueller Eingabetext..."
|
||||
value={manualInput}
|
||||
onChange={(e) => setManualInput(e.target.value)}
|
||||
className="demo-input"
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="demo-section">
|
||||
<h4>📖 Funktionen</h4>
|
||||
<ul className="demo-usage">
|
||||
<li>Die Eingabehilfe öffnet sich automatisch bei Fokus auf Eingabefelder</li>
|
||||
<li>Wählen Sie zwischen Tastatur-Eingabe und Handschrift-Erkennung</li>
|
||||
<li>Die OCR unterstützt deutsche Texterkennung mit Tesseract.js</li>
|
||||
<li>Verwenden Sie ESC zum Schließen oder klicken Sie außerhalb der Eingabehilfe</li>
|
||||
<li>Vollständig responsive für Touch-Displays und mobile Geräte</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputDemo
|
175
dashboard/src/renderer/src/components/InputDemo/InputDemo.sass
Normal file
175
dashboard/src/renderer/src/components/InputDemo/InputDemo.sass
Normal file
@@ -0,0 +1,175 @@
|
||||
.input-demo
|
||||
padding: 32px
|
||||
max-width: 900px
|
||||
margin: 0 auto
|
||||
|
||||
h3
|
||||
color: rgba(255, 255, 255, 0.95)
|
||||
margin-bottom: 32px
|
||||
font-size: 28px
|
||||
font-weight: 700
|
||||
text-align: center
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3)
|
||||
|
||||
h4
|
||||
color: rgba(255, 255, 255, 0.9)
|
||||
margin: 32px 0 16px 0
|
||||
font-size: 20px
|
||||
font-weight: 600
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2)
|
||||
|
||||
.demo-section
|
||||
margin-bottom: 40px
|
||||
padding: 32px
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
backdrop-filter: blur(20px)
|
||||
-webkit-backdrop-filter: blur(20px)
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
border-radius: 20px
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)
|
||||
|
||||
p
|
||||
color: rgba(255, 255, 255, 0.8)
|
||||
margin-bottom: 24px
|
||||
font-size: 16px
|
||||
line-height: 1.6
|
||||
|
||||
.demo-input, .demo-textarea
|
||||
width: 100%
|
||||
padding: 16px 20px
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
border-radius: 16px
|
||||
font-size: 15px
|
||||
margin-bottom: 16px
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
color: rgba(255, 255, 255, 0.95)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)
|
||||
|
||||
&::placeholder
|
||||
color: rgba(255, 255, 255, 0.5)
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
border-color: rgba(59, 130, 246, 0.5)
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 4px 15px rgba(0, 0, 0, 0.15)
|
||||
background: rgba(255, 255, 255, 0.15)
|
||||
transform: translateY(-1px)
|
||||
|
||||
.demo-textarea
|
||||
min-height: 100px
|
||||
resize: vertical
|
||||
|
||||
.demo-controls
|
||||
display: flex
|
||||
gap: 16px
|
||||
margin-bottom: 24px
|
||||
|
||||
.demo-btn
|
||||
padding: 14px 24px
|
||||
border: none
|
||||
border-radius: 16px
|
||||
cursor: pointer
|
||||
font-size: 15px
|
||||
font-weight: 500
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
position: relative
|
||||
overflow: hidden
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))
|
||||
opacity: 0
|
||||
transition: opacity 0.3s ease
|
||||
|
||||
&:hover
|
||||
transform: translateY(-2px)
|
||||
|
||||
&::before
|
||||
opacity: 1
|
||||
|
||||
&--keyboard
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(37, 99, 235, 0.8))
|
||||
color: white
|
||||
border: 1px solid rgba(59, 130, 246, 0.3)
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&--drawing
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.8), rgba(22, 163, 74, 0.8))
|
||||
color: white
|
||||
border: 1px solid rgba(34, 197, 94, 0.3)
|
||||
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
.demo-usage
|
||||
color: rgba(255, 255, 255, 0.8)
|
||||
list-style: none
|
||||
margin-left: 0
|
||||
padding-left: 0
|
||||
|
||||
li
|
||||
margin-bottom: 12px
|
||||
padding-left: 24px
|
||||
position: relative
|
||||
line-height: 1.5
|
||||
|
||||
&::before
|
||||
content: '✨'
|
||||
position: absolute
|
||||
left: 0
|
||||
top: 0
|
||||
font-size: 16px
|
||||
|
||||
@media (max-width: 768px)
|
||||
.input-demo
|
||||
padding: 24px 20px
|
||||
|
||||
.demo-section
|
||||
padding: 24px 20px
|
||||
|
||||
.demo-controls
|
||||
flex-direction: column
|
||||
|
||||
.demo-btn
|
||||
width: 100%
|
||||
padding: 12px 20px
|
||||
|
||||
h3
|
||||
font-size: 24px
|
||||
|
||||
h4
|
||||
font-size: 18px
|
||||
|
||||
@media (max-width: 480px)
|
||||
.input-demo
|
||||
padding: 20px 16px
|
||||
|
||||
.demo-section
|
||||
padding: 20px 16px
|
||||
border-radius: 16px
|
||||
|
||||
.demo-input, .demo-textarea
|
||||
padding: 12px 16px
|
||||
border-radius: 12px
|
||||
|
||||
.demo-btn
|
||||
padding: 10px 16px
|
||||
font-size: 14px
|
||||
border-radius: 12px
|
||||
|
||||
.demo-usage li
|
||||
padding-left: 20px
|
1
dashboard/src/renderer/src/components/InputDemo/index.js
Normal file
1
dashboard/src/renderer/src/components/InputDemo/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './InputDemo'
|
@@ -0,0 +1,165 @@
|
||||
import { useRef, useState, useEffect } from 'react'
|
||||
import './DrawingCanvas.sass'
|
||||
|
||||
const DrawingCanvas = ({ onDrawingComplete, disabled }) => {
|
||||
const canvasRef = useRef(null)
|
||||
const [isDrawing, setIsDrawing] = useState(false)
|
||||
const [brushSize, setBrushSize] = useState(3)
|
||||
const [hasDrawing, setHasDrawing] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Set canvas size
|
||||
canvas.width = canvas.offsetWidth
|
||||
canvas.height = canvas.offsetHeight
|
||||
|
||||
// Set drawing styles
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.strokeStyle = '#000000'
|
||||
ctx.lineWidth = brushSize
|
||||
}, [brushSize])
|
||||
|
||||
const startDrawing = (e) => {
|
||||
if (disabled) return
|
||||
|
||||
setIsDrawing(true)
|
||||
const canvas = canvasRef.current
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
}
|
||||
|
||||
const draw = (e) => {
|
||||
if (!isDrawing || disabled) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const x = e.clientX - rect.left
|
||||
const y = e.clientY - rect.top
|
||||
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.lineTo(x, y)
|
||||
ctx.stroke()
|
||||
|
||||
setHasDrawing(true)
|
||||
}
|
||||
|
||||
const stopDrawing = () => {
|
||||
setIsDrawing(false)
|
||||
}
|
||||
|
||||
// Touch event handlers for mobile/tablet support
|
||||
const handleTouchStart = (e) => {
|
||||
e.preventDefault()
|
||||
const touch = e.touches[0]
|
||||
const mouseEvent = new MouseEvent('mousedown', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
})
|
||||
startDrawing(mouseEvent)
|
||||
}
|
||||
|
||||
const handleTouchMove = (e) => {
|
||||
e.preventDefault()
|
||||
const touch = e.touches[0]
|
||||
const mouseEvent = new MouseEvent('mousemove', {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY
|
||||
})
|
||||
draw(mouseEvent)
|
||||
}
|
||||
|
||||
const handleTouchEnd = (e) => {
|
||||
e.preventDefault()
|
||||
stopDrawing()
|
||||
}
|
||||
|
||||
const clearCanvas = () => {
|
||||
const canvas = canvasRef.current
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
setHasDrawing(false)
|
||||
}
|
||||
|
||||
const processDrawing = () => {
|
||||
if (!hasDrawing || disabled) return
|
||||
|
||||
const canvas = canvasRef.current
|
||||
onDrawingComplete(canvas)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="drawing-canvas-container">
|
||||
<div className="drawing-canvas-controls">
|
||||
<div className="brush-size-control">
|
||||
<label htmlFor="brush-size">Pinselgröße:</label>
|
||||
<input
|
||||
id="brush-size"
|
||||
type="range"
|
||||
min="1"
|
||||
max="10"
|
||||
value={brushSize}
|
||||
onChange={(e) => setBrushSize(Number(e.target.value))}
|
||||
disabled={disabled}
|
||||
/>
|
||||
<span>{brushSize}px</span>
|
||||
</div>
|
||||
<div className="canvas-actions">
|
||||
<button
|
||||
onClick={clearCanvas}
|
||||
className="canvas-btn canvas-btn--secondary"
|
||||
disabled={disabled}
|
||||
>
|
||||
🗑️ Löschen
|
||||
</button>
|
||||
<button
|
||||
onClick={processDrawing}
|
||||
className="canvas-btn canvas-btn--primary"
|
||||
disabled={!hasDrawing || disabled}
|
||||
>
|
||||
🔍 Text erkennen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="drawing-canvas-wrapper">
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className={`drawing-canvas ${disabled ? 'disabled' : ''}`}
|
||||
onMouseDown={startDrawing}
|
||||
onMouseMove={draw}
|
||||
onMouseUp={stopDrawing}
|
||||
onMouseLeave={stopDrawing}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
/>
|
||||
{!hasDrawing && !disabled && (
|
||||
<div className="drawing-canvas-placeholder">
|
||||
✍️ Schreiben oder zeichnen Sie hier...
|
||||
</div>
|
||||
)}
|
||||
{disabled && (
|
||||
<div className="drawing-canvas-overlay">
|
||||
<div className="processing-spinner" />
|
||||
<span>🔍 Verarbeitung läuft...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DrawingCanvas
|
@@ -0,0 +1,249 @@
|
||||
.drawing-canvas-container
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 20px
|
||||
max-width: 900px
|
||||
|
||||
.drawing-canvas-controls
|
||||
display: flex
|
||||
justify-content: space-between
|
||||
align-items: center
|
||||
padding: 20px 24px
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
border-radius: 16px
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)
|
||||
|
||||
.brush-size-control
|
||||
display: flex
|
||||
align-items: center
|
||||
gap: 16px
|
||||
|
||||
label
|
||||
font-size: 14px
|
||||
font-weight: 500
|
||||
color: rgba(255, 255, 255, 0.9)
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)
|
||||
|
||||
input[type="range"]
|
||||
width: 120px
|
||||
height: 6px
|
||||
background: rgba(255, 255, 255, 0.2)
|
||||
border-radius: 3px
|
||||
outline: none
|
||||
-webkit-appearance: none
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
&::-webkit-slider-thumb
|
||||
-webkit-appearance: none
|
||||
width: 20px
|
||||
height: 20px
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(37, 99, 235, 0.9))
|
||||
border-radius: 50%
|
||||
cursor: pointer
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
transition: all 0.2s ease
|
||||
|
||||
&:hover
|
||||
transform: scale(1.1)
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&::-moz-range-thumb
|
||||
width: 20px
|
||||
height: 20px
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.9), rgba(37, 99, 235, 0.9))
|
||||
border-radius: 50%
|
||||
cursor: pointer
|
||||
border: none
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3)
|
||||
|
||||
span
|
||||
font-size: 13px
|
||||
font-weight: 500
|
||||
color: rgba(255, 255, 255, 0.7)
|
||||
min-width: 35px
|
||||
text-align: center
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
padding: 4px 8px
|
||||
border-radius: 8px
|
||||
backdrop-filter: blur(5px)
|
||||
|
||||
.canvas-actions
|
||||
display: flex
|
||||
gap: 12px
|
||||
|
||||
.canvas-btn
|
||||
padding: 10px 20px
|
||||
border-radius: 12px
|
||||
font-size: 14px
|
||||
font-weight: 500
|
||||
cursor: pointer
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
border: none
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))
|
||||
opacity: 0
|
||||
transition: opacity 0.3s ease
|
||||
|
||||
&:hover:not(:disabled)
|
||||
transform: translateY(-2px)
|
||||
|
||||
&::before
|
||||
opacity: 1
|
||||
|
||||
&:disabled
|
||||
cursor: not-allowed
|
||||
opacity: 0.5
|
||||
transform: none
|
||||
|
||||
&--primary
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(37, 99, 235, 0.8))
|
||||
color: white
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&:hover:not(:disabled)
|
||||
box-shadow: 0 8px 25px rgba(59, 130, 246, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&:disabled
|
||||
background: rgba(107, 114, 128, 0.3)
|
||||
box-shadow: none
|
||||
|
||||
&--secondary
|
||||
background: rgba(239, 68, 68, 0.15)
|
||||
color: rgba(248, 113, 113, 0.9)
|
||||
border: 1px solid rgba(239, 68, 68, 0.3)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
|
||||
&:hover:not(:disabled)
|
||||
background: rgba(239, 68, 68, 0.25)
|
||||
border-color: rgba(239, 68, 68, 0.4)
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.2)
|
||||
|
||||
.drawing-canvas-wrapper
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 320px
|
||||
border: 2px solid rgba(255, 255, 255, 0.2)
|
||||
border-radius: 16px
|
||||
overflow: hidden
|
||||
background: rgba(255, 255, 255, 0.95)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.5)
|
||||
|
||||
.drawing-canvas
|
||||
width: 100%
|
||||
height: 100%
|
||||
cursor: crosshair
|
||||
display: block
|
||||
background: white
|
||||
|
||||
&.disabled
|
||||
cursor: not-allowed
|
||||
opacity: 0.7
|
||||
|
||||
.drawing-canvas-placeholder
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
color: rgba(107, 114, 128, 0.6)
|
||||
font-style: italic
|
||||
font-size: 16px
|
||||
font-weight: 500
|
||||
pointer-events: none
|
||||
text-align: center
|
||||
background: rgba(255, 255, 255, 0.8)
|
||||
padding: 12px 20px
|
||||
border-radius: 12px
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05)
|
||||
|
||||
.drawing-canvas-overlay
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: rgba(255, 255, 255, 0.9)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
display: flex
|
||||
flex-direction: column
|
||||
align-items: center
|
||||
justify-content: center
|
||||
gap: 16px
|
||||
border-radius: 14px
|
||||
|
||||
span
|
||||
font-size: 16px
|
||||
font-weight: 500
|
||||
color: rgba(59, 130, 246, 0.8)
|
||||
|
||||
.processing-spinner
|
||||
width: 40px
|
||||
height: 40px
|
||||
border: 3px solid rgba(59, 130, 246, 0.2)
|
||||
border-top: 3px solid rgba(59, 130, 246, 0.8)
|
||||
border-radius: 50%
|
||||
animation: spin 1s linear infinite
|
||||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2)
|
||||
|
||||
@keyframes spin
|
||||
0%
|
||||
transform: rotate(0deg)
|
||||
100%
|
||||
transform: rotate(360deg)
|
||||
|
||||
@media (max-width: 768px)
|
||||
.drawing-canvas-controls
|
||||
flex-direction: column
|
||||
gap: 16px
|
||||
align-items: stretch
|
||||
padding: 16px 20px
|
||||
|
||||
.brush-size-control
|
||||
justify-content: center
|
||||
|
||||
.canvas-actions
|
||||
justify-content: center
|
||||
|
||||
.drawing-canvas-wrapper
|
||||
height: 280px
|
||||
|
||||
.drawing-canvas-placeholder
|
||||
font-size: 14px
|
||||
padding: 10px 16px
|
||||
|
||||
@media (max-width: 480px)
|
||||
.drawing-canvas-container
|
||||
gap: 16px
|
||||
|
||||
.drawing-canvas-controls
|
||||
padding: 14px 16px
|
||||
|
||||
.brush-size-control
|
||||
gap: 12px
|
||||
|
||||
input[type="range"]
|
||||
width: 100px
|
||||
|
||||
.canvas-btn
|
||||
padding: 8px 16px
|
||||
font-size: 13px
|
||||
|
||||
.drawing-canvas-wrapper
|
||||
height: 240px
|
@@ -0,0 +1,209 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { createWorker } from 'tesseract.js'
|
||||
import OnScreenKeyboard from './OnScreenKeyboard'
|
||||
import DrawingCanvas from './DrawingCanvas'
|
||||
import './InputOverlay.sass'
|
||||
|
||||
const InputOverlay = () => {
|
||||
const [isVisible, setIsVisible] = useState(false)
|
||||
const [inputMode, setInputMode] = useState('keyboard') // 'keyboard' or 'drawing'
|
||||
const [currentInput, setCurrentInput] = useState('')
|
||||
const [targetElement, setTargetElement] = useState(null)
|
||||
const [isProcessingOCR, setIsProcessingOCR] = useState(false)
|
||||
const workerRef = useRef(null)
|
||||
|
||||
// Initialize Tesseract worker
|
||||
useEffect(() => {
|
||||
const initWorker = async () => {
|
||||
try {
|
||||
workerRef.current = await createWorker('deu') // German language
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize Tesseract worker:', error)
|
||||
}
|
||||
}
|
||||
|
||||
initWorker()
|
||||
|
||||
return () => {
|
||||
if (workerRef.current) {
|
||||
workerRef.current.terminate()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Listen for Electron events to show/hide the input overlay
|
||||
useEffect(() => {
|
||||
const handleShowInputOverlay = (event, data) => {
|
||||
setTargetElement(data.targetElement || null)
|
||||
setCurrentInput(data.initialValue || '')
|
||||
setInputMode(data.preferredMode || 'keyboard')
|
||||
setIsVisible(true)
|
||||
}
|
||||
|
||||
const handleHideInputOverlay = () => {
|
||||
setIsVisible(false)
|
||||
setCurrentInput('')
|
||||
setTargetElement(null)
|
||||
}
|
||||
|
||||
// Listen for IPC events from main process
|
||||
if (window.electron && window.electron.ipcRenderer) {
|
||||
window.electron.ipcRenderer.on('show-input-overlay', handleShowInputOverlay)
|
||||
window.electron.ipcRenderer.on('hide-input-overlay', handleHideInputOverlay)
|
||||
}
|
||||
|
||||
// Listen for custom DOM events as fallback
|
||||
document.addEventListener('show-input-overlay', handleShowInputOverlay)
|
||||
document.addEventListener('hide-input-overlay', handleHideInputOverlay)
|
||||
|
||||
return () => {
|
||||
if (window.electron && window.electron.ipcRenderer) {
|
||||
window.electron.ipcRenderer.removeAllListeners('show-input-overlay')
|
||||
window.electron.ipcRenderer.removeAllListeners('hide-input-overlay')
|
||||
}
|
||||
document.removeEventListener('show-input-overlay', handleShowInputOverlay)
|
||||
document.removeEventListener('hide-input-overlay', handleHideInputOverlay)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleKeyboardInput = (value) => {
|
||||
setCurrentInput(value)
|
||||
}
|
||||
|
||||
const handleDrawingComplete = async (canvas) => {
|
||||
if (!workerRef.current || isProcessingOCR) return
|
||||
|
||||
setIsProcessingOCR(true)
|
||||
try {
|
||||
const { data: { text } } = await workerRef.current.recognize(canvas)
|
||||
const cleanedText = text.trim().replace(/\n/g, ' ')
|
||||
setCurrentInput(prev => prev + cleanedText)
|
||||
} catch (error) {
|
||||
console.error('OCR recognition failed:', error)
|
||||
} finally {
|
||||
setIsProcessingOCR(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Send the input back to the target element or emit an event
|
||||
if (targetElement) {
|
||||
// Try to set value for input elements
|
||||
if (targetElement.tagName === 'INPUT' || targetElement.tagName === 'TEXTAREA') {
|
||||
targetElement.value = currentInput
|
||||
targetElement.dispatchEvent(new Event('input', { bubbles: true }))
|
||||
targetElement.dispatchEvent(new Event('change', { bubbles: true }))
|
||||
}
|
||||
}
|
||||
|
||||
// Emit custom event with the input value
|
||||
const event = new CustomEvent('input-overlay-submit', {
|
||||
detail: { value: currentInput, targetElement }
|
||||
})
|
||||
document.dispatchEvent(event)
|
||||
|
||||
// Also send via Electron IPC if available
|
||||
if (window.electron && window.electron.ipcRenderer) {
|
||||
window.electron.ipcRenderer.send('input-overlay-submit', {
|
||||
value: currentInput,
|
||||
targetElement: targetElement ? targetElement.id || targetElement.className : null
|
||||
})
|
||||
}
|
||||
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false)
|
||||
setCurrentInput('')
|
||||
setTargetElement(null)
|
||||
}
|
||||
|
||||
const handleClear = () => {
|
||||
setCurrentInput('')
|
||||
}
|
||||
|
||||
if (!isVisible) return null
|
||||
|
||||
return (
|
||||
<div className="input-overlay">
|
||||
<div className="input-overlay__backdrop" onClick={handleClose} />
|
||||
<div className="input-overlay__container">
|
||||
<div className="input-overlay__header">
|
||||
<h3>Eingabehilfe</h3>
|
||||
<div className="input-overlay__mode-toggle">
|
||||
<button
|
||||
className={inputMode === 'keyboard' ? 'active' : ''}
|
||||
onClick={() => setInputMode('keyboard')}
|
||||
>
|
||||
Tastatur
|
||||
</button>
|
||||
<button
|
||||
className={inputMode === 'drawing' ? 'active' : ''}
|
||||
onClick={() => setInputMode('drawing')}
|
||||
>
|
||||
Handschrift
|
||||
</button>
|
||||
</div>
|
||||
<button className="input-overlay__close" onClick={handleClose}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="input-overlay__content">
|
||||
<div className="input-overlay__preview">
|
||||
<textarea
|
||||
value={currentInput}
|
||||
onChange={(e) => setCurrentInput(e.target.value)}
|
||||
placeholder="Ihr Text wird hier angezeigt..."
|
||||
className="input-overlay__preview-text"
|
||||
/>
|
||||
<div className="input-overlay__preview-actions">
|
||||
<button onClick={handleClear} className="input-overlay__btn input-overlay__btn--secondary">
|
||||
Zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-overlay__input-area">
|
||||
{inputMode === 'keyboard' ? (
|
||||
<OnScreenKeyboard
|
||||
value={currentInput}
|
||||
onChange={handleKeyboardInput}
|
||||
/>
|
||||
) : (
|
||||
<div className="input-overlay__drawing-container">
|
||||
<DrawingCanvas
|
||||
onDrawingComplete={handleDrawingComplete}
|
||||
disabled={isProcessingOCR}
|
||||
/>
|
||||
{isProcessingOCR && (
|
||||
<div className="input-overlay__ocr-status">
|
||||
🔍 Texterkennung läuft...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-overlay__footer">
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="input-overlay__btn input-overlay__btn--primary"
|
||||
>
|
||||
✓ Übernehmen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="input-overlay__btn input-overlay__btn--secondary"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputOverlay
|
@@ -0,0 +1,295 @@
|
||||
.input-overlay
|
||||
position: fixed
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100vw
|
||||
height: 100vh
|
||||
z-index: 9999
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
animation: overlayFadeIn 0.3s ease-out
|
||||
|
||||
&__backdrop
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: rgba(0, 0, 0, 0.4)
|
||||
backdrop-filter: blur(12px)
|
||||
-webkit-backdrop-filter: blur(12px)
|
||||
|
||||
&__container
|
||||
position: relative
|
||||
background: rgba(255, 255, 255, 0.15)
|
||||
backdrop-filter: blur(20px)
|
||||
-webkit-backdrop-filter: blur(20px)
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
border-radius: 24px
|
||||
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0 rgba(255, 255, 255, 0.1)
|
||||
max-width: 90vw
|
||||
max-height: 90vh
|
||||
overflow: hidden
|
||||
display: flex
|
||||
flex-direction: column
|
||||
animation: containerSlideUp 0.4s cubic-bezier(0.16, 1, 0.3, 1)
|
||||
|
||||
&__header
|
||||
padding: 24px 32px 20px 32px
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1)
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: space-between
|
||||
|
||||
h3
|
||||
margin: 0
|
||||
font-size: 20px
|
||||
font-weight: 600
|
||||
color: rgba(255, 255, 255, 0.95)
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)
|
||||
|
||||
&__mode-toggle
|
||||
display: flex
|
||||
gap: 4px
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
padding: 4px
|
||||
border-radius: 16px
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
|
||||
button
|
||||
padding: 10px 20px
|
||||
border: none
|
||||
background: transparent
|
||||
border-radius: 12px
|
||||
cursor: pointer
|
||||
font-size: 14px
|
||||
font-weight: 500
|
||||
color: rgba(255, 255, 255, 0.7)
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1))
|
||||
opacity: 0
|
||||
transition: opacity 0.3s ease
|
||||
border-radius: 12px
|
||||
|
||||
&:hover
|
||||
color: rgba(255, 255, 255, 0.9)
|
||||
transform: translateY(-1px)
|
||||
|
||||
&::before
|
||||
opacity: 1
|
||||
|
||||
&.active
|
||||
color: white
|
||||
background: linear-gradient(135deg, rgba(59, 130, 246, 0.8), rgba(37, 99, 235, 0.8))
|
||||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
transform: translateY(-1px)
|
||||
|
||||
&::before
|
||||
opacity: 0
|
||||
|
||||
&__close
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
font-size: 20px
|
||||
cursor: pointer
|
||||
color: rgba(255, 255, 255, 0.7)
|
||||
padding: 0
|
||||
width: 40px
|
||||
height: 40px
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
border-radius: 12px
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
|
||||
&:hover
|
||||
background: rgba(239, 68, 68, 0.2)
|
||||
color: rgb(248, 113, 113)
|
||||
transform: translateY(-1px)
|
||||
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.2)
|
||||
|
||||
&__content
|
||||
display: flex
|
||||
flex-direction: column
|
||||
flex: 1
|
||||
min-height: 0
|
||||
|
||||
&__preview
|
||||
padding: 24px 32px
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1)
|
||||
display: flex
|
||||
gap: 20px
|
||||
align-items: flex-start
|
||||
|
||||
&__preview-text
|
||||
flex: 1
|
||||
min-height: 100px
|
||||
max-height: 140px
|
||||
padding: 16px 20px
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
border-radius: 16px
|
||||
font-family: inherit
|
||||
font-size: 15px
|
||||
resize: vertical
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
color: rgba(255, 255, 255, 0.95)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
transition: all 0.3s ease
|
||||
|
||||
&::placeholder
|
||||
color: rgba(255, 255, 255, 0.5)
|
||||
|
||||
&:focus
|
||||
outline: none
|
||||
border-color: rgba(59, 130, 246, 0.5)
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 4px 15px rgba(0, 0, 0, 0.1)
|
||||
background: rgba(255, 255, 255, 0.15)
|
||||
|
||||
&__preview-actions
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
|
||||
&__input-area
|
||||
flex: 1
|
||||
padding: 24px 32px
|
||||
overflow-y: auto
|
||||
|
||||
&__drawing-container
|
||||
position: relative
|
||||
|
||||
&__ocr-status
|
||||
position: absolute
|
||||
top: 50%
|
||||
left: 50%
|
||||
transform: translate(-50%, -50%)
|
||||
background: rgba(0, 0, 0, 0.8)
|
||||
color: white
|
||||
padding: 16px 24px
|
||||
border-radius: 16px
|
||||
font-size: 14px
|
||||
font-weight: 500
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3)
|
||||
|
||||
&__footer
|
||||
padding: 24px 32px
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.02))
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1)
|
||||
display: flex
|
||||
gap: 16px
|
||||
justify-content: flex-end
|
||||
|
||||
&__btn
|
||||
padding: 12px 24px
|
||||
border-radius: 16px
|
||||
font-size: 14px
|
||||
font-weight: 500
|
||||
cursor: pointer
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
border: none
|
||||
position: relative
|
||||
overflow: hidden
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))
|
||||
opacity: 0
|
||||
transition: opacity 0.3s ease
|
||||
|
||||
&:hover
|
||||
transform: translateY(-2px)
|
||||
|
||||
&::before
|
||||
opacity: 1
|
||||
|
||||
&--primary
|
||||
background: linear-gradient(135deg, rgba(34, 197, 94, 0.8), rgba(22, 163, 74, 0.8))
|
||||
color: white
|
||||
box-shadow: 0 4px 15px rgba(34, 197, 94, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&:hover
|
||||
box-shadow: 0 8px 25px rgba(34, 197, 94, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
&--secondary
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
color: rgba(255, 255, 255, 0.8)
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
|
||||
&:hover
|
||||
background: rgba(255, 255, 255, 0.15)
|
||||
color: rgba(255, 255, 255, 0.95)
|
||||
|
||||
@keyframes overlayFadeIn
|
||||
from
|
||||
opacity: 0
|
||||
|
||||
to
|
||||
opacity: 1
|
||||
|
||||
@keyframes containerSlideUp
|
||||
from
|
||||
opacity: 0
|
||||
transform: translateY(30px) scale(0.95)
|
||||
|
||||
to
|
||||
opacity: 1
|
||||
transform: translateY(0) scale(1)
|
||||
|
||||
@media (max-width: 768px)
|
||||
.input-overlay
|
||||
&__container
|
||||
max-width: 95vw
|
||||
max-height: 95vh
|
||||
border-radius: 20px
|
||||
|
||||
&__header
|
||||
padding: 20px 24px 16px 24px
|
||||
|
||||
h3
|
||||
font-size: 18px
|
||||
|
||||
&__mode-toggle
|
||||
button
|
||||
padding: 8px 16px
|
||||
font-size: 13px
|
||||
|
||||
&__close
|
||||
width: 36px
|
||||
height: 36px
|
||||
font-size: 18px
|
||||
|
||||
&__preview, &__input-area, &__footer
|
||||
padding: 20px 24px
|
||||
|
||||
&__preview
|
||||
flex-direction: column
|
||||
gap: 16px
|
||||
|
||||
&__preview-actions
|
||||
flex-direction: row
|
||||
justify-content: center
|
@@ -0,0 +1,168 @@
|
||||
import { useState } from 'react'
|
||||
import './OnScreenKeyboard.sass'
|
||||
|
||||
const OnScreenKeyboard = ({ value, onChange }) => {
|
||||
const [isShiftActive, setIsShiftActive] = useState(false)
|
||||
const [isAltGrActive, setIsAltGrActive] = useState(false)
|
||||
|
||||
// German keyboard layout
|
||||
const keyboardLayout = [
|
||||
[
|
||||
{ key: '1', shift: '!', altGr: null },
|
||||
{ key: '2', shift: '"', altGr: '²' },
|
||||
{ key: '3', shift: '§', altGr: '³' },
|
||||
{ key: '4', shift: '$', altGr: null },
|
||||
{ key: '5', shift: '%', altGr: null },
|
||||
{ key: '6', shift: '&', altGr: null },
|
||||
{ key: '7', shift: '/', altGr: '{' },
|
||||
{ key: '8', shift: '(', altGr: '[' },
|
||||
{ key: '9', shift: ')', altGr: ']' },
|
||||
{ key: '0', shift: '=', altGr: '}' },
|
||||
{ key: 'ß', shift: '?', altGr: '\\' },
|
||||
{ key: '´', shift: '`', altGr: null },
|
||||
],
|
||||
[
|
||||
{ key: 'q', shift: 'Q', altGr: '@' },
|
||||
{ key: 'w', shift: 'W', altGr: null },
|
||||
{ key: 'e', shift: 'E', altGr: '€' },
|
||||
{ key: 'r', shift: 'R', altGr: null },
|
||||
{ key: 't', shift: 'T', altGr: null },
|
||||
{ key: 'z', shift: 'Z', altGr: null },
|
||||
{ key: 'u', shift: 'U', altGr: null },
|
||||
{ key: 'i', shift: 'I', altGr: null },
|
||||
{ key: 'o', shift: 'O', altGr: null },
|
||||
{ key: 'p', shift: 'P', altGr: null },
|
||||
{ key: 'ü', shift: 'Ü', altGr: null },
|
||||
{ key: '+', shift: '*', altGr: '~' },
|
||||
],
|
||||
[
|
||||
{ key: 'a', shift: 'A', altGr: null },
|
||||
{ key: 's', shift: 'S', altGr: null },
|
||||
{ key: 'd', shift: 'D', altGr: null },
|
||||
{ key: 'f', shift: 'F', altGr: null },
|
||||
{ key: 'g', shift: 'G', altGr: null },
|
||||
{ key: 'h', shift: 'H', altGr: null },
|
||||
{ key: 'j', shift: 'J', altGr: null },
|
||||
{ key: 'k', shift: 'K', altGr: null },
|
||||
{ key: 'l', shift: 'L', altGr: null },
|
||||
{ key: 'ö', shift: 'Ö', altGr: null },
|
||||
{ key: 'ä', shift: 'Ä', altGr: null },
|
||||
{ key: '#', shift: "'", altGr: null },
|
||||
],
|
||||
[
|
||||
{ key: '<', shift: '>', altGr: '|' },
|
||||
{ key: 'y', shift: 'Y', altGr: null },
|
||||
{ key: 'x', shift: 'X', altGr: null },
|
||||
{ key: 'c', shift: 'C', altGr: null },
|
||||
{ key: 'v', shift: 'V', altGr: null },
|
||||
{ key: 'b', shift: 'B', altGr: null },
|
||||
{ key: 'n', shift: 'N', altGr: null },
|
||||
{ key: 'm', shift: 'M', altGr: 'µ' },
|
||||
{ key: ',', shift: ';', altGr: null },
|
||||
{ key: '.', shift: ':', altGr: null },
|
||||
{ key: '-', shift: '_', altGr: null },
|
||||
]
|
||||
]
|
||||
|
||||
const getKeyValue = (keyObj) => {
|
||||
if (isAltGrActive && keyObj.altGr) return keyObj.altGr
|
||||
if (isShiftActive && keyObj.shift) return keyObj.shift
|
||||
return keyObj.key
|
||||
}
|
||||
|
||||
const handleKeyPress = (keyObj) => {
|
||||
const keyValue = getKeyValue(keyObj)
|
||||
const newValue = value + keyValue
|
||||
onChange(newValue)
|
||||
|
||||
// Reset shift after key press (unless it's a modifier key)
|
||||
if (isShiftActive && keyObj.key !== 'Shift') {
|
||||
setIsShiftActive(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackspace = () => {
|
||||
onChange(value.slice(0, -1))
|
||||
}
|
||||
|
||||
const handleSpace = () => {
|
||||
onChange(value + ' ')
|
||||
}
|
||||
|
||||
const handleShift = () => {
|
||||
setIsShiftActive(!isShiftActive)
|
||||
if (isAltGrActive) setIsAltGrActive(false)
|
||||
}
|
||||
|
||||
const handleAltGr = () => {
|
||||
setIsAltGrActive(!isAltGrActive)
|
||||
if (isShiftActive) setIsShiftActive(false)
|
||||
}
|
||||
|
||||
const handleEnter = () => {
|
||||
onChange(value + '\n')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="on-screen-keyboard">
|
||||
{keyboardLayout.map((row, rowIndex) => (
|
||||
<div key={rowIndex} className="keyboard-row">
|
||||
{row.map((keyObj, keyIndex) => (
|
||||
<button
|
||||
key={`${rowIndex}-${keyIndex}`}
|
||||
className="keyboard-key"
|
||||
onClick={() => handleKeyPress(keyObj)}
|
||||
>
|
||||
<span className="key-main">{getKeyValue(keyObj)}</span>
|
||||
{(keyObj.shift || keyObj.altGr) && (
|
||||
<div className="key-alternates">
|
||||
{keyObj.shift && (
|
||||
<span className="key-shift">{keyObj.shift}</span>
|
||||
)}
|
||||
{keyObj.altGr && (
|
||||
<span className="key-altgr">{keyObj.altGr}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="keyboard-row keyboard-row--bottom">
|
||||
<button
|
||||
className={`keyboard-key keyboard-key--modifier ${isShiftActive ? 'active' : ''}`}
|
||||
onClick={handleShift}
|
||||
>
|
||||
Shift
|
||||
</button>
|
||||
<button
|
||||
className={`keyboard-key keyboard-key--modifier ${isAltGrActive ? 'active' : ''}`}
|
||||
onClick={handleAltGr}
|
||||
>
|
||||
AltGr
|
||||
</button>
|
||||
<button
|
||||
className="keyboard-key keyboard-key--space"
|
||||
onClick={handleSpace}
|
||||
>
|
||||
Leertaste
|
||||
</button>
|
||||
<button
|
||||
className="keyboard-key keyboard-key--function"
|
||||
onClick={handleEnter}
|
||||
>
|
||||
Enter
|
||||
</button>
|
||||
<button
|
||||
className="keyboard-key keyboard-key--function"
|
||||
onClick={handleBackspace}
|
||||
>
|
||||
⌫
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OnScreenKeyboard
|
@@ -0,0 +1,187 @@
|
||||
.on-screen-keyboard
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 12px
|
||||
max-width: 900px
|
||||
padding: 24px
|
||||
background: rgba(255, 255, 255, 0.05)
|
||||
border-radius: 20px
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
border: 1px solid rgba(255, 255, 255, 0.1)
|
||||
|
||||
.keyboard-row
|
||||
display: flex
|
||||
gap: 8px
|
||||
justify-content: center
|
||||
|
||||
&--bottom
|
||||
margin-top: 12px
|
||||
|
||||
.keyboard-key
|
||||
position: relative
|
||||
min-width: 52px
|
||||
height: 52px
|
||||
border: 1px solid rgba(255, 255, 255, 0.2)
|
||||
background: rgba(255, 255, 255, 0.1)
|
||||
backdrop-filter: blur(10px)
|
||||
-webkit-backdrop-filter: blur(10px)
|
||||
border-radius: 12px
|
||||
cursor: pointer
|
||||
font-family: inherit
|
||||
font-size: 15px
|
||||
font-weight: 500
|
||||
color: rgba(255, 255, 255, 0.9)
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1)
|
||||
user-select: none
|
||||
overflow: hidden
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.1)
|
||||
|
||||
&::before
|
||||
content: ''
|
||||
position: absolute
|
||||
top: 0
|
||||
left: 0
|
||||
right: 0
|
||||
bottom: 0
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2), rgba(255, 255, 255, 0.1))
|
||||
opacity: 0
|
||||
transition: opacity 0.2s ease
|
||||
border-radius: 12px
|
||||
|
||||
&:hover
|
||||
transform: translateY(-2px)
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
border-color: rgba(255, 255, 255, 0.3)
|
||||
|
||||
&::before
|
||||
opacity: 1
|
||||
|
||||
&:active
|
||||
transform: translateY(0)
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1)
|
||||
|
||||
&--modifier
|
||||
min-width: 90px
|
||||
background: rgba(99, 102, 241, 0.2)
|
||||
border-color: rgba(99, 102, 241, 0.3)
|
||||
|
||||
&.active
|
||||
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(67, 56, 202, 0.8))
|
||||
color: white
|
||||
border-color: rgba(99, 102, 241, 0.6)
|
||||
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
transform: translateY(-1px)
|
||||
|
||||
&::before
|
||||
opacity: 0
|
||||
|
||||
&--space
|
||||
min-width: 240px
|
||||
background: rgba(59, 130, 246, 0.15)
|
||||
border-color: rgba(59, 130, 246, 0.3)
|
||||
|
||||
&:hover
|
||||
background: rgba(59, 130, 246, 0.25)
|
||||
border-color: rgba(59, 130, 246, 0.4)
|
||||
|
||||
&--function
|
||||
min-width: 90px
|
||||
background: rgba(34, 197, 94, 0.15)
|
||||
border-color: rgba(34, 197, 94, 0.3)
|
||||
color: rgba(255, 255, 255, 0.95)
|
||||
|
||||
&:hover
|
||||
background: rgba(34, 197, 94, 0.25)
|
||||
border-color: rgba(34, 197, 94, 0.4)
|
||||
|
||||
.key-main
|
||||
font-size: 17px
|
||||
font-weight: 600
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1)
|
||||
|
||||
.key-alternates
|
||||
position: absolute
|
||||
top: 4px
|
||||
right: 4px
|
||||
display: flex
|
||||
flex-direction: column
|
||||
gap: 1px
|
||||
|
||||
.key-shift
|
||||
font-size: 9px
|
||||
color: rgba(255, 255, 255, 0.6)
|
||||
line-height: 1
|
||||
text-shadow: none
|
||||
background: rgba(0, 0, 0, 0.2)
|
||||
padding: 1px 3px
|
||||
border-radius: 3px
|
||||
|
||||
.key-altgr
|
||||
font-size: 9px
|
||||
color: rgba(59, 130, 246, 0.8)
|
||||
line-height: 1
|
||||
text-shadow: none
|
||||
background: rgba(59, 130, 246, 0.1)
|
||||
padding: 1px 3px
|
||||
border-radius: 3px
|
||||
|
||||
// Special key animations
|
||||
.keyboard-key--modifier.active
|
||||
animation: pulseActive 2s infinite
|
||||
|
||||
@keyframes pulseActive
|
||||
0%, 100%
|
||||
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
50%
|
||||
box-shadow: 0 4px 20px rgba(99, 102, 241, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.2)
|
||||
|
||||
@media (max-width: 768px)
|
||||
.on-screen-keyboard
|
||||
padding: 16px
|
||||
gap: 8px
|
||||
|
||||
.keyboard-key
|
||||
min-width: 44px
|
||||
height: 44px
|
||||
font-size: 13px
|
||||
border-radius: 10px
|
||||
|
||||
&--modifier, &--function
|
||||
min-width: 70px
|
||||
|
||||
&--space
|
||||
min-width: 180px
|
||||
|
||||
.key-main
|
||||
font-size: 15px
|
||||
|
||||
.key-alternates
|
||||
top: 2px
|
||||
right: 2px
|
||||
|
||||
.key-shift, .key-altgr
|
||||
font-size: 8px
|
||||
|
||||
@media (max-width: 480px)
|
||||
.on-screen-keyboard
|
||||
padding: 12px
|
||||
gap: 6px
|
||||
|
||||
.keyboard-key
|
||||
min-width: 36px
|
||||
height: 36px
|
||||
font-size: 12px
|
||||
border-radius: 8px
|
||||
|
||||
&--modifier, &--function
|
||||
min-width: 60px
|
||||
|
||||
&--space
|
||||
min-width: 140px
|
||||
|
||||
.key-main
|
||||
font-size: 13px
|
@@ -0,0 +1 @@
|
||||
export { default } from './InputOverlay'
|
@@ -1,7 +1,9 @@
|
||||
import InputDemo from "../components/InputDemo"
|
||||
|
||||
const Calendar = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Kalender</h1>
|
||||
<InputDemo />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@ const Notes = () => {
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
const ctx = canvas.getContext('2d')
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true })
|
||||
|
||||
// Set canvas size
|
||||
const resizeCanvas = () => {
|
||||
|
135
dashboard/src/renderer/src/utils/InputOverlayHelper.js
Normal file
135
dashboard/src/renderer/src/utils/InputOverlayHelper.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* Utility functions for triggering the input overlay
|
||||
*/
|
||||
|
||||
/**
|
||||
* Show the input overlay with specified options
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {HTMLElement} options.targetElement - The target input element (optional)
|
||||
* @param {string} options.initialValue - Initial text value (optional)
|
||||
* @param {string} options.preferredMode - 'keyboard' or 'drawing' (optional)
|
||||
*/
|
||||
export const showInputOverlay = (options = {}) => {
|
||||
// Try using Electron API first
|
||||
if (window.api && window.api.showInputOverlay) {
|
||||
window.api.showInputOverlay(options)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to custom DOM event
|
||||
const event = new CustomEvent('show-input-overlay', { detail: options })
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the input overlay
|
||||
*/
|
||||
export const hideInputOverlay = () => {
|
||||
// Try using Electron API first
|
||||
if (window.api && window.api.hideInputOverlay) {
|
||||
window.api.hideInputOverlay()
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to custom DOM event
|
||||
const event = new CustomEvent('hide-input-overlay')
|
||||
document.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup automatic input overlay for input elements
|
||||
* Call this to automatically show the overlay when input elements are focused
|
||||
*/
|
||||
export const setupAutoInputOverlay = () => {
|
||||
const handleFocus = (event) => {
|
||||
const element = event.target
|
||||
|
||||
// Only handle text inputs, textareas, and contenteditable elements
|
||||
if (
|
||||
element.tagName === 'INPUT' &&
|
||||
['text', 'search', 'email', 'url', 'password'].includes(element.type)
|
||||
) {
|
||||
showInputOverlay({
|
||||
targetElement: element,
|
||||
initialValue: element.value || '',
|
||||
preferredMode: 'keyboard'
|
||||
})
|
||||
} else if (element.tagName === 'TEXTAREA') {
|
||||
showInputOverlay({
|
||||
targetElement: element,
|
||||
initialValue: element.value || '',
|
||||
preferredMode: 'keyboard'
|
||||
})
|
||||
} else if (element.contentEditable === 'true') {
|
||||
showInputOverlay({
|
||||
targetElement: element,
|
||||
initialValue: element.textContent || '',
|
||||
preferredMode: 'keyboard'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Add event listener for focus events
|
||||
document.addEventListener('focusin', handleFocus, true)
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
document.removeEventListener('focusin', handleFocus, true)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for input overlay submissions
|
||||
* @param {Function} callback - Callback function to handle submitted values
|
||||
*/
|
||||
export const onInputOverlaySubmit = (callback) => {
|
||||
const handleSubmit = (event) => {
|
||||
callback(event.detail)
|
||||
}
|
||||
|
||||
document.addEventListener('input-overlay-submit', handleSubmit)
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
document.removeEventListener('input-overlay-submit', handleSubmit)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus trap helper for accessibility
|
||||
*/
|
||||
export const setupFocusTrap = (container) => {
|
||||
const focusableElements = container.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
)
|
||||
|
||||
const firstElement = focusableElements[0]
|
||||
const lastElement = focusableElements[focusableElements.length - 1]
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Tab') {
|
||||
if (e.shiftKey && document.activeElement === firstElement) {
|
||||
e.preventDefault()
|
||||
lastElement.focus()
|
||||
} else if (!e.shiftKey && document.activeElement === lastElement) {
|
||||
e.preventDefault()
|
||||
firstElement.focus()
|
||||
}
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
hideInputOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener('keydown', handleKeyDown)
|
||||
|
||||
// Focus first element
|
||||
if (firstElement) {
|
||||
firstElement.focus()
|
||||
}
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user