Prepare browser extension release pipeline
This commit is contained in:
parent
44ca183f50
commit
83047c7a3c
|
|
@ -1,2 +1,5 @@
|
||||||
dist/
|
dist/
|
||||||
node_modules/
|
node_modules/
|
||||||
|
.env
|
||||||
|
web-ext-artifacts/
|
||||||
|
release/
|
||||||
|
|
|
||||||
26
README.md
26
README.md
|
|
@ -20,6 +20,32 @@ Build output:
|
||||||
- `dist/chromium`
|
- `dist/chromium`
|
||||||
- `dist/firefox`
|
- `dist/firefox`
|
||||||
|
|
||||||
|
## Firefox Release
|
||||||
|
|
||||||
|
Firefox signing uses `web-ext` and AMO credentials from an env file. The script
|
||||||
|
requires `WEB_EXT_API_PROXY`; AMO upload and approval polling run through that
|
||||||
|
proxy.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VERSTAK_BROWSER_ENV=/home/mirivlad/git/verstak/.env npm run release:firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
Release output:
|
||||||
|
|
||||||
|
- `release/firefox/verstak-firefox-<version>.xpi`
|
||||||
|
- `release/firefox/updates.json`
|
||||||
|
|
||||||
|
The XPI is signed as an unlisted/self-distributed Firefox extension. Build and
|
||||||
|
release artifacts are local outputs and are not committed.
|
||||||
|
|
||||||
|
## Manual Check
|
||||||
|
|
||||||
|
1. Start Verstak desktop with the `verstak.browser-inbox` plugin installed.
|
||||||
|
2. Open the `Browser Inbox` workspace item so it subscribes to capture events.
|
||||||
|
3. Install/load `dist/firefox` or the signed XPI in Firefox.
|
||||||
|
4. Use the popup `Send Page` action, or use page context menu actions for
|
||||||
|
selection/link captures.
|
||||||
|
|
||||||
## Local Receiver Protocol
|
## Local Receiver Protocol
|
||||||
|
|
||||||
Default endpoint:
|
Default endpoint:
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "Verstak Capture",
|
"name": "Verstak Bridge",
|
||||||
"version": "0.1.0",
|
"version": "2.0.1",
|
||||||
"description": "Send pages, selections, and links to the local Verstak browser inbox.",
|
"description": "Send pages, selections, and links to the local Verstak browser inbox.",
|
||||||
|
"author": "Verstak",
|
||||||
|
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension",
|
||||||
"permissions": ["contextMenus", "storage", "tabs"],
|
"permissions": ["contextMenus", "storage", "tabs"],
|
||||||
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
"host_permissions": ["http://127.0.0.1/*", "http://localhost/*"],
|
||||||
"background": {
|
"background": {
|
||||||
|
|
@ -10,6 +12,16 @@
|
||||||
},
|
},
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup/popup.html",
|
"default_popup": "popup/popup.html",
|
||||||
"default_title": "Verstak Capture"
|
"default_title": "Verstak Bridge",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,36 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Verstak Capture",
|
"name": "Verstak Bridge",
|
||||||
"version": "0.1.0",
|
"version": "2.0.1",
|
||||||
"description": "Send pages, selections, and links to the local Verstak browser inbox.",
|
"description": "Send pages, selections, and links to the local Verstak browser inbox.",
|
||||||
|
"author": "Verstak",
|
||||||
|
"homepage_url": "https://git.mirv.top/verstak/verstak-browser-extension",
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "verstak-bridge@verstak.app",
|
||||||
|
"strict_min_version": "115.0",
|
||||||
|
"update_url": "https://mirv.top/verstak/firefox/updates.json",
|
||||||
|
"data_collection_permissions": {
|
||||||
|
"required": ["none"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"permissions": ["contextMenus", "storage", "tabs", "http://127.0.0.1/*", "http://localhost/*"],
|
"permissions": ["contextMenus", "storage", "tabs", "http://127.0.0.1/*", "http://localhost/*"],
|
||||||
"background": {
|
"background": {
|
||||||
"scripts": ["protocol.js", "api.js", "queue.js", "background.js"]
|
"scripts": ["protocol.js", "api.js", "queue.js", "background.js"]
|
||||||
},
|
},
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_popup": "popup/popup.html",
|
"default_popup": "popup/popup.html",
|
||||||
"default_title": "Verstak Capture"
|
"default_title": "Verstak Bridge",
|
||||||
|
"default_icon": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
10
package.json
10
package.json
|
|
@ -1,11 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "verstak-browser-extension",
|
"name": "verstak-browser-extension",
|
||||||
"version": "0.1.0",
|
"version": "2.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Verstak browser capture extension for Chromium and Firefox",
|
"description": "Verstak browser capture extension for Chromium and Firefox",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build-extension.js",
|
"build": "node scripts/build-extension.js",
|
||||||
"test": "node scripts/test-protocol.js"
|
"test": "node scripts/test-protocol.js",
|
||||||
|
"sign:firefox": "./scripts/sign-firefox-xpi.sh",
|
||||||
|
"release:firefox": "./scripts/release-firefox-xpi.sh"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {
|
||||||
|
"web-ext": "^8.3.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,13 @@ function copyPopup(destRoot) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyIcons(destRoot) {
|
||||||
|
const iconsDir = path.join(shared, 'icons');
|
||||||
|
for (const name of ['icon16.png', 'icon48.png', 'icon128.png']) {
|
||||||
|
copy(path.join(iconsDir, name), path.join(destRoot, 'icons', name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
rm(dist);
|
rm(dist);
|
||||||
|
|
||||||
const chromiumDist = path.join(dist, 'chromium');
|
const chromiumDist = path.join(dist, 'chromium');
|
||||||
|
|
@ -43,6 +50,7 @@ concat([
|
||||||
path.join(shared, 'background.js'),
|
path.join(shared, 'background.js'),
|
||||||
], path.join(chromiumDist, 'background.js'));
|
], path.join(chromiumDist, 'background.js'));
|
||||||
copyPopup(chromiumDist);
|
copyPopup(chromiumDist);
|
||||||
|
copyIcons(chromiumDist);
|
||||||
|
|
||||||
const firefoxDist = path.join(dist, 'firefox');
|
const firefoxDist = path.join(dist, 'firefox');
|
||||||
mkdir(firefoxDist);
|
mkdir(firefoxDist);
|
||||||
|
|
@ -51,5 +59,6 @@ for (const name of ['protocol.js', 'api.js', 'queue.js', 'background.js']) {
|
||||||
copy(path.join(shared, name), path.join(firefoxDist, name));
|
copy(path.join(shared, name), path.join(firefoxDist, name));
|
||||||
}
|
}
|
||||||
copyPopup(firefoxDist);
|
copyPopup(firefoxDist);
|
||||||
|
copyIcons(firefoxDist);
|
||||||
|
|
||||||
console.log('built dist/chromium and dist/firefox');
|
console.log('built dist/chromium and dist/firefox');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
load_env_file() {
|
||||||
|
local env_file="$1"
|
||||||
|
[[ -f "$env_file" ]] || return 0
|
||||||
|
local line key value
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
|
||||||
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]]; then
|
||||||
|
key="${BASH_REMATCH[1]}"
|
||||||
|
value="${BASH_REMATCH[2]}"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
|
||||||
|
value="${value:1:${#value}-2}"
|
||||||
|
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
|
||||||
|
value="${value:1:${#value}-2}"
|
||||||
|
fi
|
||||||
|
export "$key=$value"
|
||||||
|
fi
|
||||||
|
done < "$env_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV_FILE="${VERSTAK_BROWSER_ENV:-$ROOT_DIR/.env}"
|
||||||
|
load_env_file "$ENV_FILE"
|
||||||
|
|
||||||
|
SOURCE_DIR="${VERSTAK_FIREFOX_SOURCE_DIR:-dist/firefox}"
|
||||||
|
ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}"
|
||||||
|
RELEASE_DIR="${VERSTAK_FIREFOX_RELEASE_DIR:-release/firefox}"
|
||||||
|
UPDATE_BASE_URL="${VERSTAK_FIREFOX_UPDATE_BASE_URL:-https://mirv.top/verstak/firefox}"
|
||||||
|
|
||||||
|
./scripts/sign-firefox-xpi.sh
|
||||||
|
|
||||||
|
VERSION="$(node -e "console.log(require('./${SOURCE_DIR}/manifest.json').version)")"
|
||||||
|
ADDON_ID="$(node -e "console.log(require('./${SOURCE_DIR}/manifest.json').browser_specific_settings.gecko.id)")"
|
||||||
|
|
||||||
|
if [[ -z "$VERSION" || -z "$ADDON_ID" ]]; then
|
||||||
|
echo "ERROR: could not read Firefox manifest version/addon id" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SIGNED_XPI="$(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' | sort | tail -n 1 || true)"
|
||||||
|
if [[ -z "$SIGNED_XPI" ]]; then
|
||||||
|
echo "ERROR: no signed XPI found in $ARTIFACTS_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$RELEASE_DIR"
|
||||||
|
RELEASE_XPI="verstak-firefox-${VERSION}.xpi"
|
||||||
|
cp "$SIGNED_XPI" "$RELEASE_DIR/$RELEASE_XPI"
|
||||||
|
|
||||||
|
cat > "$RELEASE_DIR/updates.json" <<EOF
|
||||||
|
{
|
||||||
|
"addons": {
|
||||||
|
"${ADDON_ID}": {
|
||||||
|
"updates": [
|
||||||
|
{
|
||||||
|
"version": "${VERSION}",
|
||||||
|
"update_link": "${UPDATE_BASE_URL}/${RELEASE_XPI}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "Firefox release artifacts:"
|
||||||
|
echo "$RELEASE_DIR/$RELEASE_XPI"
|
||||||
|
echo "$RELEASE_DIR/updates.json"
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$ROOT_DIR"
|
||||||
|
|
||||||
|
load_env_file() {
|
||||||
|
local env_file="$1"
|
||||||
|
[[ -f "$env_file" ]] || return 0
|
||||||
|
local line key value
|
||||||
|
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||||
|
[[ "$line" =~ ^[[:space:]]*$ ]] && continue
|
||||||
|
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
||||||
|
if [[ "$line" =~ ^[[:space:]]*([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=(.*)$ ]]; then
|
||||||
|
key="${BASH_REMATCH[1]}"
|
||||||
|
value="${BASH_REMATCH[2]}"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
if [[ "$value" == \"*\" && "$value" == *\" ]]; then
|
||||||
|
value="${value:1:${#value}-2}"
|
||||||
|
elif [[ "$value" == \'*\' && "$value" == *\' ]]; then
|
||||||
|
value="${value:1:${#value}-2}"
|
||||||
|
fi
|
||||||
|
export "$key=$value"
|
||||||
|
fi
|
||||||
|
done < "$env_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
ENV_FILE="${VERSTAK_BROWSER_ENV:-$ROOT_DIR/.env}"
|
||||||
|
load_env_file "$ENV_FILE"
|
||||||
|
|
||||||
|
SOURCE_DIR="${VERSTAK_FIREFOX_SOURCE_DIR:-dist/firefox}"
|
||||||
|
ARTIFACTS_DIR="${WEB_EXT_ARTIFACTS_DIR:-web-ext-artifacts}"
|
||||||
|
CHANNEL="${WEB_EXT_CHANNEL:-unlisted}"
|
||||||
|
|
||||||
|
if [[ -z "${WEB_EXT_API_KEY:-}" ]]; then
|
||||||
|
echo "ERROR: WEB_EXT_API_KEY is not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${WEB_EXT_API_SECRET:-}" ]]; then
|
||||||
|
echo "ERROR: WEB_EXT_API_SECRET is not set" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$CHANNEL" != "unlisted" ]]; then
|
||||||
|
echo "ERROR: only WEB_EXT_CHANNEL=unlisted is allowed for self-distributed builds" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${WEB_EXT_API_PROXY:-}" ]]; then
|
||||||
|
echo "ERROR: WEB_EXT_API_PROXY is required for Firefox signing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
if [[ ! -f "$SOURCE_DIR/manifest.json" ]]; then
|
||||||
|
echo "ERROR: manifest.json not found in $SOURCE_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ARTIFACTS_DIR"
|
||||||
|
|
||||||
|
echo "Linting Firefox extension..."
|
||||||
|
npx web-ext lint \
|
||||||
|
--source-dir "$SOURCE_DIR" \
|
||||||
|
--self-hosted
|
||||||
|
|
||||||
|
echo "Signing Firefox extension as unlisted/self-distributed XPI..."
|
||||||
|
SIGN_ARGS=(
|
||||||
|
sign
|
||||||
|
--source-dir "$SOURCE_DIR"
|
||||||
|
--artifacts-dir "$ARTIFACTS_DIR"
|
||||||
|
--channel "$CHANNEL"
|
||||||
|
--timeout "${WEB_EXT_TIMEOUT:-600000}"
|
||||||
|
--approval-timeout "${WEB_EXT_APPROVAL_TIMEOUT:-600000}"
|
||||||
|
--api-key "$WEB_EXT_API_KEY"
|
||||||
|
--api-secret "$WEB_EXT_API_SECRET"
|
||||||
|
--api-proxy "$WEB_EXT_API_PROXY"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Using AMO API proxy from WEB_EXT_API_PROXY"
|
||||||
|
|
||||||
|
npx web-ext "${SIGN_ARGS[@]}"
|
||||||
|
|
||||||
|
SIGNED_XPI="$(find "$ARTIFACTS_DIR" -maxdepth 1 -type f -name '*.xpi' | sort | tail -n 1 || true)"
|
||||||
|
if [[ -z "$SIGNED_XPI" ]]; then
|
||||||
|
echo "ERROR: signed XPI was not created in $ARTIFACTS_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Signed XPI created:"
|
||||||
|
echo "$SIGNED_XPI"
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
|
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
|
||||||
receiverToken: ''
|
receiverToken: ''
|
||||||
};
|
};
|
||||||
|
var STATUS_KEY = 'verstak.status';
|
||||||
|
|
||||||
function getSettings() {
|
function getSettings() {
|
||||||
return ext.storage.local.get('settings').then(function (result) {
|
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() {
|
function activeTab() {
|
||||||
return ext.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
|
return ext.tabs.query({ active: true, currentWindow: true }).then(function (tabs) {
|
||||||
return tabs && tabs[0] || {};
|
return tabs && tabs[0] || {};
|
||||||
|
|
@ -38,6 +56,15 @@
|
||||||
return queue.enqueue(payload).then(function () {
|
return queue.enqueue(payload).then(function () {
|
||||||
return { status: 'queued', captureId: payload.captureId };
|
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,8 +73,30 @@
|
||||||
return getSettings().then(function (settings) {
|
return getSettings().then(function (settings) {
|
||||||
return queue.retry(function (payload) {
|
return queue.retry(function (payload) {
|
||||||
return protocol.sendCapture(settings.receiverUrl, settings.receiverToken, 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() {
|
function setupContextMenus() {
|
||||||
|
|
@ -69,13 +118,24 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.runtime.onMessage.addListener(function (message) {
|
function handleMessage(message) {
|
||||||
if (!message || message.type !== 'verstak.capture') return undefined;
|
if (!message || message.type !== 'verstak.capture') return Promise.resolve(undefined);
|
||||||
if (message.action === 'retryPending') {
|
if (message.action === 'getState') return getState();
|
||||||
return retryPending();
|
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 activeTab().then(function (tab) {
|
||||||
return sendOrQueue(captureFromInfo(message.kind || 'page', message, 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 |
|
|
@ -1,22 +1,105 @@
|
||||||
body {
|
body {
|
||||||
min-width: 220px;
|
min-width: 320px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font: 13px system-ui, sans-serif;
|
font: 13px system-ui, sans-serif;
|
||||||
background: #111827;
|
background: #10131f;
|
||||||
color: #e5e7eb;
|
color: #e8ecf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
.root {
|
||||||
display: grid;
|
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;
|
gap: 8px;
|
||||||
padding: 12px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: 1px solid #374151;
|
border: 1px solid #374151;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
background: #1f2937;
|
background: #233047;
|
||||||
color: #f9fafb;
|
color: #f9fafb;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
@ -25,6 +108,28 @@ button:hover {
|
||||||
border-color: #10b981;
|
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 {
|
#status {
|
||||||
min-height: 18px;
|
min-height: 18px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,47 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html lang="ru">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="stylesheet" href="popup.css">
|
<link rel="stylesheet" href="popup.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<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="capture-page">Send Page</button>
|
||||||
<button id="capture-selection">Send Selection</button>
|
|
||||||
<button id="retry">Retry Pending</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>
|
<p id="status"></p>
|
||||||
</main>
|
</main>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,61 @@
|
||||||
|
|
||||||
var ext = typeof browser !== 'undefined' ? browser : chrome;
|
var ext = typeof browser !== 'undefined' ? browser : chrome;
|
||||||
var statusEl = document.getElementById('status');
|
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) {
|
function setStatus(text) {
|
||||||
statusEl.textContent = 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) {
|
function send(message) {
|
||||||
setStatus('Sending...');
|
setStatus('Sending...');
|
||||||
Promise.resolve(ext.runtime.sendMessage(message)).then(function (result) {
|
request(message).then(function (state) {
|
||||||
if (result && result.status === 'queued') setStatus('Queued until Verstak is available');
|
render(state);
|
||||||
else setStatus('Sent');
|
if (state.status && state.status.lastResult === 'queued') setStatus('Queued until Verstak is available');
|
||||||
|
else setStatus('Done');
|
||||||
}).catch(function (err) {
|
}).catch(function (err) {
|
||||||
setStatus(err && err.message ? err.message : String(err));
|
setStatus(err && err.message ? err.message : String(err));
|
||||||
});
|
});
|
||||||
|
|
@ -22,11 +67,27 @@
|
||||||
send({ type: 'verstak.capture', kind: 'page' });
|
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 () {
|
document.getElementById('retry').addEventListener('click', function () {
|
||||||
send({ type: 'verstak.capture', action: 'retryPending' });
|
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();
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue