// ==UserScript==
// @name Enhanced Wyze Group Selector & Camera Management
// @namespace http://ptelectronics.net
// @version 2.3
// @description Adds camera group management with persistent settings, minimizable UI, and person detection toggle for Wyze Events page
// @author Math Shamenson
// @match https://my.wyze.com/events*
// @grant none
// @license MIT
// @run-at document-idle
// @homepageURL https://greasyfork.org/scripts/SCRIPT_ID
// @supportURL https://greasyfork.org/scripts/SCRIPT_ID/feedback
// ==/UserScript==
(function () {
'use strict';
// Enhanced state management with validation
class GroupManager {
constructor() {
this.groups = this.loadGroups();
this.uiState = this.loadUIState();
}
loadGroups() {
try {
const stored = localStorage.getItem('cameraGroups');
return stored ? JSON.parse(stored) : {
"Group 1: Back Yard": ["2CAA8E86C673", "2CAA8E09C409"],
"Group 2: Front Yard": ["2CAA8E59E1AA", "2CAA8E6E0638"],
"Group 3: Misc Cameras": ["2CAA8E778175", "2CAA8E52C9FA"]
};
} catch (error) {
console.error('Error loading groups:', error);
return {};
}
}
loadUIState() {
try {
const stored = localStorage.getItem('cameraGroupsUIState');
return stored ? JSON.parse(stored) : { minimized: false };
} catch (error) {
console.error('Error loading UI state:', error);
return { minimized: false };
}
}
saveUIState(state) {
try {
localStorage.setItem('cameraGroupsUIState', JSON.stringify(state));
this.uiState = state;
} catch (error) {
console.error('Error saving UI state:', error);
}
}
saveGroups() {
try {
localStorage.setItem('cameraGroups', JSON.stringify(this.groups));
} catch (error) {
console.error('Error saving groups:', error);
alert('Failed to save groups. Please check console for details.');
}
}
addGroup(name, cameras) {
if (!name || !cameras || !Array.isArray(cameras)) {
throw new Error('Invalid group data');
}
this.groups[name] = cameras;
this.saveGroups();
}
updateGroup(oldName, newName, cameras) {
if (!oldName || !newName || !cameras || !Array.isArray(cameras)) {
throw new Error('Invalid group update data');
}
delete this.groups[oldName];
this.groups[newName] = cameras;
this.saveGroups();
}
deleteGroup(name) {
if (!name || !this.groups[name]) {
throw new Error('Invalid group name');
}
delete this.groups[name];
this.saveGroups();
}
}
// UI Component with improved styling
class ControlPanel {
constructor(groupManager) {
this.groupManager = groupManager;
this.position = this.loadPosition();
this.createPanel();
// Initialize minimized state from persistent storage
if (this.groupManager.uiState.minimized) {
this.toggleMinimize(false); // Don't save state on initial load
}
}
loadPosition() {
const stored = localStorage.getItem('controlPanelPosition');
if (stored) {
return JSON.parse(stored);
}
// Calculate initial position from right side
const initialLeft = window.innerWidth - 260; // 250px width + 10px margin
return { top: '50px', left: `${initialLeft}px` };
}
savePosition() {
localStorage.setItem('controlPanelPosition', JSON.stringify({
top: this.container.style.top,
left: this.container.style.left
}));
}
createPanel() {
const existingPanel = document.getElementById('wyze-control-panel');
if (existingPanel) existingPanel.remove();
this.container = document.createElement('div');
this.container.id = 'wyze-control-panel';
this.applyStyles();
this.setupDraggable();
this.createContent();
document.body.appendChild(this.container);
}
applyStyles() {
Object.assign(this.container.style, {
position: 'fixed',
top: this.position.top,
left: this.position.left,
background: 'white',
border: '2px solid #4a90e2',
borderRadius: '8px',
padding: '15px',
zIndex: '10000',
maxHeight: '90vh',
overflowY: 'auto',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.1)',
width: '250px' // Fixed width instead of minWidth
});
}
setupDraggable() {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
this.container.addEventListener('mousedown', (e) => {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
initialX = e.clientX - this.container.offsetLeft;
initialY = e.clientY - this.container.offsetTop;
});
document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
e.preventDefault();
// Calculate new position
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
// Constrain to window bounds
const maxX = window.innerWidth - this.container.offsetWidth;
const maxY = window.innerHeight - this.container.offsetHeight;
currentX = Math.max(0, Math.min(currentX, maxX));
currentY = Math.max(0, Math.min(currentY, maxY));
this.container.style.left = `${currentX}px`;
this.container.style.top = `${currentY}px`;
});
document.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
this.savePosition();
}
});
}
createContent() {
// Header (outside of content div)
const header = this.createHeader();
this.container.appendChild(header);
// Main content
const content = document.createElement('div');
content.id = 'control-panel-content';
content.style.display = 'block'; // Ensure initial state is visible
// Groups
Object.entries(this.groupManager.groups).forEach(([groupName, cameras]) => {
const groupElement = this.createGroupElement(groupName, cameras);
content.appendChild(groupElement);
});
// Control buttons
const controls = this.createControls();
content.appendChild(controls);
this.container.appendChild(content);
}
createHeader() {
const header = document.createElement('div');
header.id = 'control-panel-header';
header.style.cssText = `
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
border-radius: 6px 6px 0 0;
`;
const title = document.createElement('div');
title.textContent = 'Wyze Camera Controls';
title.style.cssText = `
font-weight: bold;
font-size: 16px;
color: #4a90e2;
`;
const minimizeBtn = document.createElement('button');
minimizeBtn.textContent = '−';
minimizeBtn.style.cssText = `
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #4a90e2;
padding: 0 5px;
`;
minimizeBtn.onclick = () => this.toggleMinimize();
header.appendChild(title);
header.appendChild(minimizeBtn);
return header;
}
createGroupElement(groupName, cameras) {
const container = document.createElement('div');
container.style.marginBottom = '10px';
const groupButton = document.createElement('button');
groupButton.textContent = groupName;
groupButton.style.cssText = `
background: #4a90e2;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
margin-right: 5px;
flex: 1;
`;
const editButton = document.createElement('button');
editButton.textContent = 'Edit';
editButton.style.cssText = `
background: #f5a623;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
`;
const deleteButton = document.createElement('button');
deleteButton.textContent = '×';
deleteButton.style.cssText = `
background: #d0021b;
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
margin-left: 5px;
`;
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.appendChild(groupButton);
buttonContainer.appendChild(editButton);
buttonContainer.appendChild(deleteButton);
groupButton.onclick = () => this.selectGroup(cameras);
editButton.onclick = () => this.editGroup(groupName);
deleteButton.onclick = () => this.deleteGroup(groupName);
container.appendChild(buttonContainer);
return container;
}
createControls() {
const container = document.createElement('div');
container.style.marginTop = '15px';
const addGroupBtn = document.createElement('button');
addGroupBtn.textContent = '+ Add Group';
addGroupBtn.style.cssText = `
background: #7ed321;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
`;
const togglePersonBtn = document.createElement('button');
togglePersonBtn.textContent = 'Toggle Person Detection';
togglePersonBtn.style.cssText = `
background: #9013fe;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
`;
addGroupBtn.onclick = () => this.addGroup();
togglePersonBtn.onclick = () => this.togglePersonDetection();
container.appendChild(addGroupBtn);
container.appendChild(togglePersonBtn);
return container;
}
toggleMinimize(saveState = true) {
const content = this.container.querySelector('#control-panel-content');
const header = this.container.querySelector('#control-panel-header');
const minimizeBtn = header.querySelector('button');
const isMinimized = content.style.display === 'none';
if (isMinimized) {
content.style.display = 'block';
minimizeBtn.textContent = '−';
this.container.style.padding = '15px';
} else {
content.style.display = 'none';
minimizeBtn.textContent = '+';
this.container.style.padding = '0';
}
// Maintain the header's border radius
header.style.borderRadius = isMinimized ? '6px' : '6px 6px 0 0';
// Save minimized state if requested
if (saveState) {
this.groupManager.saveUIState({ minimized: !isMinimized });
}
}
async selectGroup(cameras) {
for (const camera of cameras) {
const checkbox = document.querySelector(`input[name="${camera}"]`);
if (checkbox) {
const parent = checkbox.closest('.MuiButtonBase-root');
if (parent) {
parent.click();
await new Promise(resolve => setTimeout(resolve, 50));
}
}
}
}
addGroup() {
const name = prompt('Enter a name for the new group:');
if (!name) return;
const selectedCameras = Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => cb.name);
if (selectedCameras.length === 0) {
alert('Please select at least one camera.');
return;
}
try {
this.groupManager.addGroup(name, selectedCameras);
this.createPanel();
} catch (error) {
console.error('Error adding group:', error);
alert('Failed to add group. Please try again.');
}
}
editGroup(groupName) {
const newName = prompt('Edit group name:', groupName);
if (!newName) return;
const selectedCameras = Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
.map(cb => cb.name);
if (selectedCameras.length === 0) {
alert('Please select at least one camera.');
return;
}
try {
this.groupManager.updateGroup(groupName, newName, selectedCameras);
this.createPanel();
} catch (error) {
console.error('Error updating group:', error);
alert('Failed to update group. Please try again.');
}
}
deleteGroup(groupName) {
if (confirm(`Are you sure you want to delete "${groupName}"?`)) {
try {
this.groupManager.deleteGroup(groupName);
this.createPanel();
} catch (error) {
console.error('Error deleting group:', error);
alert('Failed to delete group. Please try again.');
}
}
}
togglePersonDetection() {
const personButton = Array.from(document.querySelectorAll('button'))
.find(btn => btn.innerText.trim().toLowerCase() === 'person');
if (personButton) {
personButton.click();
} else {
alert('Person Detection button not found. Please check if the UI has changed.');
}
}
}
// Custom CSS for improved video duration text
function enhanceVideoText() {
const style = document.createElement('style');
style.textContent = `
.css-1a78lvj {
font-size: 18px !important;
font-weight: bold !important;
color: #4a90e2 !important;
}
`;
document.head.appendChild(style);
}
// Initialize the application
function initApp() {
const groupManager = new GroupManager();
new ControlPanel(groupManager);
enhanceVideoText();
}
// Wait for page load
if (document.readyState === 'loading') {
window.addEventListener('load', initApp);
} else {
initApp();
}
})();