Initial commit: OP Extension Chrome Extension
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
*.log
|
||||
478
content.js
Normal file
478
content.js
Normal file
@@ -0,0 +1,478 @@
|
||||
let ws = null;
|
||||
let roomId = null;
|
||||
let playerName = null;
|
||||
let isEnabled = false;
|
||||
let autoMark = false;
|
||||
let isDragging = false;
|
||||
let dragOffset = { x: 0, y: 0 };
|
||||
|
||||
// camera state tracking
|
||||
let cameraState = {
|
||||
scale: 1.8,
|
||||
offsetX: -350,
|
||||
offsetY: -200,
|
||||
gameWidth: 0,
|
||||
gameHeight: 0,
|
||||
canvasRect: null
|
||||
};
|
||||
|
||||
// spawn positions (tile coordinates)
|
||||
let spawnPositions = new Map(); // playerName -> {x, y}
|
||||
|
||||
// game state
|
||||
let gameStarted = false;
|
||||
let spawnPhaseCheckInterval = null;
|
||||
|
||||
// overlay canvas for highlights
|
||||
let overlayCanvas = null;
|
||||
let overlayCtx = null;
|
||||
|
||||
function createOverlay() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'spawn-tracker-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div class="tracker-header" id="tracker-header">
|
||||
<span class="tracker-title">spawn tracker</span>
|
||||
<button id="tracker-toggle" class="tracker-btn">hide</button>
|
||||
</div>
|
||||
<div id="tracker-content">
|
||||
<div class="tracker-status">disconnected</div>
|
||||
<div class="tracker-controls">
|
||||
<label class="tracker-checkbox">
|
||||
<input type="checkbox" id="auto-mark-toggle">
|
||||
<span>auto-mark</span>
|
||||
</label>
|
||||
</div>
|
||||
<div id="tracker-players"></div>
|
||||
<button id="tracker-clear" class="tracker-btn tracker-btn-secondary">clear my spawn</button>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
chrome.storage.local.get(['overlayPosition'], (data) => {
|
||||
if (data.overlayPosition) {
|
||||
overlay.style.top = data.overlayPosition.top;
|
||||
overlay.style.left = data.overlayPosition.left;
|
||||
overlay.style.right = 'auto';
|
||||
}
|
||||
});
|
||||
|
||||
const header = document.getElementById('tracker-header');
|
||||
header.addEventListener('mousedown', startDrag);
|
||||
document.addEventListener('mousemove', drag);
|
||||
document.addEventListener('mouseup', stopDrag);
|
||||
|
||||
document.getElementById('tracker-toggle').addEventListener('click', toggleOverlay);
|
||||
document.getElementById('auto-mark-toggle').addEventListener('change', toggleAutoMark);
|
||||
document.getElementById('tracker-clear').addEventListener('click', clearSpawn);
|
||||
|
||||
chrome.storage.local.get(['autoMark'], (data) => {
|
||||
autoMark = data.autoMark || false;
|
||||
document.getElementById('auto-mark-toggle').checked = autoMark;
|
||||
if (autoMark) {
|
||||
setupAutoMark();
|
||||
}
|
||||
});
|
||||
|
||||
initializeHighlighting();
|
||||
startSpawnPhaseCheck();
|
||||
}
|
||||
|
||||
function initializeHighlighting() {
|
||||
const gameCanvas = document.querySelector('canvas');
|
||||
if (!gameCanvas) {
|
||||
setTimeout(initializeHighlighting, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
overlayCanvas = document.createElement('canvas');
|
||||
overlayCanvas.id = 'spawn-highlight-overlay';
|
||||
overlayCanvas.style.position = 'absolute';
|
||||
overlayCanvas.style.top = '0';
|
||||
overlayCanvas.style.left = '0';
|
||||
overlayCanvas.style.pointerEvents = 'none';
|
||||
overlayCanvas.style.zIndex = '999998';
|
||||
overlayCanvas.width = window.innerWidth;
|
||||
overlayCanvas.height = window.innerHeight;
|
||||
document.body.appendChild(overlayCanvas);
|
||||
|
||||
overlayCtx = overlayCanvas.getContext('2d');
|
||||
|
||||
cameraState.gameWidth = gameCanvas.width;
|
||||
cameraState.gameHeight = gameCanvas.height;
|
||||
cameraState.canvasRect = gameCanvas.getBoundingClientRect();
|
||||
|
||||
setupCameraTracking(gameCanvas);
|
||||
startHighlightLoop();
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
overlayCanvas.width = window.innerWidth;
|
||||
overlayCanvas.height = window.innerHeight;
|
||||
cameraState.canvasRect = gameCanvas.getBoundingClientRect();
|
||||
});
|
||||
}
|
||||
|
||||
function setupCameraTracking(canvas) {
|
||||
let isPanning = false;
|
||||
let lastMouseX = 0;
|
||||
let lastMouseY = 0;
|
||||
let hasMoved = false;
|
||||
|
||||
canvas.addEventListener('wheel', (e) => {
|
||||
const oldScale = cameraState.scale;
|
||||
const zoomFactor = 1 + e.deltaY / 600;
|
||||
cameraState.scale /= zoomFactor;
|
||||
cameraState.scale = Math.max(0.2, Math.min(20, cameraState.scale));
|
||||
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const canvasX = e.clientX - rect.left;
|
||||
const canvasY = e.clientY - rect.top;
|
||||
|
||||
const zoomPointX = (canvasX - cameraState.gameWidth / 2) / oldScale + cameraState.offsetX;
|
||||
const zoomPointY = (canvasY - cameraState.gameHeight / 2) / oldScale + cameraState.offsetY;
|
||||
|
||||
cameraState.offsetX = zoomPointX - (canvasX - cameraState.gameWidth / 2) / cameraState.scale;
|
||||
cameraState.offsetY = zoomPointY - (canvasY - cameraState.gameHeight / 2) / cameraState.scale;
|
||||
}, { passive: true });
|
||||
|
||||
canvas.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 2 || e.button === 1) {
|
||||
isPanning = true;
|
||||
hasMoved = false;
|
||||
lastMouseX = e.clientX;
|
||||
lastMouseY = e.clientY;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mousemove', (e) => {
|
||||
if (isPanning) {
|
||||
const deltaX = e.clientX - lastMouseX;
|
||||
const deltaY = e.clientY - lastMouseY;
|
||||
|
||||
if (Math.abs(deltaX) > 2 || Math.abs(deltaY) > 2) {
|
||||
hasMoved = true;
|
||||
cameraState.offsetX -= deltaX / cameraState.scale;
|
||||
cameraState.offsetY -= deltaY / cameraState.scale;
|
||||
}
|
||||
|
||||
lastMouseX = e.clientX;
|
||||
lastMouseY = e.clientY;
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener('mouseup', () => {
|
||||
isPanning = false;
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isPanning = false;
|
||||
});
|
||||
}
|
||||
|
||||
function worldToScreenCoordinates(cellX, cellY) {
|
||||
const gameX = cellX;
|
||||
const gameY = cellY;
|
||||
|
||||
const centerX = gameX - cameraState.gameWidth / 2;
|
||||
const centerY = gameY - cameraState.gameHeight / 2;
|
||||
|
||||
const canvasX = (centerX - cameraState.offsetX) * cameraState.scale + cameraState.gameWidth / 2;
|
||||
const canvasY = (centerY - cameraState.offsetY) * cameraState.scale + cameraState.gameHeight / 2;
|
||||
|
||||
const canvasRect = cameraState.canvasRect;
|
||||
const screenX = canvasX + canvasRect.left;
|
||||
const screenY = canvasY + canvasRect.top;
|
||||
|
||||
return { x: screenX, y: screenY };
|
||||
}
|
||||
|
||||
function startHighlightLoop() {
|
||||
function render() {
|
||||
if (!overlayCtx) return;
|
||||
|
||||
overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
|
||||
|
||||
spawnPositions.forEach((pos, name) => {
|
||||
const screen = worldToScreenCoordinates(pos.x, pos.y);
|
||||
|
||||
const isMySpawn = name === playerName;
|
||||
const radius = isMySpawn ? 20 : 15;
|
||||
const color = isMySpawn ? '#0f0' : '#ff0';
|
||||
const pulseOffset = isMySpawn ? Math.sin(Date.now() / 200) * 3 : 0;
|
||||
|
||||
overlayCtx.beginPath();
|
||||
overlayCtx.arc(screen.x, screen.y, radius + pulseOffset, 0, Math.PI * 2);
|
||||
overlayCtx.strokeStyle = color;
|
||||
overlayCtx.lineWidth = 3;
|
||||
overlayCtx.stroke();
|
||||
|
||||
overlayCtx.fillStyle = color + '33';
|
||||
overlayCtx.fill();
|
||||
|
||||
overlayCtx.fillStyle = '#fff';
|
||||
overlayCtx.font = '10px monospace';
|
||||
overlayCtx.textAlign = 'center';
|
||||
overlayCtx.fillText(name, screen.x, screen.y - radius - 5);
|
||||
});
|
||||
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function startDrag(e) {
|
||||
isDragging = true;
|
||||
const overlay = document.getElementById('spawn-tracker-overlay');
|
||||
const rect = overlay.getBoundingClientRect();
|
||||
dragOffset.x = e.clientX - rect.left;
|
||||
dragOffset.y = e.clientY - rect.top;
|
||||
overlay.style.cursor = 'grabbing';
|
||||
}
|
||||
|
||||
function drag(e) {
|
||||
if (!isDragging) return;
|
||||
const overlay = document.getElementById('spawn-tracker-overlay');
|
||||
const x = e.clientX - dragOffset.x;
|
||||
const y = e.clientY - dragOffset.y;
|
||||
overlay.style.left = x + 'px';
|
||||
overlay.style.top = y + 'px';
|
||||
overlay.style.right = 'auto';
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
const overlay = document.getElementById('spawn-tracker-overlay');
|
||||
overlay.style.cursor = 'grab';
|
||||
|
||||
chrome.storage.local.set({
|
||||
overlayPosition: {
|
||||
top: overlay.style.top,
|
||||
left: overlay.style.left
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function toggleOverlay() {
|
||||
const content = document.getElementById('tracker-content');
|
||||
const btn = document.getElementById('tracker-toggle');
|
||||
|
||||
if (content.style.display === 'none') {
|
||||
content.style.display = 'block';
|
||||
btn.textContent = 'hide';
|
||||
} else {
|
||||
content.style.display = 'none';
|
||||
btn.textContent = 'show';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoMark(e) {
|
||||
autoMark = e.target.checked;
|
||||
chrome.storage.local.set({ autoMark: autoMark });
|
||||
|
||||
if (autoMark) {
|
||||
updateStatus('auto-mark enabled', '#0f0');
|
||||
setupAutoMark();
|
||||
} else {
|
||||
updateStatus('auto-mark disabled', '#666');
|
||||
removeAutoMark();
|
||||
}
|
||||
}
|
||||
|
||||
function setupAutoMark() {
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (!canvas) {
|
||||
setTimeout(setupAutoMark, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', handleAutoMark);
|
||||
}
|
||||
|
||||
function removeAutoMark() {
|
||||
const canvas = document.querySelector('canvas');
|
||||
if (canvas) {
|
||||
canvas.removeEventListener('click', handleAutoMark);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAutoMark(e) {
|
||||
if (!autoMark || !ws || ws.readyState !== WebSocket.OPEN || gameStarted) return;
|
||||
|
||||
const canvas = e.target;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const canvasX = e.clientX - rect.left;
|
||||
const canvasY = e.clientY - rect.top;
|
||||
|
||||
const centerX = (canvasX - cameraState.gameWidth / 2) / cameraState.scale + cameraState.offsetX;
|
||||
const centerY = (canvasY - cameraState.gameHeight / 2) / cameraState.scale + cameraState.offsetY;
|
||||
|
||||
const gameX = centerX + cameraState.gameWidth / 2;
|
||||
const gameY = centerY + cameraState.gameHeight / 2;
|
||||
|
||||
const cellX = Math.floor(gameX);
|
||||
const cellY = Math.floor(gameY);
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: 'location',
|
||||
x: cellX,
|
||||
y: cellY
|
||||
}));
|
||||
}
|
||||
|
||||
function updateStatus(status, color = '#666') {
|
||||
const statusEl = document.querySelector('.tracker-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = status;
|
||||
statusEl.style.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlayers(players) {
|
||||
const playersEl = document.getElementById('tracker-players');
|
||||
if (!playersEl) return;
|
||||
|
||||
playersEl.innerHTML = '';
|
||||
|
||||
spawnPositions.clear();
|
||||
|
||||
players.forEach(player => {
|
||||
const playerDiv = document.createElement('div');
|
||||
playerDiv.className = 'tracker-player';
|
||||
|
||||
let locationText = 'waiting';
|
||||
if (player.location) {
|
||||
spawnPositions.set(player.name, { x: player.location.x, y: player.location.y });
|
||||
locationText = `tile (${Math.round(player.location.x)}, ${Math.round(player.location.y)})`;
|
||||
}
|
||||
|
||||
playerDiv.innerHTML = `
|
||||
<span class="player-name">${player.name}</span>
|
||||
<span class="player-location">${locationText}</span>
|
||||
`;
|
||||
|
||||
playersEl.appendChild(playerDiv);
|
||||
});
|
||||
}
|
||||
|
||||
function connectWebSocket(serverUrl, room, name) {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.close();
|
||||
}
|
||||
|
||||
roomId = room;
|
||||
playerName = name;
|
||||
|
||||
updateStatus('connecting', '#888');
|
||||
|
||||
ws = new WebSocket(serverUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
updateStatus('connected', '#0f0');
|
||||
ws.send(JSON.stringify({
|
||||
type: 'join',
|
||||
roomId: roomId,
|
||||
playerName: playerName
|
||||
}));
|
||||
|
||||
chrome.storage.local.get(['autoMark'], (data) => {
|
||||
if (data.autoMark) {
|
||||
autoMark = true;
|
||||
setupAutoMark();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'update') {
|
||||
updatePlayers(data.players);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
updateStatus('disconnected', '#f00');
|
||||
removeAutoMark();
|
||||
setTimeout(() => {
|
||||
if (isEnabled) {
|
||||
connectWebSocket(serverUrl, roomId, playerName);
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('ws error:', error);
|
||||
updateStatus('error', '#f00');
|
||||
};
|
||||
}
|
||||
|
||||
function clearSpawn() {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'clear'
|
||||
}));
|
||||
updateStatus('cleared', '#666');
|
||||
}
|
||||
}
|
||||
|
||||
function startSpawnPhaseCheck() {
|
||||
spawnPhaseCheckInterval = setInterval(() => {
|
||||
const spawnTimer = document.querySelector('spawn-timer');
|
||||
if (spawnTimer && spawnTimer.shadowRoot) {
|
||||
const timerText = spawnTimer.shadowRoot.textContent;
|
||||
if (!timerText || timerText.includes('Game') || timerText.includes('Tick')) {
|
||||
if (!gameStarted) {
|
||||
gameStarted = true;
|
||||
updateStatus('game started', '#888');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function getUsername() {
|
||||
return localStorage.getItem('username') || '';
|
||||
}
|
||||
|
||||
chrome.storage.sync.get(['serverUrl', 'roomId', 'playerName', 'enabled', 'autoMark'], (data) => {
|
||||
const serverUrl = data.serverUrl || 'wss://op.lopensed.dev';
|
||||
const roomId = data.roomId || 'default';
|
||||
const name = data.playerName || getUsername();
|
||||
const enabled = data.enabled !== false;
|
||||
|
||||
chrome.storage.sync.set({
|
||||
serverUrl: serverUrl,
|
||||
roomId: roomId,
|
||||
enabled: enabled
|
||||
});
|
||||
|
||||
if (enabled && serverUrl && roomId && name) {
|
||||
isEnabled = true;
|
||||
createOverlay();
|
||||
connectWebSocket(serverUrl, roomId, name);
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.onChanged.addListener((changes, namespace) => {
|
||||
if (namespace === 'sync') {
|
||||
chrome.storage.sync.get(['serverUrl', 'roomId', 'playerName', 'enabled'], (data) => {
|
||||
const name = data.playerName || getUsername();
|
||||
|
||||
if (data.enabled && data.serverUrl && data.roomId && name) {
|
||||
isEnabled = true;
|
||||
if (!document.getElementById('spawn-tracker-overlay')) {
|
||||
createOverlay();
|
||||
}
|
||||
connectWebSocket(data.serverUrl, data.roomId, name);
|
||||
} else {
|
||||
isEnabled = false;
|
||||
if (ws) ws.close();
|
||||
removeAutoMark();
|
||||
if (spawnPhaseCheckInterval) clearInterval(spawnPhaseCheckInterval);
|
||||
const overlay = document.getElementById('spawn-tracker-overlay');
|
||||
if (overlay) overlay.remove();
|
||||
if (overlayCanvas) overlayCanvas.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
BIN
icon128.png
Normal file
BIN
icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 339 B |
BIN
icon16.png
Normal file
BIN
icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 299 B |
BIN
icon48.png
Normal file
BIN
icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 307 B |
39
manifest.json
Normal file
39
manifest.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "openfront spawn tracker",
|
||||
"version": "1.0.0",
|
||||
"description": "track spawn locations",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"activeTab",
|
||||
"scripting"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://openfront.io/*",
|
||||
"https://www.openfront.io/*"
|
||||
],
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://openfront.io/*",
|
||||
"https://www.openfront.io/*"
|
||||
],
|
||||
"js": ["content.js"],
|
||||
"css": ["overlay.css"],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": {
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png",
|
||||
"128": "icon128.png"
|
||||
}
|
||||
},
|
||||
"icons": {
|
||||
"16": "icon16.png",
|
||||
"48": "icon48.png",
|
||||
"128": "icon128.png"
|
||||
}
|
||||
}
|
||||
145
overlay.css
Normal file
145
overlay.css
Normal file
@@ -0,0 +1,145 @@
|
||||
#spawn-tracker-overlay {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
min-width: 240px;
|
||||
max-width: 300px;
|
||||
z-index: 999999;
|
||||
font-family: monospace;
|
||||
color: #ddd;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.tracker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #333;
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tracker-title {
|
||||
font-size: 13px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.tracker-status {
|
||||
text-align: center;
|
||||
padding: 4px;
|
||||
margin-bottom: 8px;
|
||||
background: #111;
|
||||
border-radius: 2px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
#tracker-players {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.tracker-player {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 6px;
|
||||
margin-bottom: 4px;
|
||||
background: #111;
|
||||
border-radius: 2px;
|
||||
border-left: 2px solid #444;
|
||||
}
|
||||
|
||||
.player-name {
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.player-location {
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tracker-btn {
|
||||
background: #222;
|
||||
color: #888;
|
||||
border: 1px solid #333;
|
||||
border-radius: 2px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tracker-btn:hover {
|
||||
background: #2a2a2a;
|
||||
border-color: #444;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.tracker-btn-primary {
|
||||
width: 100%;
|
||||
margin-bottom: 4px;
|
||||
background: #2a2a2a;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.tracker-btn-primary:hover {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.tracker-btn-secondary {
|
||||
width: 100%;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
.tracker-btn-secondary:hover {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
#tracker-players::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
#tracker-players::-webkit-scrollbar-track {
|
||||
background: #111;
|
||||
}
|
||||
|
||||
#tracker-players::-webkit-scrollbar-thumb {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
#tracker-players::-webkit-scrollbar-thumb:hover {
|
||||
background: #444;
|
||||
}
|
||||
|
||||
.tracker-controls {
|
||||
margin-bottom: 8px;
|
||||
padding: 6px;
|
||||
background: #111;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.tracker-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.tracker-checkbox input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tracker-checkbox:hover {
|
||||
color: #aaa;
|
||||
}
|
||||
178
popup.html
Normal file
178
popup.html
Normal file
@@ -0,0 +1,178 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {
|
||||
width: 280px;
|
||||
padding: 12px;
|
||||
font-family: monospace;
|
||||
background: #1a1a1a;
|
||||
color: #ddd;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0 0 12px 0;
|
||||
color: #888;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
font-size: 11px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 6px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 2px;
|
||||
background: #111;
|
||||
color: #ddd;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.btn-group {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
button {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #333;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
font-family: monospace;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: #2a2a2a;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.btn-save:hover {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.btn-disable {
|
||||
background: #1a1a1a;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.btn-disable:hover {
|
||||
background: #222;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 10px;
|
||||
padding: 6px;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
font-size: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #1a2a1a;
|
||||
color: #0f0;
|
||||
border: 1px solid #0f0;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #2a1a1a;
|
||||
color: #f00;
|
||||
border: 1px solid #f00;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
background: #111;
|
||||
border: 1px solid #222;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.debug {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
background: #111;
|
||||
border: 1px solid #222;
|
||||
border-radius: 2px;
|
||||
font-size: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.debug.checking {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.debug.ok {
|
||||
color: #0f0;
|
||||
border-color: #0f0;
|
||||
}
|
||||
|
||||
.debug.error {
|
||||
color: #f00;
|
||||
border-color: #f00;
|
||||
}
|
||||
|
||||
.auto-detected {
|
||||
color: #0f0;
|
||||
font-size: 9px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>spawn tracker</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="serverUrl">server</label>
|
||||
<input type="text" id="serverUrl" placeholder="wss://op.lopensed.dev">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="roomId">room (share with friends)</label>
|
||||
<input type="text" id="roomId" placeholder="default">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playerName">name</label>
|
||||
<input type="text" id="playerName" placeholder="auto-detected">
|
||||
<div class="auto-detected" id="autoDetected"></div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group">
|
||||
<button class="btn-save" id="saveBtn">save</button>
|
||||
<button class="btn-disable" id="disableBtn">disable</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="status"></div>
|
||||
|
||||
<div class="debug" id="debug">checking server...</div>
|
||||
|
||||
<script src="popup.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
96
popup.js
Normal file
96
popup.js
Normal file
@@ -0,0 +1,96 @@
|
||||
// check for auto-detected username
|
||||
chrome.tabs.query({active: true, currentWindow: true}, (tabs) => {
|
||||
if (tabs[0] && tabs[0].url && tabs[0].url.includes('openfront.io')) {
|
||||
chrome.scripting.executeScript({
|
||||
target: {tabId: tabs[0].id},
|
||||
func: () => localStorage.getItem('username')
|
||||
}, (results) => {
|
||||
if (results && results[0] && results[0].result) {
|
||||
document.getElementById('autoDetected').textContent = `detected: ${results[0].result}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
chrome.storage.sync.get(['serverUrl', 'roomId', 'playerName', 'enabled'], (data) => {
|
||||
// set defaults
|
||||
const serverUrl = data.serverUrl || 'wss://op.lopensed.dev';
|
||||
const roomId = data.roomId || 'default';
|
||||
|
||||
document.getElementById('serverUrl').value = serverUrl;
|
||||
document.getElementById('roomId').value = roomId;
|
||||
if (data.playerName) document.getElementById('playerName').value = data.playerName;
|
||||
|
||||
// auto-save defaults and enable if not already done
|
||||
if (!data.enabled || !data.serverUrl || !data.roomId) {
|
||||
chrome.storage.sync.set({
|
||||
serverUrl: serverUrl,
|
||||
roomId: roomId,
|
||||
enabled: true
|
||||
});
|
||||
}
|
||||
|
||||
// ping server for debug
|
||||
pingServer(serverUrl);
|
||||
});
|
||||
|
||||
function pingServer(url) {
|
||||
const debug = document.getElementById('debug');
|
||||
debug.textContent = 'checking server...';
|
||||
debug.className = 'debug checking';
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
ws.close();
|
||||
debug.textContent = 'server: timeout';
|
||||
debug.className = 'debug error';
|
||||
}, 5000);
|
||||
|
||||
ws.onopen = () => {
|
||||
clearTimeout(timeout);
|
||||
debug.textContent = 'server: ok';
|
||||
debug.className = 'debug ok';
|
||||
setTimeout(() => ws.close(), 100);
|
||||
};
|
||||
|
||||
ws.onerror = () => {
|
||||
clearTimeout(timeout);
|
||||
debug.textContent = 'server: unreachable';
|
||||
debug.className = 'debug error';
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById('saveBtn').addEventListener('click', () => {
|
||||
const serverUrl = document.getElementById('serverUrl').value.trim() || 'wss://op.lopensed.dev';
|
||||
const roomId = document.getElementById('roomId').value.trim() || 'default';
|
||||
const playerName = document.getElementById('playerName').value.trim();
|
||||
|
||||
chrome.storage.sync.set({
|
||||
serverUrl: serverUrl,
|
||||
roomId: roomId,
|
||||
playerName: playerName,
|
||||
enabled: true
|
||||
}, () => {
|
||||
showStatus('saved - refresh page', 'success');
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById('disableBtn').addEventListener('click', () => {
|
||||
chrome.storage.sync.set({
|
||||
enabled: false
|
||||
}, () => {
|
||||
showStatus('disabled - refresh page', 'success');
|
||||
});
|
||||
});
|
||||
|
||||
function showStatus(message, type) {
|
||||
const status = document.getElementById('status');
|
||||
status.textContent = message;
|
||||
status.className = `status ${type}`;
|
||||
status.style.display = 'block';
|
||||
|
||||
setTimeout(() => {
|
||||
status.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
Reference in New Issue
Block a user