Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions port_ext/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ const BADGE_RED_TRANSLUCENT = '#ea4335cc';
const BADGE_GREEN = '#34a853'; // new port detected
const BADGE_GREEN_TRANSLUCENT = '#34a853cc';

let knownActivePorts = new Set(); // last known active ports, used to detect "new"
let knownActivePorts = new Set();
let serverDownFlashTimer = null;
let newPortFlashTimer = null;
let flashBlinkInterval = null; // drives the translucent pulse while flashing
let currentFlashMode = null; // 'red' | 'green' | null
let flashBlinkInterval = null;
let currentFlashMode = null;
let badgeEnabled = true; // default on; updated from storage

// Load badge preference at startup
chrome.storage.local.get(['badgeEnabled'], (result) => {
badgeEnabled = result.badgeEnabled !== false;
if (!badgeEnabled) chrome.action.setBadgeText({ text: '' });
});

chrome.runtime.onInstalled.addListener(() => {
console.log("Localhost Manager installed.");
Expand All @@ -37,16 +44,15 @@ function initBadge() {
// --- Badge rendering ---

function updateBadgeCount(count) {
if (!badgeEnabled) { chrome.action.setBadgeText({ text: '' }); return; }
chrome.action.setBadgeText({ text: count > 0 ? String(count) : '' });
if (currentFlashMode === null) {
chrome.action.setBadgeBackgroundColor({ color: BADGE_NEUTRAL });
}
}

function startFlash(mode, durationMs) {
// A new flash of the same or higher-priority mode resets the timer.
// Red (server down) takes priority over green (new port) since it's
// a problem state, not a celebratory one.
if (!badgeEnabled) return;
if (currentFlashMode === 'red' && mode === 'green') return;

stopFlash();
Expand Down Expand Up @@ -88,7 +94,8 @@ async function pollOnce() {
// 1. Check active ports among knownPorts (lightweight — no title/favicon fetch here,
// that detail work stays in popup.js; background only needs up/down + count)
const activeNow = new Set();
await Promise.all(knownPorts.map(async (port) => {
const validPorts = knownPorts.filter(p => Number.isInteger(p) && p > 0 && p <= 65535);
await Promise.all(validPorts.map(async (port) => {
try {
const controller = new AbortController();
const t = setTimeout(() => controller.abort(), 800);
Expand Down Expand Up @@ -126,6 +133,18 @@ async function pollOnce() {
// popup.js calls this after its own scan completes so the badge updates
// immediately rather than waiting for the next background poll cycle.
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === "setBadgeEnabled") {
badgeEnabled = request.enabled;
if (!badgeEnabled) {
stopFlash();
chrome.action.setBadgeText({ text: '' });
} else {
// Immediately restore by re-running a poll
pollOnce();
}
sendResponse({ ok: true });
return true;
}
if (request.action === "getPorts") {
sendResponse({ status: "Background worker is active" });
return true;
Expand Down
130 changes: 130 additions & 0 deletions port_ext/scripts/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,7 @@ document.getElementById('addPortBtn').addEventListener('click', () => {
});

document.getElementById('hostBtn').addEventListener('click', () => htmlFileInput.click());
document.getElementById('hostProjectBtn').addEventListener('click', () => document.getElementById('projectFolder').click());

htmlFileInput.addEventListener('change', function (e) {
const file = e.target.files[0];
Expand All @@ -696,4 +697,133 @@ htmlFileInput.addEventListener('change', function (e) {
reader.onload = (event) => hostFile(event.target.result);
reader.readAsText(file);
htmlFileInput.value = '';
});

// Multi-file project hosting
// Reads every file in the selected folder, encodes binary files as base64,
// and sends them all to /start-multi on the control server.
document.getElementById('projectFolder').addEventListener('change', async function (e) {
const files = [...e.target.files];
if (!files.length) return;

const ready = await controlServerReady();
if (!ready) { setError('Server offline — run: node server.js'); return; }

const candidates = [9000, 9001, 9002, 9003, 9004, 9005];
const port = candidates.find(p => !activePortSet.has(p) && !hostedPortSet.has(p));
if (port === undefined) { setError('No free port in range 9000–9005'); return; }

setStatus('scanning', `Reading ${files.length} files…`);

const TEXT_TYPES = /\.(html?|css|js|mjs|json|svg|txt|xml|md|ts|jsx|tsx|map|yaml|yml|toml|ini)$/i;

// Step 1: read all files, keeping the raw webkitRelativePath
const fileData = [];
for (const file of files) {
const rawPath = file.webkitRelativePath || file.name;

const result = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onerror = () => reject(new Error(`Failed to read ${file.name}`));
if (TEXT_TYPES.test(file.name)) {
reader.onload = (ev) => resolve({ rawPath, content: ev.target.result, encoding: 'utf8' });
reader.readAsText(file, 'UTF-8');
} else {
reader.onload = (ev) => {
const raw = ev.target.result || '';
const commaIdx = raw.indexOf(',');
resolve({ rawPath, content: commaIdx !== -1 ? raw.slice(commaIdx + 1) : '', encoding: 'base64' });
};
reader.readAsDataURL(file);
}
}).catch(err => { console.warn(`Skipping ${file.name}:`, err); return null; });

if (result) fileData.push(result);
}

if (fileData.length === 0) {
setError('No readable files found in folder');
document.getElementById('projectFolder').value = '';
return;
}

// DEBUG: log all raw paths to see exactly what Chrome gives us
console.log('[LM] Raw paths from webkitRelativePath:');
fileData.forEach(f => console.log(' ', JSON.stringify(f.rawPath)));

// Read the entry file name — default to index.html if blank
const entryFileName = (document.getElementById('entryFile').value.trim() || 'index.html').toLowerCase();

// Find the entry file to determine root
const indexFile = fileData.find(f =>
f.rawPath.toLowerCase().endsWith('/' + entryFileName) ||
f.rawPath.toLowerCase() === entryFileName
);

console.log('[LM] entry file:', entryFileName, '| found at:', indexFile ? indexFile.rawPath : 'NOT FOUND');

let rootPrefix = '';
if (indexFile) {
const lastSlash = indexFile.rawPath.lastIndexOf('/');
rootPrefix = lastSlash !== -1 ? indexFile.rawPath.slice(0, lastSlash + 1) : '';
} else {
const firstSlash = fileData[0].rawPath.indexOf('/');
rootPrefix = firstSlash !== -1 ? fileData[0].rawPath.slice(0, firstSlash + 1) : '';
}

console.log('[LM] rootPrefix to strip:', JSON.stringify(rootPrefix));

const strippedFiles = fileData
.map(f => ({
path: f.rawPath.startsWith(rootPrefix)
? f.rawPath.slice(rootPrefix.length)
: f.rawPath,
content: f.content,
encoding: f.encoding
}))
.filter(f => f.path);

console.log('[LM] Stripped paths being sent to server:');
strippedFiles.forEach(f => console.log(' ', JSON.stringify(f.path)));

// Warn clearly if no entry file was found
if (!indexFile) {
setError(`No ${entryFileName} found — pick your project root folder`);
document.getElementById('projectFolder').value = '';
return;
}

if (strippedFiles.length === 0) {
setError('Could not determine project root');
document.getElementById('projectFolder').value = '';
return;
}

setStatus('scanning', `Starting server on port ${port} · ${strippedFiles.length} files…`);

let result;
try {
const res = await fetch(`${CONTROL_SERVER}/start-multi`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ port, files: strippedFiles, entryFile: entryFileName })
});
result = await res.json();
} catch {
setError('Server offline — run: node server.js');
document.getElementById('projectFolder').value = '';
return;
}

if (!result.ok) { setError(`Failed: ${result.error}`); return; }

hostedPortSet.add(port);
if (!knownPorts.includes(port)) knownPorts.push(port);
chrome.storage.local.set({ knownPorts, hostedPorts: [...hostedPortSet] });
const entryUrl = entryFileName === 'index.html'
? `http://localhost:${port}`
: `http://localhost:${port}/${entryFileName}`;
chrome.tabs.create({ url: entryUrl });
await scanPorts();
document.getElementById('projectFolder').value = '';
});
20 changes: 19 additions & 1 deletion port_ext/scripts/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ serverFlashSlider.addEventListener('input', () => {
});
});

// --- BADGE TOGGLE ---
function setBadgeSubEnabled(enabled) {
document.getElementById('badgeSubSettings').classList.toggle('disabled', !enabled);
}

document.getElementById('badgeToggle').addEventListener('change', (e) => {
const enabled = e.target.checked;
chrome.storage.local.set({ badgeEnabled: enabled });
setBadgeSubEnabled(enabled);
// Tell background to immediately clear or restore the badge
chrome.runtime.sendMessage({ action: 'setBadgeEnabled', enabled });
});

// --- AUTO SCAN ---
document.getElementById('autoScanToggle').addEventListener('change', (e) => {
const enabled = e.target.checked;
Expand Down Expand Up @@ -217,7 +230,7 @@ document.getElementById('shortcutBtn').addEventListener('click', () => {
document.getElementById('backBtn').addEventListener('click', () => window.close());

// --- LOAD SAVED SETTINGS ---
chrome.storage.local.get(['theme', 'viewMode', 'networkScan', 'networkRange', 'autoScan', 'autoScanIntervalIndex', 'accentColor', 'serverDownFlashIndex'], (result) => {
chrome.storage.local.get(['theme', 'viewMode', 'networkScan', 'networkRange', 'autoScan', 'autoScanIntervalIndex', 'accentColor', 'serverDownFlashIndex', 'badgeEnabled'], (result) => {
const theme = result.theme || 'auto';
const radio = document.querySelector(`input[name="theme"][value="${theme}"]`);
if (radio) radio.checked = true;
Expand Down Expand Up @@ -262,4 +275,9 @@ chrome.storage.local.get(['theme', 'viewMode', 'networkScan', 'networkRange', 'a
const flashIndex = result.serverDownFlashIndex ?? 8;
serverFlashSlider.value = flashIndex;
updateServerFlashLabel(flashIndex);

// Restore badge toggle; default true (enabled)
const badgeOn = result.badgeEnabled !== false;
document.getElementById('badgeToggle').checked = badgeOn;
setBadgeSubEnabled(badgeOn);
});
Loading
Loading