343 lines
12 KiB
JavaScript
343 lines
12 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
var ext = typeof browser !== 'undefined' ? browser : chrome;
|
|
var protocol = globalThis.VerstakBrowser;
|
|
var queue = new protocol.CaptureQueue(protocol.browserStorageAdapter(ext));
|
|
var activityTracker = new protocol.DomainActivityTracker(
|
|
protocol.browserActivityStorageAdapter(ext),
|
|
function (batch) {
|
|
return getSettings().then(function (settings) {
|
|
return protocol.sendActivityBatch(settings.receiverUrl, settings.receiverToken, batch);
|
|
});
|
|
}
|
|
);
|
|
var i18n = globalThis.VerstakBrowserI18n;
|
|
var DEFAULT_SETTINGS = {
|
|
receiverUrl: protocol.DEFAULT_RECEIVER_URL,
|
|
receiverToken: '',
|
|
language: 'system',
|
|
passiveActivityEnabled: false,
|
|
passiveActivityExcludedDomains: []
|
|
};
|
|
var STATUS_KEY = 'verstak.status';
|
|
var ACTIVITY_FLUSH_ALARM = 'verstak-domain-activity-flush';
|
|
var localeCatalogs = null;
|
|
var focusedWindowID = null;
|
|
var activeTabID = null;
|
|
|
|
function getSettings() {
|
|
return ext.storage.local.get('settings').then(function (result) {
|
|
var settings = Object.assign({}, DEFAULT_SETTINGS, result && result.settings || {});
|
|
settings.language = i18n.normalizePreference(settings.language);
|
|
settings.passiveActivityEnabled = settings.passiveActivityEnabled === true;
|
|
settings.passiveActivityExcludedDomains = normalizeExcludedDomains(settings.passiveActivityExcludedDomains);
|
|
return settings;
|
|
});
|
|
}
|
|
|
|
function saveSettings(settings) {
|
|
settings = Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
|
settings.language = i18n.normalizePreference(settings.language);
|
|
settings.passiveActivityEnabled = settings.passiveActivityEnabled === true;
|
|
settings.passiveActivityExcludedDomains = normalizeExcludedDomains(settings.passiveActivityExcludedDomains);
|
|
return ext.storage.local.set({ settings: settings });
|
|
}
|
|
|
|
function normalizeExcludedDomains(value) {
|
|
var values = Array.isArray(value) ? value : String(value || '').split(/[\n,]/);
|
|
var seen = {};
|
|
return values.map(function (item) {
|
|
return protocol.normalizeHostnameV1(String(item || '').trim());
|
|
}).filter(function (item) {
|
|
if (!item || seen[item]) return false;
|
|
seen[item] = true;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function loadLocaleCatalogs() {
|
|
if (localeCatalogs) return localeCatalogs;
|
|
localeCatalogs = i18n.loadCatalogs(function (locale) {
|
|
return fetch(ext.runtime.getURL('locales/' + locale + '.json')).then(function (response) {
|
|
if (!response.ok) throw new Error('catalog load failed: ' + locale);
|
|
return response.json();
|
|
});
|
|
}).catch(function (error) {
|
|
console.warn('[verstak] localization catalogs unavailable:', error);
|
|
return { en: {}, ru: {} };
|
|
});
|
|
return localeCatalogs;
|
|
}
|
|
|
|
function browserLocale() {
|
|
try {
|
|
return ext.i18n && ext.i18n.getUILanguage ? ext.i18n.getUILanguage() : 'en';
|
|
} catch (_) {
|
|
return 'en';
|
|
}
|
|
}
|
|
|
|
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] || {};
|
|
});
|
|
}
|
|
|
|
function isFocusedWindow(windowID) {
|
|
return focusedWindowID == null || focusedWindowID === windowID;
|
|
}
|
|
|
|
function trackTab(tab, settings) {
|
|
if (!settings.passiveActivityEnabled || !tab || !isFocusedWindow(tab.windowId)) {
|
|
return activityTracker.setActiveHostname('', false);
|
|
}
|
|
activeTabID = tab.id == null ? activeTabID : tab.id;
|
|
var hostname = protocol.normalizeURLHostnameV1(tab.url || '');
|
|
if (!hostname || protocol.isExcludedHostname(hostname, settings.passiveActivityExcludedDomains)) {
|
|
return activityTracker.setActiveHostname('', false);
|
|
}
|
|
return activityTracker.setActiveHostname(hostname, true);
|
|
}
|
|
|
|
function refreshFocusedTab(settings) {
|
|
return activeTab().then(function (tab) {
|
|
return trackTab(tab, settings);
|
|
}).catch(function () {
|
|
return activityTracker.setActiveHostname('', false);
|
|
});
|
|
}
|
|
|
|
function configurePassiveActivity(settings) {
|
|
return activityTracker.setEnabled(settings.passiveActivityEnabled).then(function () {
|
|
if (!settings.passiveActivityEnabled) return undefined;
|
|
return refreshFocusedTab(settings);
|
|
});
|
|
}
|
|
|
|
function flushPassiveActivity() {
|
|
return getSettings().then(function (settings) {
|
|
if (!settings.passiveActivityEnabled) return activityTracker.setEnabled(false);
|
|
return activityTracker.setEnabled(true).then(function () {
|
|
return refreshFocusedTab(settings);
|
|
}).then(function () {
|
|
return activityTracker.flush();
|
|
});
|
|
});
|
|
}
|
|
|
|
function setupPassiveActivity() {
|
|
if (ext.alarms && ext.alarms.create) {
|
|
ext.alarms.create(ACTIVITY_FLUSH_ALARM, { periodInMinutes: 5 });
|
|
}
|
|
if (ext.idle && ext.idle.setDetectionInterval) {
|
|
ext.idle.setDetectionInterval(60);
|
|
}
|
|
return getSettings().then(configurePassiveActivity);
|
|
}
|
|
|
|
function captureFromInfo(kind, info, tab) {
|
|
return protocol.buildCapture({
|
|
kind: kind,
|
|
url: tab && tab.url || info.pageUrl || info.frameUrl || '',
|
|
title: tab && tab.title || '',
|
|
selectionText: info.selectionText || '',
|
|
linkUrl: info.linkUrl || '',
|
|
linkText: info.selectionText || '',
|
|
fileName: info.fileName || '',
|
|
fileMime: info.fileMime || '',
|
|
fileSize: info.fileSize || 0,
|
|
fileText: info.fileText || '',
|
|
fileDataBase64: info.fileDataBase64 || ''
|
|
});
|
|
}
|
|
|
|
function sendOrQueue(payload) {
|
|
return getSettings().then(function (settings) {
|
|
return protocol.sendCapture(settings.receiverUrl, settings.receiverToken, payload).catch(function () {
|
|
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;
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function retryPending() {
|
|
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),
|
|
activityTracker.getState()
|
|
]).then(function (results) {
|
|
return {
|
|
settings: results[0],
|
|
pendingCount: results[1].length,
|
|
pendingActivityCount: results[3].pendingBatches.length,
|
|
activityState: results[3],
|
|
status: results[2] && results[2][STATUS_KEY] || {}
|
|
};
|
|
});
|
|
}
|
|
|
|
function setupContextMenus() {
|
|
if (!ext.contextMenus) return Promise.resolve();
|
|
return Promise.all([getSettings(), loadLocaleCatalogs()]).then(function (results) {
|
|
var locale = i18n.resolveLocale(results[0].language, browserLocale());
|
|
var t = i18n.createTranslator(results[1], locale);
|
|
return new Promise(function (resolve) {
|
|
var created = false;
|
|
function createMenus() {
|
|
if (created) return;
|
|
created = true;
|
|
ext.contextMenus.create({ id: 'verstak-capture-page', title: t('context.sendPage', null, 'Send page to Verstak'), contexts: ['page'] });
|
|
ext.contextMenus.create({ id: 'verstak-capture-selection', title: t('context.sendSelection', null, 'Send selection to Verstak'), contexts: ['selection'] });
|
|
ext.contextMenus.create({ id: 'verstak-capture-link', title: t('context.sendLink', null, 'Send link to Verstak'), contexts: ['link'] });
|
|
resolve();
|
|
}
|
|
var removal;
|
|
try {
|
|
removal = ext.contextMenus.removeAll(createMenus);
|
|
} catch (_) {
|
|
removal = ext.contextMenus.removeAll();
|
|
}
|
|
if (removal && typeof removal.then === 'function') removal.then(createMenus, createMenus);
|
|
});
|
|
});
|
|
}
|
|
|
|
ext.runtime.onInstalled.addListener(setupContextMenus);
|
|
if (ext.contextMenus && ext.contextMenus.onClicked) {
|
|
ext.contextMenus.onClicked.addListener(function (info, tab) {
|
|
var kind = info.menuItemId === 'verstak-capture-selection' ? 'selection'
|
|
: info.menuItemId === 'verstak-capture-link' ? 'link'
|
|
: 'page';
|
|
sendOrQueue(captureFromInfo(kind, info, tab || {}));
|
|
});
|
|
}
|
|
|
|
function handleMessage(message) {
|
|
return ready.then(function () {
|
|
if (!message || message.type !== 'verstak.capture') return undefined;
|
|
if (message.action === 'getState') return getState();
|
|
if (message.action === 'saveSettings') {
|
|
return saveSettings(message.settings).then(function () {
|
|
return setupContextMenus();
|
|
}).then(function () {
|
|
return getSettings().then(configurePassiveActivity);
|
|
}).then(function () {
|
|
return setStatus({ receiverReachable: null, lastResult: 'settings-saved', lastError: '' });
|
|
}).then(getState);
|
|
}
|
|
if (message.action === 'retryPending') return retryPending().then(getState);
|
|
if (message.action === 'retryPendingActivity') return activityTracker.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;
|
|
});
|
|
|
|
if (ext.tabs && ext.tabs.onActivated) {
|
|
ext.tabs.onActivated.addListener(function (info) {
|
|
if (!isFocusedWindow(info.windowId) || !ext.tabs.get) return;
|
|
activeTabID = info.tabId;
|
|
ready.then(function () { return getSettings(); }).then(function (settings) {
|
|
return ext.tabs.get(info.tabId).then(function (tab) { return trackTab(tab, settings); });
|
|
}).catch(function () {});
|
|
});
|
|
}
|
|
|
|
if (ext.tabs && ext.tabs.onUpdated) {
|
|
ext.tabs.onUpdated.addListener(function (tabID, changeInfo, tab) {
|
|
if (tabID !== activeTabID || (!changeInfo.url && !(tab && tab.url))) return;
|
|
ready.then(function () { return getSettings(); }).then(function (settings) {
|
|
return trackTab(tab || { id: tabID, url: changeInfo.url }, settings);
|
|
}).catch(function () {});
|
|
});
|
|
}
|
|
|
|
if (ext.windows && ext.windows.onFocusChanged) {
|
|
ext.windows.onFocusChanged.addListener(function (windowID) {
|
|
focusedWindowID = windowID;
|
|
if (windowID === (ext.windows.WINDOW_ID_NONE == null ? -1 : ext.windows.WINDOW_ID_NONE)) {
|
|
activityTracker.setActiveHostname('', false).catch(function () {});
|
|
return;
|
|
}
|
|
ready.then(function () { return getSettings(); }).then(refreshFocusedTab).catch(function () {});
|
|
});
|
|
}
|
|
|
|
if (ext.idle && ext.idle.onStateChanged) {
|
|
ext.idle.onStateChanged.addListener(function (state) {
|
|
if (state === 'active') {
|
|
ready.then(function () { return getSettings(); }).then(refreshFocusedTab).catch(function () {});
|
|
return;
|
|
}
|
|
activityTracker.setActiveHostname('', false).catch(function () {});
|
|
});
|
|
}
|
|
|
|
if (ext.alarms && ext.alarms.onAlarm) {
|
|
ext.alarms.onAlarm.addListener(function (alarm) {
|
|
if (alarm && alarm.name === ACTIVITY_FLUSH_ALARM) flushPassiveActivity().catch(function () {});
|
|
});
|
|
}
|
|
|
|
if (ext.tabs && ext.tabs.onRemoved) {
|
|
ext.tabs.onRemoved.addListener(function (tabID) {
|
|
if (tabID === activeTabID) {
|
|
activeTabID = null;
|
|
activityTracker.setActiveHostname('', false).catch(function () {});
|
|
}
|
|
});
|
|
}
|
|
|
|
var ready = activityTracker.initialize().then(setupPassiveActivity).catch(function (error) {
|
|
console.warn('[verstak] passive activity initialization failed:', error);
|
|
});
|
|
})();
|