parent
78a0a8bd18
commit
fe8b589787
@ -26,27 +26,64 @@
|
|||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<p>This tool allows you to receive and view HTTP events (kind 21120) from Nostr relays. Connect to a relay, subscribe to events, and decrypt messages.</p>
|
<p>This tool allows you to receive and view HTTP events (kind 21120) from Nostr relays, QR codes, or raw text input. Choose your preferred method using the tabs below.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Relay Connection</h2>
|
<!-- Tab Navigation - Updated data-tab attributes to match section IDs -->
|
||||||
<div class="relay-connection">
|
<div class="tabs">
|
||||||
<div class="relay-input-container">
|
<button class="tab-button active" data-tab="relay-connection-section">From Relay</button>
|
||||||
<label for="relayUrl">Relay URL:</label>
|
<button class="tab-button" data-tab="qr-code-scanner-section">From QR Scanner</button>
|
||||||
<input type="text" id="relayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
|
<button class="tab-button" data-tab="raw-event-input-section">From Raw Text</button>
|
||||||
<button id="connectRelayBtn" class="relay-connect-button">Connect</button>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Content - Each section gets a distinct ID -->
|
||||||
|
<!-- Relay Tab Content -->
|
||||||
|
<div id="relay-connection-section" class="active">
|
||||||
|
<h2>Relay Connection</h2>
|
||||||
|
<div class="relay-connection">
|
||||||
|
<div class="relay-input-container">
|
||||||
|
<label for="relayUrl">Relay URL:</label>
|
||||||
|
<input type="text" id="relayUrl" value="wss://relay.degmods.com" placeholder="wss://relay.example.com">
|
||||||
|
<button id="connectRelayBtn" class="relay-connect-button">Connect</button>
|
||||||
|
</div>
|
||||||
|
<div id="relayStatus" class="relay-status">Not connected</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- QR Scanner Tab Content -->
|
||||||
|
<div id="qr-code-scanner-section">
|
||||||
|
<h2>QR Code Scanner</h2>
|
||||||
|
<div class="qr-scanner-container">
|
||||||
|
<div class="qr-viewport">
|
||||||
|
<video id="qrVideo"></video>
|
||||||
|
<canvas id="qrCanvas" style="display: none;"></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="qr-controls">
|
||||||
|
<button id="startScanBtn" class="action-button">Start Camera</button>
|
||||||
|
<button id="stopScanBtn" class="action-button" disabled>Stop Camera</button>
|
||||||
|
</div>
|
||||||
|
<div id="qrStatus" class="status-message">Camera inactive. Click Start Camera to begin scanning.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Raw Text Tab Content -->
|
||||||
|
<div id="raw-event-input-section">
|
||||||
|
<h2>Raw Event Input</h2>
|
||||||
|
<div class="raw-input-container">
|
||||||
|
<p>Paste a raw Nostr event JSON below:</p>
|
||||||
|
<textarea id="rawEventInput" placeholder='{"id": "...", "pubkey": "...", "created_at": 1234567890, "kind": 21120, "tags": [], "content": "..."}' rows="10"></textarea>
|
||||||
|
<button id="parseRawEventBtn" class="action-button">Parse Event</button>
|
||||||
|
<div id="rawInputStatus" class="status-message"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="relayStatus" class="relay-status">Not connected</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>Received Events</h2>
|
<h2>Received Events</h2>
|
||||||
<div class="received-events">
|
<div class="received-events">
|
||||||
|
|
||||||
<div class="events-container">
|
<div class="events-container">
|
||||||
<div class="events-sidebar">
|
<div class="events-sidebar">
|
||||||
<div id="eventsList" class="events-list">
|
<div id="eventsList" class="events-list">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
No events received yet. Connect to a relay to start receiving events.
|
No events received yet. Use one of the methods above to receive events.
|
||||||
</div>
|
</div>
|
||||||
<!-- Events will be displayed here -->
|
<!-- Events will be displayed here -->
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
// External dependencies
|
// External dependencies
|
||||||
|
import jsQR from 'jsqr';
|
||||||
import * as nostrTools from 'nostr-tools';
|
import * as nostrTools from 'nostr-tools';
|
||||||
|
|
||||||
// Internal imports
|
// Internal imports
|
||||||
@ -706,6 +707,271 @@ Decrypted key: ${decryptedKey}`;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup tab navigation
|
||||||
|
function setupTabNavigation(): void {
|
||||||
|
const tabButtons = document.querySelectorAll('.tab-button');
|
||||||
|
const contentSections = document.querySelectorAll('#relay-connection-section, #qr-code-scanner-section, #raw-event-input-section');
|
||||||
|
|
||||||
|
// Add click event listeners to tab buttons
|
||||||
|
tabButtons.forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
|
// Get the target tab
|
||||||
|
const targetTabId = (button as HTMLElement).dataset.tab;
|
||||||
|
if (!targetTabId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove active class from all buttons and content sections
|
||||||
|
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
contentSections.forEach(section => section.classList.remove('active'));
|
||||||
|
|
||||||
|
// Add active class to clicked button and corresponding content section
|
||||||
|
button.classList.add('active');
|
||||||
|
const targetSection = document.getElementById(targetTabId);
|
||||||
|
if (targetSection) {
|
||||||
|
targetSection.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log for debugging
|
||||||
|
console.log(`Tab switched to: ${targetTabId}, active sections: ${document.querySelectorAll('.active').length}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Variables for QR scanner
|
||||||
|
let qrScanning = false;
|
||||||
|
let videoStream: MediaStream | null = null;
|
||||||
|
|
||||||
|
// Setup QR code scanner
|
||||||
|
function setupQRScanner(): void {
|
||||||
|
const startScanBtn = document.getElementById('startScanBtn');
|
||||||
|
const stopScanBtn = document.getElementById('stopScanBtn');
|
||||||
|
const qrVideo = document.getElementById('qrVideo') as HTMLVideoElement;
|
||||||
|
const qrCanvas = document.getElementById('qrCanvas') as HTMLCanvasElement;
|
||||||
|
const qrStatus = document.getElementById('qrStatus');
|
||||||
|
|
||||||
|
// Function to start the QR scanner
|
||||||
|
async function startQRScanner() {
|
||||||
|
// Already scanning
|
||||||
|
if (qrScanning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get access to the camera
|
||||||
|
const stream = await navigator.mediaDevices.getUserMedia({
|
||||||
|
video: { facingMode: 'environment' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = 'Camera active. Point at a QR code.';
|
||||||
|
qrStatus.className = 'status-message';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set video source to camera stream
|
||||||
|
if (qrVideo) {
|
||||||
|
qrVideo.srcObject = stream;
|
||||||
|
qrVideo.play();
|
||||||
|
videoStream = stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable buttons
|
||||||
|
if (startScanBtn) {
|
||||||
|
startScanBtn.setAttribute('disabled', 'true');
|
||||||
|
}
|
||||||
|
if (stopScanBtn) {
|
||||||
|
stopScanBtn.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start scanning
|
||||||
|
qrScanning = true;
|
||||||
|
scanQRCode();
|
||||||
|
} catch (error) {
|
||||||
|
// Handle errors
|
||||||
|
console.error('Error accessing camera:', error);
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
qrStatus.className = 'status-message error';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to stop the QR scanner
|
||||||
|
function stopQRScanner() {
|
||||||
|
if (!qrScanning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop all tracks
|
||||||
|
if (videoStream) {
|
||||||
|
videoStream.getTracks().forEach(track => track.stop());
|
||||||
|
videoStream = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = 'Camera inactive. Click Start Camera to begin scanning.';
|
||||||
|
qrStatus.className = 'status-message';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable/disable buttons
|
||||||
|
if (startScanBtn) {
|
||||||
|
startScanBtn.removeAttribute('disabled');
|
||||||
|
}
|
||||||
|
if (stopScanBtn) {
|
||||||
|
stopScanBtn.setAttribute('disabled', 'true');
|
||||||
|
}
|
||||||
|
|
||||||
|
qrScanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
if (startScanBtn) {
|
||||||
|
startScanBtn.addEventListener('click', startQRScanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopScanBtn) {
|
||||||
|
stopScanBtn.addEventListener('click', stopQRScanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to scan for QR codes
|
||||||
|
function scanQRCode() {
|
||||||
|
if (!qrScanning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request animation frame for next scan
|
||||||
|
window.requestAnimationFrame(scanQRCode);
|
||||||
|
|
||||||
|
if (!qrVideo || !qrCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if video isn't playing
|
||||||
|
if (qrVideo.readyState !== qrVideo.HAVE_ENOUGH_DATA) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get canvas context
|
||||||
|
const context = qrCanvas.getContext('2d');
|
||||||
|
if (!context) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas dimensions to match video
|
||||||
|
qrCanvas.width = qrVideo.videoWidth;
|
||||||
|
qrCanvas.height = qrVideo.videoHeight;
|
||||||
|
|
||||||
|
// Draw current video frame to canvas
|
||||||
|
context.drawImage(qrVideo, 0, 0, qrCanvas.width, qrCanvas.height);
|
||||||
|
|
||||||
|
// Get image data for QR detection
|
||||||
|
const imageData = context.getImageData(0, 0, qrCanvas.width, qrCanvas.height);
|
||||||
|
|
||||||
|
// Process with jsQR
|
||||||
|
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||||
|
inversionAttempts: 'dontInvert'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process QR code if found
|
||||||
|
if (code) {
|
||||||
|
console.log('QR code detected:', code.data);
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = 'QR code found! Processing...';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse as a Nostr event
|
||||||
|
try {
|
||||||
|
const eventData = JSON.parse(code.data);
|
||||||
|
|
||||||
|
// Check if this is a valid Nostr event
|
||||||
|
if (eventData && eventData.id && eventData.pubkey &&
|
||||||
|
eventData.created_at && eventData.kind === 21120) {
|
||||||
|
|
||||||
|
// Stop scanning after successful detection
|
||||||
|
stopQRScanner();
|
||||||
|
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = 'Valid Nostr event found! Event processed.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the event
|
||||||
|
processEvent(eventData);
|
||||||
|
} else {
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = 'QR code found, but not a valid kind 21120 Nostr event.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing QR code data:', error);
|
||||||
|
if (qrStatus) {
|
||||||
|
qrStatus.textContent = `Error parsing QR data: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
if (startScanBtn) {
|
||||||
|
startScanBtn.addEventListener('click', startQRScanner);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stopScanBtn) {
|
||||||
|
stopScanBtn.addEventListener('click', stopQRScanner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup raw text input
|
||||||
|
function setupRawTextInput(): void {
|
||||||
|
const parseRawEventBtn = document.getElementById('parseRawEventBtn');
|
||||||
|
const rawEventInput = document.getElementById('rawEventInput') as HTMLTextAreaElement;
|
||||||
|
const rawInputStatus = document.getElementById('rawInputStatus');
|
||||||
|
|
||||||
|
if (parseRawEventBtn && rawEventInput) {
|
||||||
|
parseRawEventBtn.addEventListener('click', () => {
|
||||||
|
const rawText = rawEventInput.value.trim();
|
||||||
|
|
||||||
|
if (!rawText) {
|
||||||
|
if (rawInputStatus) {
|
||||||
|
rawInputStatus.textContent = 'Please enter a raw Nostr event.';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Parse the JSON data
|
||||||
|
const eventData = JSON.parse(rawText);
|
||||||
|
|
||||||
|
// Validate as Nostr event
|
||||||
|
if (eventData && eventData.id && eventData.pubkey &&
|
||||||
|
eventData.created_at && eventData.kind === 21120) {
|
||||||
|
|
||||||
|
// Process the event
|
||||||
|
processEvent(eventData);
|
||||||
|
|
||||||
|
if (rawInputStatus) {
|
||||||
|
rawInputStatus.textContent = 'Event successfully processed!';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the input for convenience
|
||||||
|
rawEventInput.value = '';
|
||||||
|
} else {
|
||||||
|
if (rawInputStatus) {
|
||||||
|
rawInputStatus.textContent = 'Not a valid kind 21120 Nostr event.';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing raw event:', error);
|
||||||
|
if (rawInputStatus) {
|
||||||
|
rawInputStatus.textContent = `Error parsing: ${error instanceof Error ? error.message : String(error)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Get DOM elements
|
// Get DOM elements
|
||||||
@ -716,6 +982,15 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
const connectRelayBtn = document.getElementById('connectRelayBtn');
|
const connectRelayBtn = document.getElementById('connectRelayBtn');
|
||||||
|
|
||||||
|
// Tab functionality
|
||||||
|
setupTabNavigation();
|
||||||
|
|
||||||
|
// QR code scanner setup
|
||||||
|
setupQRScanner();
|
||||||
|
|
||||||
|
// Raw text input setup
|
||||||
|
setupRawTextInput();
|
||||||
|
|
||||||
// Connect to relay and automatically start subscription for logged-in user
|
// Connect to relay and automatically start subscription for logged-in user
|
||||||
if (connectRelayBtn) {
|
if (connectRelayBtn) {
|
||||||
connectRelayBtn.addEventListener('click', async () => {
|
connectRelayBtn.addEventListener('click', async () => {
|
||||||
|
@ -471,4 +471,131 @@ pre {
|
|||||||
.content {
|
.content {
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Interface Styles */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
color: var(--accent-color);
|
||||||
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab content sections */
|
||||||
|
#relay-connection-section,
|
||||||
|
#qr-code-scanner-section,
|
||||||
|
#raw-event-input-section {
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#relay-connection-section.active,
|
||||||
|
#qr-code-scanner-section.active,
|
||||||
|
#raw-event-input-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR Scanner Styles */
|
||||||
|
.qr-scanner-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-viewport {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
height: 300px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid var(--accent-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-viewport video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: var(--button-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:disabled {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raw Input Styles */
|
||||||
|
.raw-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raw-input-container textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raw-input-container button {
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
@ -1158,4 +1158,131 @@ footer {
|
|||||||
|
|
||||||
.clear-events-button:hover {
|
.clear-events-button:hover {
|
||||||
background-color: #c82333;
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab Interface Styles */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button:hover {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-button.active {
|
||||||
|
color: var(--accent-color);
|
||||||
|
border-bottom: 2px solid var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab content sections */
|
||||||
|
#relay-connection-section,
|
||||||
|
#qr-code-scanner-section,
|
||||||
|
#raw-event-input-section {
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#relay-connection-section.active,
|
||||||
|
#qr-code-scanner-section.active,
|
||||||
|
#raw-event-input-section.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* QR Scanner Styles */
|
||||||
|
.qr-scanner-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-viewport {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
height: 300px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid var(--accent-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-viewport video {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
background-color: var(--button-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:hover {
|
||||||
|
background-color: var(--button-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-button:disabled {
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Raw Input Styles */
|
||||||
|
.raw-input-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raw-input-container textarea {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.raw-input-container button {
|
||||||
|
align-self: flex-start;
|
||||||
}
|
}
|
@ -25,7 +25,7 @@ module.exports = {
|
|||||||
new NodePolyfillPlugin(),
|
new NodePolyfillPlugin(),
|
||||||
new CopyPlugin({
|
new CopyPlugin({
|
||||||
patterns: [
|
patterns: [
|
||||||
{ from: 'src/styles.css', to: 'styles.css' },
|
{ from: 'styles.css', to: 'styles.css' },
|
||||||
{ from: 'http.png', to: 'http.png' },
|
{ from: 'http.png', to: 'http.png' },
|
||||||
{ from: 'index.html', to: 'index.html' },
|
{ from: 'index.html', to: 'index.html' },
|
||||||
{ from: 'help.html', to: 'help.html' },
|
{ from: 'help.html', to: 'help.html' },
|
||||||
|
7
package-lock.json
generated
7
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"qrcode": "^1.5.4"
|
"qrcode": "^1.5.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -126,6 +127,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsqr": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsqr/-/jsqr-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
"node_modules/locate-path": {
|
"node_modules/locate-path": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"jsqr": "^1.4.0",
|
||||||
"qrcode": "^1.5.4"
|
"qrcode": "^1.5.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user