Remove profiles and password proxy UI, simplify to cookie-only auth
Strip out User Profiles section, username/password proxy method, and related modals. Cookie proxy section is now always visible and renamed to just "Camera Proxy". Cookie key input hidden (auto-populated by Chrome extension). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+7
-90
@@ -16,14 +16,14 @@
|
||||
<div class="sidebar-header">
|
||||
<h2>Available Devices</h2>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="deviceStatus" class="status-message"></div>
|
||||
|
||||
|
||||
<!-- Device Search -->
|
||||
<div class="device-search-container">
|
||||
<input type="text" id="deviceSearch" placeholder="Search devices..." class="device-search-input">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-list-container">
|
||||
<div id="deviceList" class="device-list">
|
||||
<p class="placeholder-text">Connect to API to load devices</p>
|
||||
@@ -37,21 +37,6 @@
|
||||
<h1>Alta Video Camera Proxy</h1>
|
||||
</div>
|
||||
|
||||
<!-- User Profiles Section -->
|
||||
<section class="content-section profile-section">
|
||||
<h2>User Profiles</h2>
|
||||
<div class="profile-controls">
|
||||
<div class="profile-row">
|
||||
<label for="profileSelect">Select Profile:</label>
|
||||
<select id="profileSelect">
|
||||
<option value="">Select a profile...</option>
|
||||
</select>
|
||||
<button type="button" id="addProfileBtn" class="btn-primary">Add User</button>
|
||||
<button type="button" id="manageProfilesBtn" class="btn-success">Manage Users</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- API Connection Section -->
|
||||
<section class="content-section">
|
||||
<h2>API Connection</h2>
|
||||
@@ -65,41 +50,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="connection-controls">
|
||||
<button type="button" id="connectBtn" class="btn-primary" disabled>Connect to API</button>
|
||||
<button type="button" id="testConnectionBtn" class="btn-outline" disabled>Test Connection</button>
|
||||
<button type="button" id="disconnectBtn" class="btn-outline" disabled>Disconnect</button>
|
||||
</div>
|
||||
<div id="connectionStatus" class="status-message"></div>
|
||||
</section>
|
||||
|
||||
<!-- Camera Proxy Section -->
|
||||
<!-- Cookie-Based Camera Proxy Section -->
|
||||
<section class="content-section">
|
||||
<h2>Camera Proxy (Username/Password Method)</h2>
|
||||
<h2>Camera Proxy</h2>
|
||||
<div class="proxy-controls">
|
||||
<div class="input-row">
|
||||
<label for="deviceUUID">Device UUID:</label>
|
||||
<input type="text" id="deviceUUID" placeholder="(Auto-filled when you select a device from the list)" readonly>
|
||||
</div>
|
||||
<div class="proxy-buttons">
|
||||
<button type="button" id="startProxyBtn" class="btn-primary" disabled>Start Camera Proxy</button>
|
||||
<button type="button" id="checkVersionBtn" class="btn-outline" disabled>Check Version</button>
|
||||
<button type="button" id="stopProxyBtn" class="btn-outline" disabled>Stop Proxy</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cookie-Based Camera Proxy Section (Collapsible) -->
|
||||
<section class="content-section">
|
||||
<div class="collapsible-header" id="cookieSectionHeader">
|
||||
<h2>Camera Proxy (Cookie Method)</h2>
|
||||
<span class="collapse-icon" id="cookieCollapseIcon">▼</span>
|
||||
</div>
|
||||
<div class="proxy-controls collapsible-content" id="cookieProxyContent" style="display: none;">
|
||||
<div class="input-row">
|
||||
<label for="cookieDeviceUUID">Device UUID:</label>
|
||||
<input type="text" id="cookieDeviceUUID" placeholder="(Auto-filled when you select a device from the list)" readonly>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<div class="input-row" style="display: none;">
|
||||
<label for="cookieKey">Cookie Key:</label>
|
||||
<input type="text" id="cookieKey" placeholder="Paste your cookie key here">
|
||||
</div>
|
||||
@@ -119,54 +84,6 @@
|
||||
<div id="mainContent"></div>
|
||||
</div>
|
||||
|
||||
<!-- Add Profile Modal -->
|
||||
<div id="addProfileModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Add Connection Profile</h3>
|
||||
<span class="close" id="closeAddProfile">×</span>
|
||||
</div>
|
||||
<form id="addProfileForm">
|
||||
<div class="form-group">
|
||||
<label for="profileName">Profile Name:</label>
|
||||
<input type="text" id="profileName" placeholder="Enter profile name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profileUrl">Deployment URL:</label>
|
||||
<input type="url" id="profileUrl" placeholder="https://your-deployment.eu1.aware.avasecurity.com" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profileUsername">Username:</label>
|
||||
<input type="text" id="profileUsername" placeholder="Enter username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="profilePassword">Password:</label>
|
||||
<input type="password" id="profilePassword" placeholder="Enter password" required>
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button type="submit" class="modal-btn primary">Save Profile</button>
|
||||
<button type="button" class="modal-btn secondary" id="cancelAddProfile">Cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Manage Profiles Modal -->
|
||||
<div id="manageProfilesModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>Manage Connection Profiles</h3>
|
||||
<span class="close" id="closeManageProfiles">×</span>
|
||||
</div>
|
||||
<div class="profiles-list" id="profilesList">
|
||||
<!-- Profiles will be populated here -->
|
||||
</div>
|
||||
<div class="modal-buttons">
|
||||
<button type="button" class="modal-btn secondary" id="closeManageProfilesBtn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+67
-602
@@ -6,21 +6,16 @@ let sessionData = {
|
||||
};
|
||||
|
||||
// DOM elements
|
||||
const connectBtn = document.getElementById('connectBtn');
|
||||
const connectionStatus = document.getElementById('connectionStatus');
|
||||
const deviceStatus = document.getElementById('deviceStatus');
|
||||
const deviceList = document.getElementById('deviceList');
|
||||
const deviceDetails = document.getElementById('deviceDetails');
|
||||
const statusIndicator = document.getElementById('statusIndicator');
|
||||
const deviceUUID = document.getElementById('deviceUUID');
|
||||
const deviceSearch = document.getElementById('deviceSearch');
|
||||
|
||||
// New buttons for the layout
|
||||
// Connection buttons
|
||||
const testConnectionBtn = document.getElementById('testConnectionBtn');
|
||||
const disconnectBtn = document.getElementById('disconnectBtn');
|
||||
const startProxyBtn = document.getElementById('startProxyBtn');
|
||||
const checkVersionBtn = document.getElementById('checkVersionBtn');
|
||||
const stopProxyBtn = document.getElementById('stopProxyBtn');
|
||||
|
||||
// Cookie proxy elements
|
||||
const cookieDeviceUUID = document.getElementById('cookieDeviceUUID');
|
||||
@@ -28,31 +23,14 @@ const cookieKey = document.getElementById('cookieKey');
|
||||
const startCookieProxyBtn = document.getElementById('startCookieProxyBtn');
|
||||
const stopCookieProxyBtn = document.getElementById('stopCookieProxyBtn');
|
||||
|
||||
// Profile management elements
|
||||
const profileSelect = document.getElementById('profileSelect');
|
||||
const addProfileBtn = document.getElementById('addProfileBtn');
|
||||
const manageProfilesBtn = document.getElementById('manageProfilesBtn');
|
||||
const addProfileModal = document.getElementById('addProfileModal');
|
||||
const manageProfilesModal = document.getElementById('manageProfilesModal');
|
||||
const addProfileForm = document.getElementById('addProfileForm');
|
||||
const profilesList = document.getElementById('profilesList');
|
||||
|
||||
// Track selected device and profiles
|
||||
// Track selected device
|
||||
let selectedDevice = null;
|
||||
let currentProfiles = [];
|
||||
let selectedProfile = null;
|
||||
let activeProxyConnections = new Map(); // Track multiple active connections
|
||||
let activeCookieProxyConnections = new Map(); // Track cookie-based proxy connections
|
||||
const MAX_PROXY_CONNECTIONS = 2; // Limit to 2 simultaneous connections
|
||||
let allDevices = []; // Store all devices for search functionality
|
||||
|
||||
// Event listeners
|
||||
connectBtn.addEventListener('click', handleConnection);
|
||||
testConnectionBtn.addEventListener('click', handleTestConnection);
|
||||
disconnectBtn.addEventListener('click', handleDisconnect);
|
||||
startProxyBtn.addEventListener('click', handleStartProxy);
|
||||
checkVersionBtn.addEventListener('click', handleCheckVersion);
|
||||
stopProxyBtn.addEventListener('click', handleStopProxy);
|
||||
|
||||
// Cookie proxy event listeners
|
||||
startCookieProxyBtn.addEventListener('click', handleStartCookieProxy);
|
||||
@@ -64,76 +42,6 @@ cookieKey.addEventListener('input', updateCookieProxyButtonStates);
|
||||
// Device search event listener
|
||||
deviceSearch.addEventListener('input', handleDeviceSearch);
|
||||
|
||||
// Cookie section collapsible header
|
||||
document.getElementById('cookieSectionHeader').addEventListener('click', toggleCookieSection);
|
||||
|
||||
// Profile management event listeners
|
||||
profileSelect.addEventListener('change', handleProfileSelection);
|
||||
addProfileBtn.addEventListener('click', showAddProfileModal);
|
||||
manageProfilesBtn.addEventListener('click', showManageProfilesModal);
|
||||
addProfileForm.addEventListener('submit', handleAddProfile);
|
||||
|
||||
// Modal event listeners
|
||||
document.getElementById('closeAddProfile').addEventListener('click', hideAddProfileModal);
|
||||
document.getElementById('cancelAddProfile').addEventListener('click', hideAddProfileModal);
|
||||
document.getElementById('closeManageProfiles').addEventListener('click', hideManageProfilesModal);
|
||||
document.getElementById('closeManageProfilesBtn').addEventListener('click', hideManageProfilesModal);
|
||||
|
||||
// Close modals when clicking outside
|
||||
addProfileModal.addEventListener('click', (e) => {
|
||||
if (e.target === addProfileModal) hideAddProfileModal();
|
||||
});
|
||||
manageProfilesModal.addEventListener('click', (e) => {
|
||||
if (e.target === manageProfilesModal) hideManageProfilesModal();
|
||||
});
|
||||
|
||||
// Handle connection button click
|
||||
async function handleConnection() {
|
||||
if (!selectedProfile) {
|
||||
showStatus(connectionStatus, 'Please select a profile to connect', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button during connection
|
||||
setConnectButtonEnabled(false);
|
||||
showStatus(connectionStatus, 'Connecting to Alta API...', 'info');
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.login({
|
||||
deploymentUrl: selectedProfile.deploymentUrl,
|
||||
username: selectedProfile.username,
|
||||
password: selectedProfile.password
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Store session data
|
||||
sessionData.deploymentUrl = selectedProfile.deploymentUrl;
|
||||
sessionData.cookies = result.cookies;
|
||||
sessionData.isConnected = true;
|
||||
|
||||
showStatus(connectionStatus, 'Connected successfully!', 'success');
|
||||
updateConnectionStatus(true);
|
||||
updateButtonStates();
|
||||
|
||||
// Auto-retrieve devices when connected
|
||||
try {
|
||||
await handleGetDevices();
|
||||
} catch (deviceError) {
|
||||
console.error('Failed to auto-fetch devices:', deviceError);
|
||||
showStatus(deviceStatus, 'Connected, but failed to load devices. Try selecting a profile and reconnecting.', 'warning');
|
||||
}
|
||||
|
||||
} else {
|
||||
showStatus(connectionStatus, `Connection failed: ${result.message}`, 'error');
|
||||
setConnectButtonEnabled(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Connection error:', error);
|
||||
showStatus(connectionStatus, `Connection error: ${error.message}`, 'error');
|
||||
setConnectButtonEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle test connection
|
||||
async function handleTestConnection() {
|
||||
if (!sessionData.isConnected) {
|
||||
@@ -142,7 +50,7 @@ async function handleTestConnection() {
|
||||
}
|
||||
|
||||
showStatus(connectionStatus, 'Testing connection...', 'info');
|
||||
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.getAuthInfo({
|
||||
deploymentUrl: sessionData.deploymentUrl,
|
||||
@@ -166,191 +74,59 @@ function handleDisconnect() {
|
||||
sessionData.cookies = null;
|
||||
sessionData.deploymentUrl = '';
|
||||
selectedDevice = null;
|
||||
activeProxyConnections.clear();
|
||||
activeCookieProxyConnections.clear();
|
||||
allDevices = []; // Clear stored devices
|
||||
|
||||
|
||||
updateConnectionStatus(false);
|
||||
updateButtonStates();
|
||||
clearDeviceList();
|
||||
deviceUUID.value = '';
|
||||
cookieDeviceUUID.value = '';
|
||||
cookieKey.value = '';
|
||||
deviceSearch.value = ''; // Clear search input
|
||||
|
||||
|
||||
// Clear device status message
|
||||
deviceStatus.style.display = 'none';
|
||||
deviceStatus.textContent = '';
|
||||
|
||||
// Reset connect button text if it was stuck
|
||||
connectBtn.textContent = 'Connect to API';
|
||||
connectBtn.disabled = !selectedProfile;
|
||||
|
||||
// Reset proxy buttons
|
||||
startProxyBtn.disabled = true;
|
||||
stopProxyBtn.disabled = true;
|
||||
|
||||
|
||||
showStatus(connectionStatus, 'Disconnected from API', 'info');
|
||||
}
|
||||
|
||||
// Handle start proxy
|
||||
async function handleStartProxy() {
|
||||
if (!selectedDevice) {
|
||||
showStatus(connectionStatus, 'Please select a device first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedProfile) {
|
||||
showStatus(connectionStatus, 'Please select a profile first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check connection limit
|
||||
if (activeProxyConnections.size >= MAX_PROXY_CONNECTIONS) {
|
||||
showStatus(connectionStatus, `Maximum of ${MAX_PROXY_CONNECTIONS} camera proxy connections allowed. Stop an existing connection first.`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this device already has an active connection
|
||||
const deviceId = selectedDevice.guid || selectedDevice.id;
|
||||
if (activeProxyConnections.has(deviceId)) {
|
||||
showStatus(connectionStatus, `Camera proxy already running for device ${selectedDevice.name}`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if camera proxy executable exists
|
||||
try {
|
||||
const checkResult = await window.electronAPI.checkCameraProxy();
|
||||
if (!checkResult.exists) {
|
||||
showStatus(connectionStatus, 'Camera proxy executable (aware-cam-proxy-win.exe) not found in application directory', 'error');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
showStatus(connectionStatus, 'Failed to check camera proxy executable', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable button during launch
|
||||
startProxyBtn.disabled = true;
|
||||
showStatus(connectionStatus, `Starting camera proxy for device ${selectedDevice.name}...`, 'info');
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.launchCameraProxy({
|
||||
deploymentUrl: selectedProfile.deploymentUrl,
|
||||
username: selectedProfile.username,
|
||||
password: selectedProfile.password,
|
||||
deviceUuid: deviceId
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Track this connection
|
||||
activeProxyConnections.set(deviceId, {
|
||||
processId: result.processId,
|
||||
deviceName: selectedDevice.name,
|
||||
deviceId: deviceId,
|
||||
startTime: Date.now()
|
||||
});
|
||||
|
||||
updateProxyButtonStates();
|
||||
showStatus(connectionStatus, `${result.message} (${activeProxyConnections.size}/${MAX_PROXY_CONNECTIONS} connections active)`, 'success');
|
||||
} else {
|
||||
showStatus(connectionStatus, `Failed to start camera proxy: ${result.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Camera proxy launch error:', error);
|
||||
showStatus(connectionStatus, `Error launching camera proxy: ${error.message}`, 'error');
|
||||
} finally {
|
||||
startProxyBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle check version
|
||||
async function handleCheckVersion() {
|
||||
showStatus(connectionStatus, 'Checking camera proxy version...', 'info');
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.getCameraProxyVersion();
|
||||
|
||||
if (result.success) {
|
||||
showStatus(connectionStatus, `Camera Proxy Version: ${result.version}`, 'info');
|
||||
} else {
|
||||
showStatus(connectionStatus, `Failed to get version: ${result.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Version check error:', error);
|
||||
showStatus(connectionStatus, `Error checking version: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Handle stop proxy
|
||||
async function handleStopProxy() {
|
||||
if (activeProxyConnections.size === 0) {
|
||||
showStatus(connectionStatus, 'No active camera proxy connections found', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
showStatus(connectionStatus, 'Stopping all camera proxy connections...', 'info');
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.stopCameraProxy(null); // Stop all processes
|
||||
|
||||
if (result.success) {
|
||||
activeProxyConnections.clear();
|
||||
showStatus(connectionStatus, 'All camera proxy connections stopped successfully', 'success');
|
||||
|
||||
// Update visual indicators for all devices
|
||||
const deviceItems = deviceList.querySelectorAll('.device-item');
|
||||
deviceItems.forEach(item => {
|
||||
item.classList.remove('proxy-active');
|
||||
});
|
||||
} else {
|
||||
showStatus(connectionStatus, `Failed to stop camera proxy: ${result.message}`, 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Stop proxy error:', error);
|
||||
showStatus(connectionStatus, 'Error stopping camera proxy', 'error');
|
||||
}
|
||||
|
||||
updateProxyButtonStates();
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
|
||||
// Handle start cookie proxy
|
||||
async function handleStartCookieProxy() {
|
||||
if (!selectedDevice) {
|
||||
showStatus(connectionStatus, 'Please select a device first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const cookieKeyValue = cookieKey.value.trim();
|
||||
if (!cookieKeyValue) {
|
||||
showStatus(connectionStatus, 'Please enter a cookie key', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (!sessionData.deploymentUrl) {
|
||||
showStatus(connectionStatus, 'Please connect to API first to get deployment URL', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Check if this device already has an active cookie connection
|
||||
const deviceId = selectedDevice.guid || selectedDevice.id;
|
||||
if (activeCookieProxyConnections.has(deviceId)) {
|
||||
showStatus(connectionStatus, `Cookie proxy already running for device ${selectedDevice.name}`, 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Disable button during launch
|
||||
startCookieProxyBtn.disabled = true;
|
||||
showStatus(connectionStatus, `Starting cookie proxy for device ${selectedDevice.name}...`, 'info');
|
||||
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.launchCookieCameraProxy({
|
||||
deploymentUrl: sessionData.deploymentUrl,
|
||||
cookieKey: cookieKeyValue,
|
||||
deviceUuid: deviceId
|
||||
});
|
||||
|
||||
|
||||
if (result.success) {
|
||||
// Track this connection
|
||||
activeCookieProxyConnections.set(deviceId, {
|
||||
@@ -360,7 +136,7 @@ async function handleStartCookieProxy() {
|
||||
startTime: Date.now(),
|
||||
type: 'cookie'
|
||||
});
|
||||
|
||||
|
||||
updateCookieProxyButtonStates();
|
||||
showStatus(connectionStatus, `${result.message} (Cookie proxy active for ${selectedDevice.name})`, 'success');
|
||||
} else {
|
||||
@@ -380,16 +156,16 @@ async function handleStopCookieProxy() {
|
||||
showStatus(connectionStatus, 'No active cookie proxy connections found', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
showStatus(connectionStatus, 'Stopping cookie proxy connections...', 'info');
|
||||
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.stopCameraProxy(null); // Stop all processes
|
||||
|
||||
|
||||
if (result.success) {
|
||||
activeCookieProxyConnections.clear();
|
||||
showStatus(connectionStatus, 'Cookie proxy connections stopped successfully', 'success');
|
||||
|
||||
|
||||
// Update visual indicators for all devices
|
||||
const deviceItems = deviceList.querySelectorAll('.device-item');
|
||||
deviceItems.forEach(item => {
|
||||
@@ -402,7 +178,7 @@ async function handleStopCookieProxy() {
|
||||
console.error('Stop cookie proxy error:', error);
|
||||
showStatus(connectionStatus, 'Error stopping cookie proxy', 'error');
|
||||
}
|
||||
|
||||
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
|
||||
@@ -411,11 +187,11 @@ function updateCookieProxyButtonStates() {
|
||||
const hasSelectedDevice = selectedDevice !== null;
|
||||
const hasCookieKey = cookieKey.value.trim().length > 0;
|
||||
const hasActiveConnection = activeCookieProxyConnections.size > 0;
|
||||
|
||||
|
||||
if (sessionData.isConnected && hasSelectedDevice && hasCookieKey) {
|
||||
const deviceId = selectedDevice.guid || selectedDevice.id;
|
||||
const isThisDeviceActive = activeCookieProxyConnections.has(deviceId);
|
||||
|
||||
|
||||
startCookieProxyBtn.disabled = isThisDeviceActive;
|
||||
stopCookieProxyBtn.disabled = !hasActiveConnection;
|
||||
} else {
|
||||
@@ -428,7 +204,7 @@ function updateCookieProxyButtonStates() {
|
||||
function updateConnectionStatus(connected) {
|
||||
const statusDot = statusIndicator.querySelector('.status-dot');
|
||||
const statusText = statusIndicator.querySelector('.status-text');
|
||||
|
||||
|
||||
if (connected) {
|
||||
statusDot.className = 'status-dot online';
|
||||
statusText.textContent = 'Connected';
|
||||
@@ -441,23 +217,15 @@ function updateConnectionStatus(connected) {
|
||||
// Update button states based on connection status
|
||||
function updateButtonStates() {
|
||||
if (sessionData.isConnected) {
|
||||
connectBtn.style.display = 'none'; // Hide connect button when connected
|
||||
testConnectionBtn.disabled = false;
|
||||
disconnectBtn.disabled = false;
|
||||
checkVersionBtn.disabled = false;
|
||||
updateProxyButtonStates(); // Update proxy buttons when connected
|
||||
} else {
|
||||
connectBtn.style.display = 'inline-block'; // Show connect button when disconnected
|
||||
connectBtn.disabled = !selectedProfile;
|
||||
testConnectionBtn.disabled = true;
|
||||
disconnectBtn.disabled = true;
|
||||
startProxyBtn.disabled = true;
|
||||
checkVersionBtn.disabled = true;
|
||||
stopProxyBtn.disabled = true;
|
||||
startCookieProxyBtn.disabled = true;
|
||||
stopCookieProxyBtn.disabled = true;
|
||||
}
|
||||
|
||||
|
||||
// Always update cookie proxy button states
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
@@ -489,18 +257,18 @@ async function handleGetDevices() {
|
||||
// If no capabilities or localStorage property, include the device (fallback)
|
||||
return true;
|
||||
});
|
||||
|
||||
|
||||
const totalDevices = result.devices.length;
|
||||
const filteredCount = filteredDevices.length;
|
||||
const cloudDevicesHidden = totalDevices - filteredCount;
|
||||
|
||||
|
||||
let statusMessage = `Found ${filteredCount} local camera${filteredCount !== 1 ? 's' : ''}`;
|
||||
if (cloudDevicesHidden > 0) {
|
||||
statusMessage += ` (${cloudDevicesHidden} cloud camera${cloudDevicesHidden !== 1 ? 's' : ''} hidden)`;
|
||||
}
|
||||
|
||||
|
||||
showStatus(deviceStatus, statusMessage, 'success');
|
||||
|
||||
|
||||
// Store all devices for search functionality
|
||||
allDevices = filteredDevices;
|
||||
displayDevices(filteredDevices);
|
||||
@@ -518,7 +286,7 @@ function getDeviceStatus(device) {
|
||||
// Check for live.display_status first (Alta API standard)
|
||||
if (device.live && device.live.display_status) {
|
||||
const status = device.live.display_status.toLowerCase();
|
||||
|
||||
|
||||
// Handle color-based status responses from Alta API
|
||||
if (status === 'green') {
|
||||
return {
|
||||
@@ -536,14 +304,14 @@ function getDeviceStatus(device) {
|
||||
statusText: 'Warning'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Handle text-based status responses
|
||||
return {
|
||||
isOnline: status === 'online' || status === 'live' || status === 'connected',
|
||||
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Fallback to other possible status fields
|
||||
if (device.online !== undefined) {
|
||||
return {
|
||||
@@ -551,10 +319,10 @@ function getDeviceStatus(device) {
|
||||
statusText: device.online ? 'Online' : 'Offline'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
if (device.status) {
|
||||
const status = device.status.toLowerCase();
|
||||
|
||||
|
||||
// Handle color-based status in other fields
|
||||
if (status === 'green') {
|
||||
return {
|
||||
@@ -567,13 +335,13 @@ function getDeviceStatus(device) {
|
||||
statusText: 'Offline'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
isOnline: status === 'online' || status === 'live' || status === 'connected',
|
||||
statusText: status === 'online' || status === 'live' || status === 'connected' ? 'Online' : 'Offline'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// Default to offline if no status information available
|
||||
return {
|
||||
isOnline: false,
|
||||
@@ -584,13 +352,13 @@ function getDeviceStatus(device) {
|
||||
// Handle device search
|
||||
function handleDeviceSearch() {
|
||||
const searchTerm = deviceSearch.value.toLowerCase().trim();
|
||||
|
||||
|
||||
if (!searchTerm) {
|
||||
// Show all devices if search is empty
|
||||
displayDevices(allDevices);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Filter devices based on search term
|
||||
const filteredDevices = allDevices.filter(device => {
|
||||
const deviceName = (device.name || '').toLowerCase();
|
||||
@@ -598,14 +366,14 @@ function handleDeviceSearch() {
|
||||
const deviceType = (device.type || '').toLowerCase();
|
||||
const deviceModel = (device.model || '').toLowerCase();
|
||||
const deviceIp = (device.ipAddress || '').toLowerCase();
|
||||
|
||||
|
||||
return deviceName.includes(searchTerm) ||
|
||||
deviceId.includes(searchTerm) ||
|
||||
deviceType.includes(searchTerm) ||
|
||||
deviceModel.includes(searchTerm) ||
|
||||
deviceIp.includes(searchTerm);
|
||||
});
|
||||
|
||||
|
||||
displayDevices(filteredDevices);
|
||||
}
|
||||
|
||||
@@ -625,29 +393,29 @@ function displayDevices(devices) {
|
||||
deviceItem.className = 'device-item';
|
||||
deviceItem.dataset.deviceIndex = index;
|
||||
deviceItem.dataset.deviceId = device.guid || device.id;
|
||||
|
||||
|
||||
const deviceStatus = getDeviceStatus(device);
|
||||
const deviceId = device.guid || device.id;
|
||||
const isProxyActive = activeProxyConnections.has(deviceId);
|
||||
|
||||
// Add proxy-active class if this device has an active connection
|
||||
if (isProxyActive) {
|
||||
deviceItem.classList.add('proxy-active');
|
||||
const isCookieProxyActive = activeCookieProxyConnections.has(deviceId);
|
||||
|
||||
// Add cookie-proxy-active class if this device has an active cookie connection
|
||||
if (isCookieProxyActive) {
|
||||
deviceItem.classList.add('cookie-proxy-active');
|
||||
}
|
||||
|
||||
|
||||
deviceItem.innerHTML = `
|
||||
<div class="device-name">${escapeHtml(device.name || 'Unnamed Device')}</div>
|
||||
<div class="device-status-dot ${deviceStatus.isOnline ? 'online' : 'offline'}"></div>
|
||||
`;
|
||||
|
||||
|
||||
// Add click handler for device selection
|
||||
deviceItem.addEventListener('click', () => selectDevice(device, deviceItem));
|
||||
|
||||
|
||||
deviceList.appendChild(deviceItem);
|
||||
});
|
||||
|
||||
// Update proxy button states after displaying devices
|
||||
updateProxyButtonStates();
|
||||
|
||||
// Update cookie proxy button states after displaying devices
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
|
||||
// Handle device selection
|
||||
@@ -657,33 +425,29 @@ function selectDevice(device, deviceElement) {
|
||||
if (previousSelected) {
|
||||
previousSelected.classList.remove('selected');
|
||||
}
|
||||
|
||||
|
||||
// Select current device
|
||||
deviceElement.classList.add('selected');
|
||||
selectedDevice = device;
|
||||
|
||||
// Auto-populate device UUID fields
|
||||
|
||||
// Auto-populate cookie device UUID field
|
||||
const uuid = device.guid || device.id || '';
|
||||
deviceUUID.value = uuid;
|
||||
cookieDeviceUUID.value = uuid; // Also populate cookie proxy UUID field
|
||||
|
||||
cookieDeviceUUID.value = uuid;
|
||||
|
||||
// Show device selection feedback with connection status
|
||||
if (uuid) {
|
||||
const isActive = activeProxyConnections.has(uuid);
|
||||
const isCookieActive = activeCookieProxyConnections.has(uuid);
|
||||
const statusText = isActive ? ' (PROXY ACTIVE)' : '';
|
||||
const cookieStatusText = isCookieActive ? ' (COOKIE PROXY ACTIVE)' : '';
|
||||
showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'} (UUID: ${uuid})${statusText}${cookieStatusText}`, 'info');
|
||||
showStatus(connectionStatus, `Selected device: ${device.name || 'Unnamed Device'} (UUID: ${uuid})${cookieStatusText}`, 'info');
|
||||
}
|
||||
|
||||
updateProxyButtonStates();
|
||||
|
||||
updateCookieProxyButtonStates();
|
||||
}
|
||||
|
||||
// Display detailed device information
|
||||
function displayDeviceDetails(device) {
|
||||
const deviceStatus = getDeviceStatus(device);
|
||||
|
||||
|
||||
const detailsHtml = `
|
||||
<div class="device-details-card">
|
||||
<div class="device-details-header">
|
||||
@@ -692,43 +456,43 @@ function displayDeviceDetails(device) {
|
||||
${escapeHtml(deviceStatus.statusText)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-details-grid">
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">Device ID</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.guid || device.id || 'N/A')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">Device Type</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.type || 'Unknown')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">Model</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.model || 'Unknown')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">IP Address</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.ipAddress || 'N/A')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">MAC Address</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.macAddress || 'N/A')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">Firmware Version</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.firmwareVersion || 'N/A')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">Serial Number</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.serialNumber || 'N/A')}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="device-detail-item">
|
||||
<div class="device-detail-label">Location</div>
|
||||
<div class="device-detail-value">${escapeHtml(device.location || 'N/A')}</div>
|
||||
@@ -736,7 +500,7 @@ function displayDeviceDetails(device) {
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
deviceDetails.innerHTML = detailsHtml;
|
||||
}
|
||||
|
||||
@@ -747,41 +511,9 @@ function showStatus(element, message, type) {
|
||||
element.style.display = 'block';
|
||||
}
|
||||
|
||||
function setConnectButtonEnabled(enabled) {
|
||||
connectBtn.disabled = !enabled;
|
||||
connectBtn.textContent = enabled ? 'Connect to Alta API' : 'Connecting...';
|
||||
}
|
||||
|
||||
function clearDeviceList() {
|
||||
deviceList.innerHTML = '<p class="placeholder-text">Connect to API to load devices</p>';
|
||||
selectedDevice = null;
|
||||
deviceUUID.value = '';
|
||||
}
|
||||
|
||||
// New function to update proxy button states based on active connections
|
||||
function updateProxyButtonStates() {
|
||||
if (!sessionData.isConnected) {
|
||||
startProxyBtn.disabled = true;
|
||||
stopProxyBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const hasActiveConnections = activeProxyConnections.size > 0;
|
||||
const atMaxConnections = activeProxyConnections.size >= MAX_PROXY_CONNECTIONS;
|
||||
const selectedDeviceActive = selectedDevice && activeProxyConnections.has(selectedDevice.guid || selectedDevice.id);
|
||||
|
||||
// Enable start button if: connected, device selected, not at max connections, and device not already active
|
||||
startProxyBtn.disabled = !selectedDevice || atMaxConnections || selectedDeviceActive;
|
||||
|
||||
// Enable stop button if there are active connections
|
||||
stopProxyBtn.disabled = !hasActiveConnections;
|
||||
|
||||
// Update button text to show connection count
|
||||
if (hasActiveConnections) {
|
||||
stopProxyBtn.textContent = `Stop All Proxies (${activeProxyConnections.size})`;
|
||||
} else {
|
||||
stopProxyBtn.textContent = 'Stop Proxy';
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
@@ -791,211 +523,6 @@ function escapeHtml(text) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Profile Management Functions
|
||||
async function loadProfiles() {
|
||||
try {
|
||||
const result = await window.electronAPI.loadProfiles();
|
||||
if (result.success) {
|
||||
currentProfiles = result.profiles;
|
||||
updateProfileDropdown();
|
||||
} else {
|
||||
console.error('Failed to load profiles:', result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading profiles:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function updateProfileDropdown() {
|
||||
// Clear existing options except the first one
|
||||
profileSelect.innerHTML = '<option value="">Select a profile...</option>';
|
||||
|
||||
currentProfiles.forEach(profile => {
|
||||
const option = document.createElement('option');
|
||||
option.value = profile.id;
|
||||
option.textContent = `${profile.name} (${profile.username})`;
|
||||
profileSelect.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleProfileSelection() {
|
||||
const selectedProfileId = profileSelect.value;
|
||||
if (!selectedProfileId) {
|
||||
selectedProfile = null;
|
||||
updateButtonStates();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.getProfile(selectedProfileId);
|
||||
if (result.success) {
|
||||
selectedProfile = result.profile;
|
||||
updateButtonStates();
|
||||
} else {
|
||||
showStatus(connectionStatus, `Failed to load profile: ${result.message}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading profile:', error);
|
||||
showStatus(connectionStatus, 'Error loading profile', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showAddProfileModal() {
|
||||
// Clear the form for new profile
|
||||
addProfileForm.reset();
|
||||
|
||||
addProfileModal.style.display = 'block';
|
||||
document.getElementById('profileName').focus();
|
||||
}
|
||||
|
||||
function hideAddProfileModal() {
|
||||
addProfileModal.style.display = 'none';
|
||||
addProfileForm.reset();
|
||||
}
|
||||
|
||||
async function handleAddProfile(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const name = document.getElementById('profileName').value.trim();
|
||||
const deploymentUrl = document.getElementById('profileUrl').value.trim();
|
||||
const username = document.getElementById('profileUsername').value.trim();
|
||||
const password = document.getElementById('profilePassword').value;
|
||||
|
||||
if (!name || !deploymentUrl || !username || !password) {
|
||||
alert('Please fill in all fields');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if profile name already exists
|
||||
if (currentProfiles.some(p => p.name.toLowerCase() === name.toLowerCase())) {
|
||||
alert('A profile with this name already exists');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await window.electronAPI.saveProfile({
|
||||
name: name,
|
||||
deploymentUrl: deploymentUrl.replace(/\/$/, ''), // Remove trailing slash
|
||||
username: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
hideAddProfileModal();
|
||||
await loadProfiles(); // Reload profiles
|
||||
|
||||
// Select the newly created profile
|
||||
profileSelect.value = result.profile.id;
|
||||
await handleProfileSelection();
|
||||
|
||||
showStatus(connectionStatus, 'Profile saved successfully!', 'success');
|
||||
} else {
|
||||
alert(`Failed to save profile: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving profile:', error);
|
||||
alert('Error saving profile');
|
||||
}
|
||||
}
|
||||
|
||||
function showManageProfilesModal() {
|
||||
updateProfilesList();
|
||||
manageProfilesModal.style.display = 'block';
|
||||
}
|
||||
|
||||
function hideManageProfilesModal() {
|
||||
manageProfilesModal.style.display = 'none';
|
||||
}
|
||||
|
||||
function updateProfilesList() {
|
||||
profilesList.innerHTML = '';
|
||||
|
||||
if (currentProfiles.length === 0) {
|
||||
profilesList.innerHTML = '<div class="no-profiles">No profiles saved</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
currentProfiles.forEach(profile => {
|
||||
const profileItem = document.createElement('div');
|
||||
profileItem.className = 'profile-item';
|
||||
|
||||
profileItem.innerHTML = `
|
||||
<div class="profile-info">
|
||||
<h4>${escapeHtml(profile.name)}</h4>
|
||||
<p>${escapeHtml(profile.username)} @ ${escapeHtml(profile.deploymentUrl)}</p>
|
||||
</div>
|
||||
<div class="profile-actions">
|
||||
<button class="profile-action-btn edit">Edit</button>
|
||||
<button class="profile-action-btn delete">Delete</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
profileItem.querySelector('.edit').addEventListener('click', () => editProfile(profile.id));
|
||||
profileItem.querySelector('.delete').addEventListener('click', () => deleteProfile(profile.id));
|
||||
|
||||
profilesList.appendChild(profileItem);
|
||||
});
|
||||
}
|
||||
|
||||
async function editProfile(profileId) {
|
||||
try {
|
||||
const result = await window.electronAPI.getProfile(profileId);
|
||||
if (result.success) {
|
||||
const profile = result.profile;
|
||||
const newName = prompt('Enter new profile name:', profile.name);
|
||||
if (newName && newName.trim() !== profile.name) {
|
||||
const updateResult = await window.electronAPI.updateProfile(profileId, {
|
||||
name: newName.trim(),
|
||||
deploymentUrl: profile.deploymentUrl,
|
||||
username: profile.username,
|
||||
password: profile.password
|
||||
});
|
||||
|
||||
if (updateResult.success) {
|
||||
await loadProfiles();
|
||||
updateProfilesList();
|
||||
showStatus(connectionStatus, 'Profile updated successfully!', 'success');
|
||||
} else {
|
||||
alert(`Failed to update profile: ${updateResult.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error editing profile:', error);
|
||||
alert('Error editing profile');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteProfile(profileId) {
|
||||
const profile = currentProfiles.find(p => p.id === profileId);
|
||||
if (!profile) return;
|
||||
|
||||
if (confirm(`Are you sure you want to delete the profile "${profile.name}"?`)) {
|
||||
try {
|
||||
const result = await window.electronAPI.deleteProfile(profileId);
|
||||
if (result.success) {
|
||||
await loadProfiles();
|
||||
updateProfilesList();
|
||||
|
||||
// Clear selection if deleted profile was selected
|
||||
if (profileSelect.value === profileId) {
|
||||
profileSelect.value = '';
|
||||
await handleProfileSelection();
|
||||
}
|
||||
|
||||
showStatus(connectionStatus, 'Profile deleted successfully!', 'success');
|
||||
} else {
|
||||
alert(`Failed to delete profile: ${result.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting profile:', error);
|
||||
alert('Error deleting profile');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// toggleCookieSection is attached via addEventListener in the event listeners section above
|
||||
|
||||
// Handle cookie received from Chrome extension via local HTTP bridge
|
||||
async function handleExtensionCookie(data) {
|
||||
const { deploymentUrl, cookies, cookieValue } = data;
|
||||
@@ -1014,15 +541,8 @@ async function handleExtensionCookie(data) {
|
||||
updateConnectionStatus(true);
|
||||
updateButtonStates();
|
||||
|
||||
// Auto-populate cookie key and expand cookie proxy section
|
||||
// Auto-populate cookie key
|
||||
cookieKey.value = cookieValue;
|
||||
const cookieContent = document.getElementById('cookieProxyContent');
|
||||
const cookieIcon = document.getElementById('cookieCollapseIcon');
|
||||
if (cookieContent.style.display === 'none') {
|
||||
cookieContent.style.display = 'block';
|
||||
cookieIcon.textContent = '\u25B2';
|
||||
cookieIcon.classList.add('expanded');
|
||||
}
|
||||
updateCookieProxyButtonStates();
|
||||
|
||||
// Fetch devices
|
||||
@@ -1044,59 +564,4 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
// Listen for cookies pushed from Chrome extension
|
||||
window.electronAPI.onExtensionCookie(handleExtensionCookie);
|
||||
|
||||
// Load saved profiles
|
||||
await loadProfiles();
|
||||
|
||||
// Check camera proxy executable availability
|
||||
await checkCameraProxyAvailability();
|
||||
});
|
||||
|
||||
// Check if camera proxy executable is available
|
||||
async function checkCameraProxyAvailability() {
|
||||
try {
|
||||
const result = await window.electronAPI.checkCameraProxy();
|
||||
|
||||
if (!result.exists) {
|
||||
showStatus(connectionStatus, 'Warning: aware-cam-proxy-win.exe not found. Camera proxy functionality will not work.', 'warning');
|
||||
// Disable proxy-related buttons
|
||||
startProxyBtn.disabled = true;
|
||||
checkVersionBtn.disabled = true;
|
||||
|
||||
// Add a tooltip or visual indicator
|
||||
startProxyBtn.title = 'Camera proxy executable not found';
|
||||
checkVersionBtn.title = 'Camera proxy executable not found';
|
||||
} else {
|
||||
console.log('Camera proxy executable found at:', result.path);
|
||||
|
||||
// Optionally get version info
|
||||
try {
|
||||
const versionResult = await window.electronAPI.getCameraProxyVersion();
|
||||
if (versionResult.success) {
|
||||
console.log('Camera proxy version:', versionResult.version);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not get camera proxy version:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking camera proxy availability:', error);
|
||||
showStatus(connectionStatus, 'Error checking camera proxy availability', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle cookie section visibility
|
||||
function toggleCookieSection() {
|
||||
const content = document.getElementById('cookieProxyContent');
|
||||
const icon = document.getElementById('cookieCollapseIcon');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
icon.textContent = '▲';
|
||||
icon.classList.add('expanded');
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
icon.textContent = '▼';
|
||||
icon.classList.remove('expanded');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user