Prepare browser extension release pipeline

This commit is contained in:
2026-06-28 00:37:36 +08:00
parent 44ca183f50
commit 83047c7a3c
16 changed files with 4318 additions and 33 deletions
+64 -4
View File
@@ -8,6 +8,7 @@
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
receiverToken: ''
};
var STATUS_KEY = 'verstak.status';
function getSettings() {
return ext.storage.local.get('settings').then(function (result) {
@@ -15,6 +16,23 @@
});
}
function saveSettings(settings) {
return ext.storage.local.set({ settings: Object.assign({}, DEFAULT_SETTINGS, settings || {}) });
}
function setStatus(patch) {
return ext.storage.local.get(STATUS_KEY).then(function (result) {
var status = Object.assign({}, result && result[STATUS_KEY] || {}, patch || {}, {
updatedAt: new Date().toISOString()
});
var update = {};
update[STATUS_KEY] = status;
return ext.storage.local.set(update).then(function () {
return status;
});
});
}
function activeTab() {
return ext.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
return tabs && tabs[0] || {};
@@ -38,6 +56,15 @@
return queue.enqueue(payload).then(function () {
return { status: 'queued', captureId: payload.captureId };
});
}).then(function (result) {
return setStatus({
receiverReachable: result && result.status !== 'queued',
lastResult: result && result.status || 'accepted',
lastCaptureId: payload.captureId,
lastError: ''
}).then(function () {
return result;
});
});
});
}
@@ -46,10 +73,32 @@
return getSettings().then(function (settings) {
return queue.retry(function (payload) {
return protocol.sendCapture(settings.receiverUrl, settings.receiverToken, payload);
}).then(function (result) {
return setStatus({
receiverReachable: result.pending === 0,
lastResult: 'retry',
lastError: result.pending === 0 ? '' : 'Some captures are still pending'
}).then(function () {
return result;
});
});
});
}
function getState() {
return Promise.all([
getSettings(),
queue.list(),
ext.storage.local.get(STATUS_KEY)
]).then(function (results) {
return {
settings: results[0],
pendingCount: results[1].length,
status: results[2] && results[2][STATUS_KEY] || {}
};
});
}
function setupContextMenus() {
if (!ext.contextMenus) return;
ext.contextMenus.removeAll(function () {
@@ -69,13 +118,24 @@
});
}
ext.runtime.onMessage.addListener(function (message) {
if (!message || message.type !== 'verstak.capture') return undefined;
if (message.action === 'retryPending') {
return retryPending();
function handleMessage(message) {
if (!message || message.type !== 'verstak.capture') return Promise.resolve(undefined);
if (message.action === 'getState') return getState();
if (message.action === 'saveSettings') {
return saveSettings(message.settings).then(function () {
return setStatus({ receiverReachable: null, lastResult: 'settings-saved', lastError: '' });
}).then(getState);
}
if (message.action === 'retryPending') return retryPending().then(getState);
return activeTab().then(function (tab) {
return sendOrQueue(captureFromInfo(message.kind || 'page', message, tab));
}).then(getState);
}
ext.runtime.onMessage.addListener(function (message, sender, sendResponse) {
handleMessage(message).then(sendResponse).catch(function (err) {
sendResponse({ error: err && err.message ? err.message : String(err) });
});
return true;
});
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

+111 -6
View File
@@ -1,22 +1,105 @@
body {
min-width: 220px;
min-width: 320px;
margin: 0;
font: 13px system-ui, sans-serif;
background: #111827;
color: #e5e7eb;
background: #10131f;
color: #e8ecf3;
}
main {
.root {
display: grid;
gap: 10px;
padding: 14px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
h1 {
margin: 0;
font-size: 15px;
line-height: 1.2;
}
#subtitle {
margin: 2px 0 0;
color: #94a3b8;
font-size: 12px;
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: #64748b;
box-shadow: 0 0 0 3px rgba(100, 116, 139, .14);
}
.dot.online {
background: #22c55e;
box-shadow: 0 0 0 3px rgba(34, 197, 94, .16);
}
.dot.offline {
background: #ef4444;
box-shadow: 0 0 0 3px rgba(239, 68, 68, .16);
}
.panel,
.settings {
border: 1px solid #273244;
border-radius: 6px;
background: #161b2a;
padding: 10px;
}
.row {
display: grid;
grid-template-columns: 82px minmax(0, 1fr);
gap: 8px;
align-items: center;
min-height: 24px;
}
.row span,
.settings label,
.hint {
color: #94a3b8;
}
.row strong {
font-weight: 600;
}
.online {
color: #86efac;
}
.offline {
color: #fca5a5;
}
.url-row code {
color: #cbd5e1;
overflow-wrap: anywhere;
font-size: 11px;
}
.actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
padding: 12px;
}
button {
border: 1px solid #374151;
border-radius: 4px;
padding: 8px 10px;
background: #1f2937;
background: #233047;
color: #f9fafb;
cursor: pointer;
}
@@ -25,6 +108,28 @@ button:hover {
border-color: #10b981;
}
.secondary {
margin-top: 8px;
width: 100%;
}
input {
box-sizing: border-box;
width: 100%;
margin-top: 5px;
border: 1px solid #374151;
border-radius: 4px;
padding: 7px 8px;
background: #0f172a;
color: #e5e7eb;
}
.hint {
margin: 0;
font-size: 12px;
line-height: 1.35;
}
#status {
min-height: 18px;
margin: 0;
+38 -5
View File
@@ -1,14 +1,47 @@
<!doctype html>
<html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="popup.css">
</head>
<body>
<main>
<button id="capture-page">Send Page</button>
<button id="capture-selection">Send Selection</button>
<button id="retry">Retry Pending</button>
<main class="root">
<header class="header">
<div>
<h1>Verstak Bridge</h1>
<p id="subtitle">Browser Inbox</p>
</div>
<span id="status-dot" class="dot unknown"></span>
</header>
<section class="panel">
<div class="row">
<span>Receiver</span>
<strong id="receiver-state">Unknown</strong>
</div>
<div class="row">
<span>Pending</span>
<strong id="pending-count">0</strong>
</div>
<div class="row url-row">
<span>URL</span>
<code id="receiver-url"></code>
</div>
</section>
<section class="actions">
<button id="capture-page">Send Page</button>
<button id="retry">Retry Pending</button>
</section>
<section class="settings">
<label for="receiver-input">Receiver URL</label>
<input id="receiver-input" type="url" spellcheck="false">
<button id="save-settings" class="secondary">Save</button>
</section>
<p class="hint">Selection and link captures are available from the page context menu.</p>
<p id="status"></p>
</main>
<script src="popup.js"></script>
+68 -7
View File
@@ -3,16 +3,61 @@
var ext = typeof browser !== 'undefined' ? browser : chrome;
var statusEl = document.getElementById('status');
var receiverStateEl = document.getElementById('receiver-state');
var receiverUrlEl = document.getElementById('receiver-url');
var receiverInputEl = document.getElementById('receiver-input');
var pendingCountEl = document.getElementById('pending-count');
var statusDotEl = document.getElementById('status-dot');
function setStatus(text) {
statusEl.textContent = text;
}
function request(message) {
return Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) {
if (result && result.error) throw new Error(result.error);
return result || {};
});
}
function render(state) {
state = state || {};
var settings = state.settings || {};
var status = state.status || {};
var reachable = status.receiverReachable;
pendingCountEl.textContent = String(state.pendingCount || 0);
receiverUrlEl.textContent = settings.receiverUrl || '';
if (document.activeElement !== receiverInputEl) {
receiverInputEl.value = settings.receiverUrl || '';
}
if (reachable === true) {
receiverStateEl.textContent = 'Online';
receiverStateEl.className = 'online';
statusDotEl.className = 'dot online';
} else if (reachable === false) {
receiverStateEl.textContent = 'Offline';
receiverStateEl.className = 'offline';
statusDotEl.className = 'dot offline';
} else {
receiverStateEl.textContent = 'Unknown';
receiverStateEl.className = '';
statusDotEl.className = 'dot unknown';
}
}
function refresh() {
return request({ type: 'verstak.capture', action: 'getState' }).then(render).catch(function (err) {
setStatus(err && err.message ? err.message : String(err));
});
}
function send(message) {
setStatus('Sending...');
Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) {
if (result && result.status === 'queued') setStatus('Queued until Verstak is available');
else setStatus('Sent');
request(message).then(function (state) {
render(state);
if (state.status && state.status.lastResult === 'queued') setStatus('Queued until Verstak is available');
else setStatus('Done');
}).catch(function (err) {
setStatus(err && err.message ? err.message : String(err));
});
@@ -22,11 +67,27 @@
send({ type: 'verstak.capture', kind: 'page' });
});
document.getElementById('capture-selection').addEventListener('click', function () {
send({ type: 'verstak.capture', kind: 'selection' });
});
document.getElementById('retry').addEventListener('click', function () {
send({ type: 'verstak.capture', action: 'retryPending' });
});
document.getElementById('save-settings').addEventListener('click', function () {
var receiverUrl = receiverInputEl.value.trim();
if (!/^https?:\/\//.test(receiverUrl)) {
setStatus('Receiver URL must start with http:// or https://');
return;
}
request({
type: 'verstak.capture',
action: 'saveSettings',
settings: { receiverUrl: receiverUrl, receiverToken: '' }
}).then(function (state) {
render(state);
setStatus('Saved');
}).catch(function (err) {
setStatus(err && err.message ? err.message : String(err));
});
});
refresh();
})();