diff --git a/index.html b/index.html index 7060dde..273c30d 100644 --- a/index.html +++ b/index.html @@ -78,12 +78,6 @@ - -
-
-
-
- diff --git a/main.js b/main.js index 96cc340..583825b 100644 --- a/main.js +++ b/main.js @@ -1,11 +1,9 @@ -const { app, BrowserWindow, ipcMain, shell, clipboard } = require('electron'); +const { app, BrowserWindow, ipcMain, shell } = require('electron'); const path = require('path'); const fs = require('fs'); const os = require('os'); const axios = require('axios'); -const CryptoJS = require('crypto-js'); const { spawn } = require('child_process'); -const crypto = require('crypto'); const http = require('http'); let mainWindow; @@ -21,102 +19,6 @@ function sanitizeBatchInput(input) { return input.replace(/[&|<>^%"`!]/g, ''); } -// Profile management -const PROFILES_FILE = path.join(os.homedir(), '.alta-api-profiles.json'); -// Derive encryption key from machine-specific identifiers so the profiles file -// is only decryptable on this machine. Falls back to a static key if derivation fails. -function deriveEncryptionKey() { - try { - const machineFactors = [ - os.hostname(), - os.homedir(), - os.userInfo().username - ].join('|'); - return crypto.createHash('sha256').update('alta-proxy-' + machineFactors).digest('hex'); - } catch { - return 'alta-api-client-key-2024-fallback'; - } -} -const ENCRYPTION_KEY = deriveEncryptionKey(); - -// Helper functions for profile management -function getProfilesFilePath() { - return PROFILES_FILE; -} - -function encryptPassword(password) { - return CryptoJS.AES.encrypt(password, ENCRYPTION_KEY).toString(); -} - -function decryptPassword(encryptedPassword) { - try { - const bytes = CryptoJS.AES.decrypt(encryptedPassword, ENCRYPTION_KEY); - return bytes.toString(CryptoJS.enc.Utf8); - } catch (error) { - console.error('Failed to decrypt password:', error); - return ''; - } -} - -// Legacy key for migrating existing profiles -const LEGACY_ENCRYPTION_KEY = 'alta-api-client-key-2024'; - -function decryptPasswordLegacy(encryptedPassword) { - try { - const bytes = CryptoJS.AES.decrypt(encryptedPassword, LEGACY_ENCRYPTION_KEY); - const result = bytes.toString(CryptoJS.enc.Utf8); - return result || null; - } catch { - return null; - } -} - -// Migrate profiles from legacy encryption key to machine-derived key -function migrateProfilesIfNeeded(profiles) { - let migrated = false; - for (const profile of profiles) { - if (!profile.password) continue; - // Try decrypting with current key first - const currentDecrypt = decryptPassword(profile.password); - if (currentDecrypt) continue; - // Try legacy key - const legacyDecrypt = decryptPasswordLegacy(profile.password); - if (legacyDecrypt) { - profile.password = encryptPassword(legacyDecrypt); - migrated = true; - } - } - if (migrated) { - saveProfiles(profiles); - console.log('Migrated profiles to new encryption key'); - } - return profiles; -} - -function loadProfiles() { - try { - if (fs.existsSync(PROFILES_FILE)) { - const data = fs.readFileSync(PROFILES_FILE, 'utf8'); - const profiles = JSON.parse(data); - if (!Array.isArray(profiles)) return []; - return migrateProfilesIfNeeded(profiles); - } - } catch (error) { - console.error('Failed to load profiles:', error); - } - return []; -} - -function saveProfiles(profiles) { - try { - fs.writeFileSync(PROFILES_FILE, JSON.stringify(profiles, null, 2)); - return true; - } catch (error) { - console.error('Failed to save profiles:', error); - return false; - } -} - function startCookieServer() { cookieServer = http.createServer((req, res) => { // CORS headers — only allow Chrome extension origins @@ -277,33 +179,6 @@ app.on('activate', () => { }); // IPC handlers for API communication -ipcMain.handle('api-login', async (event, { deploymentUrl, username, password }) => { - try { - const loginUrl = `${deploymentUrl}/api/v1/dologin`; - const response = await axios.post(loginUrl, { - username: username, - password: password - }, { - timeout: 10000, - withCredentials: true - }); - - // Store cookies for subsequent requests - const cookies = response.headers['set-cookie']; - return { - success: true, - cookies: cookies, - message: 'Login successful' - }; - } catch (error) { - console.error('Login error:', error); - return { - success: false, - message: error.response?.data?.message || error.message || 'Login failed' - }; - } -}); - ipcMain.handle('api-get-devices', async (event, { deploymentUrl, cookies }) => { try { const devicesUrl = `${deploymentUrl}/api/v1/devices`; @@ -358,266 +233,6 @@ ipcMain.handle('api-get-auth-info', async (event, { deploymentUrl, cookies }) => } }); -// Profile management IPC handlers -ipcMain.handle('profiles-load', async () => { - try { - const profiles = loadProfiles(); - // Return profiles without passwords for security - const safeProfiles = profiles.map(profile => ({ - id: profile.id, - name: profile.name, - deploymentUrl: profile.deploymentUrl, - username: profile.username - })); - return { success: true, profiles: safeProfiles }; - } catch (error) { - console.error('Failed to load profiles:', error); - return { success: false, message: 'Failed to load profiles' }; - } -}); - -ipcMain.handle('profiles-save', async (event, { name, deploymentUrl, username, password }) => { - try { - const profiles = loadProfiles(); - const newProfile = { - id: Date.now().toString(), - name: name, - deploymentUrl: deploymentUrl, - username: username, - password: encryptPassword(password), - createdAt: new Date().toISOString() - }; - - profiles.push(newProfile); - const saved = saveProfiles(profiles); - - if (saved) { - return { - success: true, - message: 'Profile saved successfully', - profile: { - id: newProfile.id, - name: newProfile.name, - deploymentUrl: newProfile.deploymentUrl, - username: newProfile.username - } - }; - } else { - return { success: false, message: 'Failed to save profile' }; - } - } catch (error) { - console.error('Failed to save profile:', error); - return { success: false, message: 'Failed to save profile' }; - } -}); - -ipcMain.handle('profiles-get', async (event, { profileId }) => { - try { - const profiles = loadProfiles(); - const profile = profiles.find(p => p.id === profileId); - - if (profile) { - return { - success: true, - profile: { - id: profile.id, - name: profile.name, - deploymentUrl: profile.deploymentUrl, - username: profile.username, - password: decryptPassword(profile.password) - } - }; - } else { - return { success: false, message: 'Profile not found' }; - } - } catch (error) { - console.error('Failed to get profile:', error); - return { success: false, message: 'Failed to get profile' }; - } -}); - -ipcMain.handle('profiles-delete', async (event, { profileId }) => { - try { - const profiles = loadProfiles(); - const filteredProfiles = profiles.filter(p => p.id !== profileId); - - if (filteredProfiles.length < profiles.length) { - const saved = saveProfiles(filteredProfiles); - if (saved) { - return { success: true, message: 'Profile deleted successfully' }; - } else { - return { success: false, message: 'Failed to save changes' }; - } - } else { - return { success: false, message: 'Profile not found' }; - } - } catch (error) { - console.error('Failed to delete profile:', error); - return { success: false, message: 'Failed to delete profile' }; - } -}); - -ipcMain.handle('profiles-update', async (event, { profileId, name, deploymentUrl, username, password }) => { - try { - const profiles = loadProfiles(); - const profileIndex = profiles.findIndex(p => p.id === profileId); - - if (profileIndex !== -1) { - profiles[profileIndex] = { - ...profiles[profileIndex], - name: name, - deploymentUrl: deploymentUrl, - username: username, - password: password ? encryptPassword(password) : profiles[profileIndex].password, - updatedAt: new Date().toISOString() - }; - - const saved = saveProfiles(profiles); - if (saved) { - return { - success: true, - message: 'Profile updated successfully', - profile: { - id: profiles[profileIndex].id, - name: profiles[profileIndex].name, - deploymentUrl: profiles[profileIndex].deploymentUrl, - username: profiles[profileIndex].username - } - }; - } else { - return { success: false, message: 'Failed to save changes' }; - } - } else { - return { success: false, message: 'Profile not found' }; - } - } catch (error) { - console.error('Failed to update profile:', error); - return { success: false, message: 'Failed to update profile' }; - } -}); - -// Camera Proxy functionality -ipcMain.handle('camera-proxy-launch', async (event, { deploymentUrl, username, password, deviceUuid }) => { - try { - // Path to the camera proxy executable - const proxyExePath = path.join(__dirname, 'aware-cam-proxy-win.exe'); - - // Check if the executable exists - if (!fs.existsSync(proxyExePath)) { - return { - success: false, - message: 'Camera proxy executable not found. Please ensure aware-cam-proxy-win.exe is in the application directory.' - }; - } - - // Extract the domain from the deployment URL - let domain = deploymentUrl; - if (domain.startsWith('https://')) { - domain = domain.substring(8); - } else if (domain.startsWith('http://')) { - domain = domain.substring(7); - } - // Remove trailing path segments - domain = domain.split('/')[0]; - - // Sanitize all inputs before embedding in batch file - const safeDomain = sanitizeBatchInput(domain); - const safeUsername = sanitizeBatchInput(username); - const safeDeviceUuid = sanitizeBatchInput(deviceUuid); - - if (!safeDomain || !safeUsername || !safeDeviceUuid) { - return { - success: false, - message: 'Invalid characters detected in connection parameters. Please check your profile settings.' - }; - } - - // Copy password to clipboard for easy pasting, then clear after 30 seconds - clipboard.writeText(password); - setTimeout(() => { - try { - if (clipboard.readText() === password) { - clipboard.clear(); - } - } catch { /* ignore clipboard errors */ } - }, 30000); - - // Create a batch file to launch the camera proxy with proper console - const batchContent = `@echo off -echo Launching Alta Video Camera Proxy... -echo Domain: ${safeDomain} -echo Username: ${safeUsername} -echo Device UUID: ${safeDeviceUuid} -echo. -echo *** PASSWORD HAS BEEN COPIED TO CLIPBOARD *** -echo When prompted for password, simply press Ctrl+V to paste and then Enter. -echo. -"${proxyExePath}" -a "${safeDomain}" -u "${safeUsername}" -d "${safeDeviceUuid}" -echo. -echo Camera proxy has finished. Press any key to close this window. -pause >nul`; - - const tempDir = os.tmpdir(); - const batchPath = path.join(tempDir, `camera-proxy-${Date.now()}.bat`); - - // Write the batch file - fs.writeFileSync(batchPath, batchContent); - - console.log('Launching camera proxy via batch file:', batchPath); - console.log('Command will be: aware-cam-proxy-win.exe -a', safeDomain, '-u', safeUsername, '-d', safeDeviceUuid); - - // Launch the batch file in a new command prompt window - const cmdProcess = spawn('cmd', ['/c', 'start', 'cmd', '/k', batchPath], { - detached: true, - stdio: 'ignore' - }); - - // Store the process information for later termination - const processInfo = { - process: cmdProcess, - batchPath: batchPath, - deviceUuid: safeDeviceUuid, - startTime: Date.now(), - username: safeUsername, - domain: safeDomain - }; - - activeProxyProcesses.set(cmdProcess.pid, processInfo); - - // Clean up the batch file after a delay - setTimeout(() => { - try { - if (fs.existsSync(batchPath)) { - fs.unlinkSync(batchPath); - } - } catch (error) { - console.log('Could not clean up batch file:', error.message); - } - }, 60000); // Clean up after 1 minute - - cmdProcess.unref(); // Allow the parent process to exit independently - - // Clean up process tracking when it exits - cmdProcess.on('exit', () => { - activeProxyProcesses.delete(cmdProcess.pid); - }); - - return { - success: true, - message: `Camera proxy launched for ${deviceUuid}! Password copied to clipboard - press Ctrl+V to paste when prompted.`, - processId: cmdProcess.pid, - deviceUuid: deviceUuid - }; - - } catch (error) { - console.error('Failed to launch camera proxy:', error); - return { - success: false, - message: `Failed to launch camera proxy: ${error.message}` - }; - } -}); - // Cookie-based camera proxy functionality ipcMain.handle('camera-proxy-cookie-launch', async (event, { deploymentUrl, cookieKey, deviceUuid }) => { try { @@ -860,103 +475,3 @@ ipcMain.handle('camera-proxy-stop', async (event, { processId }) => { } }); -// Check if camera proxy executable exists -ipcMain.handle('camera-proxy-check', async () => { - try { - const proxyExePath = path.join(__dirname, 'aware-cam-proxy-win.exe'); - const exists = fs.existsSync(proxyExePath); - - return { - success: true, - exists: exists, - path: proxyExePath - }; - } catch (error) { - return { - success: false, - exists: false, - message: error.message - }; - } -}); - -// Get version of camera proxy -ipcMain.handle('camera-proxy-version', async () => { - try { - const proxyExePath = path.join(__dirname, 'aware-cam-proxy-win.exe'); - - if (!fs.existsSync(proxyExePath)) { - return { - success: false, - message: 'Camera proxy executable not found' - }; - } - - return new Promise((resolve) => { - const versionProcess = spawn(proxyExePath, ['-v'], { - stdio: ['pipe', 'pipe', 'pipe'] - }); - - let output = ''; - - versionProcess.stdout.on('data', (data) => { - output += data.toString(); - }); - - versionProcess.stderr.on('data', (data) => { - output += data.toString(); - }); - - versionProcess.on('close', (code) => { - resolve({ - success: true, - version: output.trim(), - exitCode: code - }); - }); - - versionProcess.on('error', (error) => { - resolve({ - success: false, - message: error.message - }); - }); - - // Timeout after 5 seconds - setTimeout(() => { - versionProcess.kill(); - resolve({ - success: false, - message: 'Version check timed out' - }); - }, 5000); - }); - } catch (error) { - return { - success: false, - message: error.message - }; - } -}); - -// Get list of active camera proxy processes -ipcMain.handle('camera-proxy-list-active', async () => { - try { - const activeProcesses = Array.from(activeProxyProcesses.entries()).map(([pid, info]) => ({ - processId: pid, - deviceUuid: info.deviceUuid, - startTime: info.startTime - })); - - return { - success: true, - processes: activeProcesses - }; - } catch (error) { - return { - success: false, - message: error.message, - processes: [] - }; - } -}); \ No newline at end of file diff --git a/preload.js b/preload.js index 47a22bd..d0b3531 100644 --- a/preload.js +++ b/preload.js @@ -3,24 +3,12 @@ const { contextBridge, ipcRenderer } = require('electron'); // Expose protected methods that allow the renderer process to use // the ipcRenderer without exposing the entire object contextBridge.exposeInMainWorld('electronAPI', { - login: (credentials) => ipcRenderer.invoke('api-login', credentials), getDevices: (params) => ipcRenderer.invoke('api-get-devices', params), getAuthInfo: (params) => ipcRenderer.invoke('api-get-auth-info', params), - - // Profile management - loadProfiles: () => ipcRenderer.invoke('profiles-load'), - saveProfile: (profile) => ipcRenderer.invoke('profiles-save', profile), - getProfile: (profileId) => ipcRenderer.invoke('profiles-get', { profileId }), - deleteProfile: (profileId) => ipcRenderer.invoke('profiles-delete', { profileId }), - updateProfile: (profileId, profile) => ipcRenderer.invoke('profiles-update', { profileId, ...profile }), - + // Camera proxy functionality - launchCameraProxy: (params) => ipcRenderer.invoke('camera-proxy-launch', params), launchCookieCameraProxy: (params) => ipcRenderer.invoke('camera-proxy-cookie-launch', params), stopCameraProxy: (processId) => ipcRenderer.invoke('camera-proxy-stop', { processId }), - checkCameraProxy: () => ipcRenderer.invoke('camera-proxy-check'), - getCameraProxyVersion: () => ipcRenderer.invoke('camera-proxy-version'), - listActiveCameraProxies: () => ipcRenderer.invoke('camera-proxy-list-active'), // Extension cookie bridge (push from main process) onExtensionCookie: (callback) => { diff --git a/renderer.js b/renderer.js index a8b9e1c..4869437 100644 --- a/renderer.js +++ b/renderer.js @@ -9,7 +9,6 @@ let sessionData = { 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 deviceSearch = document.getElementById('deviceSearch'); @@ -444,66 +443,6 @@ function selectDevice(device, deviceElement) { updateCookieProxyButtonStates(); } -// Display detailed device information -function displayDeviceDetails(device) { - const deviceStatus = getDeviceStatus(device); - - const detailsHtml = ` -
-
-

${escapeHtml(device.name || 'Unnamed Device')}

- - ${escapeHtml(deviceStatus.statusText)} - -
- -
-
-
Device ID
-
${escapeHtml(device.guid || device.id || 'N/A')}
-
- -
-
Device Type
-
${escapeHtml(device.type || 'Unknown')}
-
- -
-
Model
-
${escapeHtml(device.model || 'Unknown')}
-
- -
-
IP Address
-
${escapeHtml(device.ipAddress || 'N/A')}
-
- -
-
MAC Address
-
${escapeHtml(device.macAddress || 'N/A')}
-
- -
-
Firmware Version
-
${escapeHtml(device.firmwareVersion || 'N/A')}
-
- -
-
Serial Number
-
${escapeHtml(device.serialNumber || 'N/A')}
-
- -
-
Location
-
${escapeHtml(device.location || 'N/A')}
-
-
-
- `; - - deviceDetails.innerHTML = detailsHtml; -} - // Utility functions function showStatus(element, message, type) { element.textContent = message; diff --git a/styles.css b/styles.css index 0e355c4..5bfc2fe 100644 --- a/styles.css +++ b/styles.css @@ -42,37 +42,6 @@ body { flex-direction: column; } -/* Navigation Tabs */ -.nav-tabs { - display: flex; - background: var(--bg-secondary); - border-bottom: 1px solid var(--border); - padding: 0; -} - -.nav-tab { - background: transparent; - border: none; - color: var(--text-secondary); - padding: 12px 24px; - font-size: 12px; - font-weight: bold; - cursor: pointer; - transition: all 0.2s ease; - border-bottom: 2px solid transparent; -} - -.nav-tab:hover { - background: var(--hover-bg); - color: var(--text-primary); -} - -.nav-tab.active { - color: var(--tab-active); - border-bottom-color: var(--tab-active); - background: var(--bg-primary); -} - /* Main Layout */ .main-layout { flex: 1; @@ -172,31 +141,6 @@ body { border-color: var(--accent-primary); } -.device-item.proxy-active { - border-color: var(--success); - box-shadow: 0 0 8px rgba(76, 175, 80, 0.3); -} - -.device-item.proxy-active::before { - content: "PROXY ACTIVE"; - position: absolute; - top: -8px; - right: 8px; - background: var(--success); - color: white; - font-size: 10px; - font-weight: bold; - padding: 2px 6px; - border-radius: 8px; - text-transform: uppercase; - letter-spacing: 0.5px; -} - -.device-item.selected.proxy-active { - border-color: var(--success); - box-shadow: 0 0 8px rgba(76, 175, 80, 0.5); -} - .device-name { font-size: 14px; font-weight: bold; @@ -269,45 +213,6 @@ body { letter-spacing: 0.5px; } -/* Profile Controls - Smaller and more compact */ -.profile-section { - padding: 12px 20px; -} - -.profile-controls { - margin-bottom: 0; -} - -.profile-row { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 0; -} - -.profile-row label { - font-size: 14px; - font-weight: bold; - color: var(--text-primary); - min-width: 100px; -} - -.profile-row select { - flex: 1; - padding: 8px 12px; - border: 1px solid var(--button-outline); - border-radius: 4px; - font-size: 14px; - background: var(--input-bg); - color: var(--text-primary); - max-width: 250px; -} - -.profile-row select:focus { - outline: none; - border-color: var(--accent-primary); -} - /* Buttons */ button { font-family: inherit; @@ -354,39 +259,6 @@ button:disabled { cursor: not-allowed; } -/* Selected Profile Info */ -.selected-profile-info { - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 4px; - padding: 12px; - margin-top: 8px; -} - -.selected-profile-info p { - margin: 0; - color: var(--text-secondary); - font-style: italic; - font-size: 8px; -} - -.selected-profile-info.has-profile p { - color: var(--text-primary); - font-style: normal; -} - -.profile-detail { - display: flex; - justify-content: space-between; - margin: 4px 0; - font-size: 8px; -} - -.profile-detail strong { - color: var(--text-primary); - font-weight: bold; -} - /* Connection Status */ .connection-status { margin-bottom: 16px; @@ -522,208 +394,6 @@ button:disabled { border-color: var(--accent-primary); } -/* Modal Styles */ -.modal { - display: none; - position: fixed; - z-index: 1000; - left: 0; - top: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.7); - backdrop-filter: blur(4px); -} - -.modal-content { - background-color: var(--card-bg); - margin: 2vh auto; - padding: 0; - border-radius: 8px; - width: clamp(400px, 80vw, 800px); - max-height: 90vh; - border: 1px solid var(--border); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); - animation: modalSlideIn 0.3s ease; - overflow: hidden; - display: flex; - flex-direction: column; -} - -@keyframes modalSlideIn { - from { - opacity: 0; - transform: translateY(-30px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.modal-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 16px 20px; - border-bottom: 1px solid var(--border); - background: var(--bg-secondary); - border-radius: 8px 8px 0 0; -} - -.modal-header h3 { - margin: 0; - font-size: clamp(16px, 2.5vw, 20px); - font-weight: bold; - color: var(--text-primary); -} - -.close { - color: var(--text-secondary); - font-size: clamp(20px, 3vw, 28px); - font-weight: bold; - cursor: pointer; - line-height: 1; - transition: color 0.2s ease; -} - -.close:hover { - color: var(--text-primary); -} - -.modal form { - padding: 20px; -} - -.form-group { - margin-bottom: 16px; -} - -.form-group label { - display: block; - margin-bottom: 8px; - font-weight: bold; - font-size: clamp(14px, 2vw, 18px); - color: var(--text-primary); -} - -.form-group input { - width: 100%; - padding: clamp(8px, 1.5vw, 12px) clamp(12px, 2vw, 16px); - border: 1px solid var(--button-outline); - border-radius: 4px; - font-size: clamp(14px, 2vw, 18px); - background: var(--input-bg); - color: var(--text-primary); - transition: border-color 0.2s ease; -} - -.form-group input:focus { - outline: none; - border-color: var(--accent-primary); -} - -.modal-buttons { - display: flex; - justify-content: flex-end; - gap: 8px; - padding: 16px 20px; - border-top: 1px solid var(--border); - background: var(--bg-secondary); - border-radius: 0 0 8px 8px; -} - -.modal-btn { - padding: clamp(8px, 1.5vw, 12px) clamp(16px, 2.5vw, 24px); - font-size: clamp(14px, 2vw, 18px); - font-weight: bold; -} - -.modal-btn.primary { - background: var(--accent-primary); - color: white; -} - -.modal-btn.secondary { - background: var(--button-outline); - color: var(--text-primary); -} - -/* Profiles List */ -.profiles-list { - max-height: 60vh; - overflow-y: auto; - padding: clamp(16px, 2.5vw, 24px) clamp(20px, 3vw, 32px); - flex: 1; -} - -.profile-item { - display: flex; - justify-content: space-between; - align-items: center; - padding: clamp(12px, 2vw, 18px); - margin-bottom: clamp(8px, 1.5vw, 12px); - background: var(--bg-secondary); - border: 1px solid var(--border); - border-radius: 4px; - transition: background-color 0.2s ease; -} - -.profile-item:hover { - background: var(--hover-bg); -} - -.profile-info h4 { - margin: 0 0 clamp(4px, 1vw, 8px) 0; - color: var(--text-primary); - font-size: clamp(14px, 2vw, 18px); - font-weight: bold; -} - -.profile-info p { - margin: 0; - color: var(--text-secondary); - font-size: clamp(12px, 1.8vw, 16px); -} - -.profile-actions { - display: flex; - gap: 6px; -} - -.profile-action-btn { - padding: clamp(6px, 1vw, 10px) clamp(8px, 1.5vw, 12px); - font-size: clamp(12px, 1.8vw, 16px); - font-weight: bold; - border: none; - border-radius: 3px; - cursor: pointer; - transition: all 0.2s ease; - min-width: clamp(50px, 8vw, 80px); -} - -.profile-action-btn.edit { - background: var(--warning); - color: white; -} - -.profile-action-btn.delete { - background: var(--error); - color: white; -} - -.profile-action-btn:hover { - opacity: 0.8; -} - -.no-profiles { - text-align: center; - color: var(--text-secondary); - font-style: italic; - padding: clamp(30px, 5vw, 50px); - font-size: clamp(14px, 2vw, 18px); -} - /* Dark Scrollbars */ ::-webkit-scrollbar { width: 6px; @@ -759,44 +429,16 @@ button:disabled { .devices-sidebar { width: 150px; } - + .main-content { padding: 16px; } - - .profile-row, + .connection-controls, .proxy-buttons { flex-direction: column; gap: 8px; } - - .profile-row select { - max-width: none; - } - - .modal-content { - width: 95vw; - margin: 1vh auto; - } - - .profile-item { - flex-direction: column; - align-items: flex-start; - gap: 12px; - } - - .profile-actions { - align-self: stretch; - justify-content: flex-end; - } -} - -/* Large screen optimizations */ -@media (min-width: 1200px) { - .modal-content { - max-width: 900px; - } } /* Focus States for Accessibility */ @@ -816,43 +458,3 @@ select:focus { } } -/* Collapsible Section Styles */ -.collapsible-header { - display: flex; - align-items: center; - justify-content: space-between; - cursor: pointer; - padding: 0; - margin: 0 0 16px 0; - transition: all 0.2s ease; -} - -.collapsible-header:hover { - opacity: 0.8; -} - -.collapsible-header h2 { - margin: 0; - flex: 1; -} - -.collapse-icon { - font-size: 16px; - color: var(--accent-primary); - font-weight: bold; - transition: transform 0.3s ease; - margin-left: 8px; -} - -.collapse-icon.expanded { - transform: rotate(180deg); -} - -.collapsible-content { - transition: all 0.3s ease; - overflow: hidden; -} - -.collapsible-content.collapsed { - display: none !important; -} \ No newline at end of file