Prepare browser extension release pipeline
This commit is contained in:
+64
-4
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user