mirror of
https://github.com/bellingcat/auto-archiver-extension.git
synced 2026-06-07 19:18:33 +03:00
new features
This commit is contained in:
11
package-lock.json
generated
11
package-lock.json
generated
@@ -5,6 +5,7 @@
|
||||
"packages": {
|
||||
"": {
|
||||
"dependencies": {
|
||||
"http-status-codes": "^2.2.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"materialize-css": "^1.0.0-rc.2",
|
||||
"vue": "^3.2.45",
|
||||
@@ -4442,6 +4443,11 @@
|
||||
"entities": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/http-status-codes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz",
|
||||
"integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="
|
||||
},
|
||||
"node_modules/human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
@@ -12165,6 +12171,11 @@
|
||||
"entities": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"http-status-codes": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz",
|
||||
"integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="
|
||||
},
|
||||
"human-signals": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"extends": "stylelint-config-xo"
|
||||
},
|
||||
"dependencies": {
|
||||
"http-status-codes": "^2.2.0",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"materialize-css": "^1.0.0-rc.2",
|
||||
"vue": "^3.2.45",
|
||||
@@ -42,7 +43,7 @@
|
||||
"sourceDir": "distribution",
|
||||
"run": {
|
||||
"startUrl": [
|
||||
"https://github.com/fregante/browser-extension-template"
|
||||
"https://github.com/bellingcat/auto-archiver-extension"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@ body {
|
||||
}
|
||||
|
||||
#app {
|
||||
min-width: 40em;
|
||||
min-width: 45em;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ body {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#archive-results .row {
|
||||
table.archive-results .row {
|
||||
/* table-layout: fixed; */
|
||||
width: 90%;
|
||||
max-width: 100px;
|
||||
}
|
||||
|
||||
/* #archive-results td {
|
||||
/* .archive-results td {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#archive-results td:nth-child(2) {
|
||||
.archive-results td:nth-child(2) {
|
||||
width: 150px;
|
||||
} */
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!doctype html>
|
||||
<!-- <!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>Options</title>
|
||||
|
||||
@@ -33,4 +33,4 @@
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script src="../js/options.js" type="module"></script>
|
||||
<script src="../js/options.js" type="module"></script> -->
|
||||
|
||||
@@ -1,64 +1,142 @@
|
||||
|
||||
// Import './options-storage.js';
|
||||
import optionsStorage from './options-storage.js';
|
||||
import { getReasonPhrase } from 'http-status-codes';
|
||||
|
||||
// TODO: stable ID https://developer.chrome.com/docs/extensions/mv3/tut_oauth/
|
||||
const API_ENDPOINT = 'http://localhost:8004/tasks'
|
||||
// const API_ENDPOINT = 'http://134.122.58.133:8004/tasks';
|
||||
|
||||
chrome.runtime.onMessage.addListener(((r, s, sR) => {
|
||||
processMessages(r, s, sR);
|
||||
const LOGIN_FAILED = `Could not login, make sure your google account email has been granted access by the developers.`;
|
||||
|
||||
chrome.runtime.onMessage.addListener(((r, s, sendResponse) => {
|
||||
processMessages(r, s)
|
||||
//TODO: improve body
|
||||
.then(response => {
|
||||
console.log(`SUCCESS (${r.action}): ${JSON.stringify(response)}`)
|
||||
sendResponse({ status: "success", result: response })
|
||||
}
|
||||
).catch(error => {
|
||||
let message = error.message == "Failed to fetch" ? `Unable to call the API: ${error.message}` : error.message;
|
||||
console.log(`ERROR (${r.action}): ${error}`)
|
||||
setErrorMessage(message);
|
||||
sendResponse({ status: "error", result: message })
|
||||
});
|
||||
return true; // Needed for sendResponse to be async
|
||||
}));
|
||||
|
||||
async function processMessages(request, sender, sendResponse) {
|
||||
console.info(`action {${request.action}} from ${sender.tab ? 'content-script (' + sender.tab.url + ')' : 'the extension'}`);
|
||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||
function processMessages(request, sender) {
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
console.info(`action {${request.action}} from ${sender.tab ? 'content-script (' + sender.tab.url + ')' : 'the extension'}`);
|
||||
|
||||
switch (request.action) {
|
||||
case 'archive': {
|
||||
archiveUrl(sendResponse, accessToken);
|
||||
archiveUrl(resolve, reject, request.optionalUrl);
|
||||
break;
|
||||
}
|
||||
case 'search': {
|
||||
const tasks = await search(request.query, accessToken);
|
||||
sendResponse(tasks);
|
||||
search(resolve, reject, request.query);
|
||||
break;
|
||||
}
|
||||
case 'status': {
|
||||
const taskDb = await getTaskById(request.task.task_id);
|
||||
if (taskDb?.status === 'SUCCESS' || taskDb?.status === 'FAILURE'|| taskDb?.status === 'REVOKED') {
|
||||
console.log('ALREADY FINSIHED, NO REQS');
|
||||
sendResponse(taskDb);
|
||||
const taskDb = await getTaskById(request.task.id);
|
||||
if (taskDb?.status === 'SUCCESS' || taskDb?.status === 'FAILURE' || taskDb?.status === 'REVOKED') {
|
||||
console.log('ALREADY FINISHED, NO REQS');
|
||||
resolve(taskDb);
|
||||
} else {
|
||||
const taskFresh = await checkTaskStatus(request.task, accessToken);
|
||||
sendResponse(taskFresh);
|
||||
resolve(await checkTaskStatus(resolve, reject, request.task));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'getTasks': {
|
||||
sendResponse(await getAllTasks());
|
||||
resolve(await getAllTasks());
|
||||
break;
|
||||
}
|
||||
case 'syncLocalTasks': {
|
||||
syncLocalTasks(resolve, reject);
|
||||
break;
|
||||
}
|
||||
case 'getErrorMessage': {
|
||||
resolve(await getErrorMessage());
|
||||
break;
|
||||
}
|
||||
case 'setErrorMessage': {
|
||||
await setErrorMessage(request.errorMessage)
|
||||
break;
|
||||
}
|
||||
case 'getCurrentUrl': {
|
||||
getUrl(resolve, reject);
|
||||
break;
|
||||
}
|
||||
case 'getProfileEmail': {
|
||||
getProfileEmail(resolve, reject);
|
||||
break;
|
||||
}
|
||||
case 'oauthLogin': {
|
||||
oauthLogin(resolve, reject);
|
||||
break;
|
||||
}
|
||||
// No default
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function archiveUrl(sendResponse, accessToken) {
|
||||
|
||||
function getUrl(resolve, reject) {
|
||||
chrome.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
}, async tabs => {
|
||||
const url = tabs[0].url;
|
||||
console.log(`url=${url}`);
|
||||
const response = await searchTask(url, accessToken);
|
||||
const newArchive = { url, task_id: response.task_id, status: 'PENDING', result: {} };
|
||||
await upsertTask(newArchive);
|
||||
sendResponse(newArchive);
|
||||
resolve(url);
|
||||
});
|
||||
}
|
||||
|
||||
function searchTask(url, accessToken) {
|
||||
function getProfileEmail(resolve, reject) {
|
||||
chrome.identity.getProfileUserInfo({ accountStatus: 'ANY' }, async userInfo => {
|
||||
resolve(userInfo);
|
||||
//TODO: reject if bad user info?
|
||||
});
|
||||
}
|
||||
|
||||
function oauthLogin(resolve, reject) {
|
||||
try {
|
||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||
console.error(`GOT token ${accessToken}`);
|
||||
resolve(true);
|
||||
});
|
||||
} catch (e) {
|
||||
// reject(new Error(`LOGIN FAILED: ${e}`));
|
||||
console.error(`LOGIN FAILED: ${e}`);
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
function archiveUrl(resolve, reject, optionalUrl) {
|
||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||
if (accessToken == undefined) {
|
||||
reject(new Error(LOGIN_FAILED));
|
||||
return;
|
||||
}
|
||||
chrome.tabs.query({
|
||||
active: true,
|
||||
lastFocusedWindow: true,
|
||||
}, async tabs => {
|
||||
console.warn(optionalUrl)
|
||||
const url = optionalUrl || tabs[0].url;
|
||||
console.log(`url=${url}`);
|
||||
submitUrlArchive(url, accessToken).then(async response => {
|
||||
const newArchive = { url, id: response.id, status: 'PENDING', result: {} };
|
||||
await upsertTask(newArchive);
|
||||
resolve(newArchive);
|
||||
}).catch(e => reject(e));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function submitUrlArchive(url, accessToken) {
|
||||
console.log('API: SUBMIT');
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(API_ENDPOINT, {
|
||||
@@ -67,72 +145,128 @@ function searchTask(url, accessToken) {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ url, access_token: accessToken }),
|
||||
}).then(
|
||||
response => response.json(),
|
||||
).then(response => resolve(response),
|
||||
).catch(error => {
|
||||
console.log(`There was an error: ${error}`);
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
.then(response => resolve(response))
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
function checkTaskStatus(task, accessToken) {
|
||||
function checkTaskStatus(resolve, reject, task) {
|
||||
console.log('API: STATUS');
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`${API_ENDPOINT}/${task.task_id}?` + new URLSearchParams({ access_token: accessToken }), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(
|
||||
response => response.json(),
|
||||
).then(response => {
|
||||
const new_task = {
|
||||
url: task.url,
|
||||
task_id: response.task_id,
|
||||
status: response.task_status,
|
||||
result: typeof response.task_result == "object" ? response.task_result : JSON.parse(response.task_result),
|
||||
};
|
||||
console.log(`status ${new_task.url}: ${new_task.task_id}`);
|
||||
upsertTask(new_task);
|
||||
resolve(new_task);
|
||||
},
|
||||
).catch(error => reject(error));
|
||||
return new Promise((InnerResolve, innerReject) => {
|
||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||
if (accessToken == undefined) {
|
||||
reject(new Error(LOGIN_FAILED));
|
||||
return;
|
||||
}
|
||||
fetch(`${API_ENDPOINT}/${task.id}?` + new URLSearchParams({ access_token: accessToken }), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
.then(response => {
|
||||
const new_task = {
|
||||
url: task.url,
|
||||
id: response.id,
|
||||
status: response.status,
|
||||
result: typeof response.result == "object" ? response.result : JSON.parse(response.result),
|
||||
};
|
||||
console.log(`status ${new_task.url}: ${new_task.id}`);
|
||||
(async () => {
|
||||
await upsertTask(new_task);
|
||||
InnerResolve(new_task);
|
||||
})();
|
||||
})
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function search(query, accessToken) {
|
||||
function search(resolve, reject, url) {
|
||||
console.log('API: SEARCH');
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(`${API_ENDPOINT}/search?` + new URLSearchParams({ access_token: accessToken, query }), {
|
||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||
if (accessToken == undefined) {
|
||||
reject(new Error(LOGIN_FAILED));
|
||||
return;
|
||||
}
|
||||
fetch(`${API_ENDPOINT}/search-url?` + new URLSearchParams({ access_token: accessToken, url }), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
}).then(
|
||||
response => response.json(),
|
||||
).then(response => resolve(response),
|
||||
).catch(error => {
|
||||
console.log(`There was an error: ${error}`);
|
||||
reject(error);
|
||||
});
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
.then(jsonResponse => { resolve(jsonResponse.map(t => { t.status = "SUCCESS"; return t; })) })
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
async function syncLocalTasks(resolve, reject) {
|
||||
console.log('API: SYNC');
|
||||
chrome.identity.getAuthToken({ interactive: true }, async accessToken => {
|
||||
if (accessToken == undefined) {
|
||||
reject(new Error(LOGIN_FAILED));
|
||||
return;
|
||||
}
|
||||
fetch(`${API_ENDPOINT}/sync?` + new URLSearchParams({ access_token: accessToken }), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(getJsonOrError)
|
||||
.then(async cloudTasks => {
|
||||
const storage = await optionsStorage.getAll();
|
||||
cloudTasks.forEach(cTask => {
|
||||
storage.archivedUrls[cTask.id] = {
|
||||
url: cTask.url,
|
||||
id: cTask.id,
|
||||
status: "SUCCESS",
|
||||
result: typeof cTask.result == "object" ? cTask.result : JSON.parse(cTask.result),
|
||||
};
|
||||
})
|
||||
await optionsStorage.set(storage);
|
||||
resolve(storage.archivedUrls)
|
||||
})
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
}
|
||||
|
||||
async function getJsonOrError(response) {
|
||||
let additionalErrorInfo = "";
|
||||
if (response.status == 401) additionalErrorInfo = `Check that this email has been granted permission.`;
|
||||
if (response.status != 200) throw new Error(`${response.status}: ${getReasonPhrase(response.status)} ${additionalErrorInfo}`);
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function getAllTasks() {
|
||||
const storage = await optionsStorage.getAll();
|
||||
console.log(`OP: GET_ALL, has ${Object.keys(storage.archivedUrls).length} entries`)
|
||||
return storage.archivedUrls;
|
||||
}
|
||||
|
||||
// TODO: improve with less reads from storage
|
||||
async function upsertTask(task) {
|
||||
const storage = await optionsStorage.getAll();
|
||||
storage.archivedUrls[task.task_id] = task;
|
||||
storage.archivedUrls[task.id] = task;
|
||||
await optionsStorage.set(storage);
|
||||
}
|
||||
|
||||
async function getTaskById(task) {
|
||||
const storage = await optionsStorage.getAll();
|
||||
return storage.archivedUrls[task.task_id];
|
||||
return storage.archivedUrls[task.id];
|
||||
}
|
||||
|
||||
async function getErrorMessage() {
|
||||
const storage = await optionsStorage.getAll();
|
||||
console.log(`OP: GET_ERROR_MESSAGE has '${storage.errorMessage}'`)
|
||||
return storage.errorMessage;
|
||||
}
|
||||
|
||||
async function setErrorMessage(errorMessage) {
|
||||
const storage = await optionsStorage.getAll();
|
||||
storage.errorMessage = errorMessage;
|
||||
await optionsStorage.set(storage);
|
||||
}
|
||||
|
||||
@@ -3,9 +3,11 @@ import OptionsSync from 'webext-options-sync';
|
||||
export default new OptionsSync({
|
||||
defaults: {
|
||||
archivedUrls: {},
|
||||
errorMessage: ""
|
||||
},
|
||||
migrations: [
|
||||
OptionsSync.migrations.removeUnused,
|
||||
// OptionsSync.migrations.removeUnused,
|
||||
],
|
||||
logging: true,
|
||||
storageType: "local"
|
||||
});
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
// eslint-disable-next-line import/no-unassigned-import
|
||||
import 'webext-base-css';
|
||||
import '../css/options.css';
|
||||
// import 'webext-base-css';
|
||||
// import '../css/options.css';
|
||||
|
||||
import optionsStorage from './options-storage.js';
|
||||
// import optionsStorage from './options-storage.js';
|
||||
|
||||
const rangeInputs = [...document.querySelectorAll('input[type="range"][name^="color"]')];
|
||||
const numberInputs = [...document.querySelectorAll('input[type="number"][name^="color"]')];
|
||||
const output = document.querySelector('.color-output');
|
||||
// const rangeInputs = [...document.querySelectorAll('input[type="range"][name^="color"]')];
|
||||
// const numberInputs = [...document.querySelectorAll('input[type="number"][name^="color"]')];
|
||||
// const output = document.querySelector('.color-output');
|
||||
|
||||
function updateOutputColor() {
|
||||
output.style.backgroundColor = `rgb(${rangeInputs[0].value}, ${rangeInputs[1].value}, ${rangeInputs[2].value})`;
|
||||
}
|
||||
// function updateOutputColor() {
|
||||
// output.style.backgroundColor = `rgb(${rangeInputs[0].value}, ${rangeInputs[1].value}, ${rangeInputs[2].value})`;
|
||||
// }
|
||||
|
||||
function updateInputField(event) {
|
||||
numberInputs[rangeInputs.indexOf(event.currentTarget)].value = event.currentTarget.value;
|
||||
}
|
||||
// function updateInputField(event) {
|
||||
// numberInputs[rangeInputs.indexOf(event.currentTarget)].value = event.currentTarget.value;
|
||||
// }
|
||||
|
||||
for (const input of rangeInputs) {
|
||||
input.addEventListener('input', updateOutputColor);
|
||||
input.addEventListener('input', updateInputField);
|
||||
}
|
||||
// for (const input of rangeInputs) {
|
||||
// input.addEventListener('input', updateOutputColor);
|
||||
// input.addEventListener('input', updateInputField);
|
||||
// }
|
||||
|
||||
async function init() {
|
||||
await optionsStorage.syncForm('#options-form');
|
||||
updateOutputColor();
|
||||
}
|
||||
// async function init() {
|
||||
// await optionsStorage.syncForm('#options-form');
|
||||
// updateOutputColor();
|
||||
// }
|
||||
|
||||
init();
|
||||
// init();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {createApp} from 'vue';
|
||||
import { createApp } from 'vue';
|
||||
import Popup from '../vue/Popup.vue';
|
||||
import 'materialize-css/dist/css/materialize.min.css';
|
||||
import 'material-design-icons/iconfont/material-icons.css';
|
||||
@@ -12,6 +12,7 @@ app.mount('#app');
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
// TODO: uncomment if using options
|
||||
// listenForOptionsClick();
|
||||
M.Tooltip.init(document.querySelectorAll('.tooltipped'), { enterDelay: 500 }); // enable tooltips
|
||||
});
|
||||
|
||||
// Function listenForOptionsClick() {
|
||||
|
||||
19
source/js/utils.js
Normal file
19
source/js/utils.js
Normal file
@@ -0,0 +1,19 @@
|
||||
// /**
|
||||
// *
|
||||
// *
|
||||
// */
|
||||
// export async function callBackground(parameters) {
|
||||
// try {
|
||||
// const answer = await chrome.runtime.sendMessage(parameters);
|
||||
// if (answer.status == "error") {
|
||||
// console.error(`error: ${answer.result}`)
|
||||
// //TODO: modal/errors
|
||||
// return null;
|
||||
// } else {
|
||||
// return answer.result;
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error(e);
|
||||
// return null;
|
||||
// }
|
||||
// }
|
||||
@@ -15,7 +15,7 @@
|
||||
"128": "img/ben-archiver.png"
|
||||
},
|
||||
"permissions": [
|
||||
"storage", "tabs", "identity"
|
||||
"storage", "tabs", "identity", "identity.email"
|
||||
],
|
||||
"host_permissions": [
|
||||
],
|
||||
|
||||
@@ -1,16 +1,23 @@
|
||||
<template>
|
||||
<p v-if="!login">please <a v-on:click="oauthLogin" href="#">login</a> into your google account</p>
|
||||
<div v-if="errorMessage.length" class="red darken-1 white-text">Error: {{ errorMessage }}</div>
|
||||
<h5>
|
||||
<img src="../img/ben-archiver.png" alt="icon" id="icon">
|
||||
auto-archiver extension
|
||||
<button v-on:click="archive" class="waves-effect waves-light btn-small right">Archive!</button>
|
||||
<!-- <button v-on:click="searchF" class="waves-effect waves-light btn-small right">SEARCH</button> -->
|
||||
<button v-on:click="archive" class="tooltipped waves-effect waves-light btn-small right" data-position="bottom"
|
||||
data-tooltip="Archive this URL">
|
||||
<i class="material-icons left">cloud</i> Archive!</button>
|
||||
<button v-on:click="checkArchive"
|
||||
class="tooltipped waves-effect waves-light btn-small right flat light-blue darken-3" style="margin-right:10px;"
|
||||
data-position="bottom" data-tooltip="Check if this URL has been archived">lookup URL</button>
|
||||
</h5>
|
||||
<!-- <label><input type="checkbox" v-model="takeScreenshot" /><span>take screenshot</span></label> -->
|
||||
<div class="input-field col s6">
|
||||
<i class="material-icons prefix">search</i>
|
||||
<input id="icon_prefix" type="text" v-model="search">
|
||||
<input id="icon_prefix" type="text" ref="search" v-model="search" v-on:input="searchTasks">
|
||||
<label for="icon_prefix">Search for URLs</label>
|
||||
</div>
|
||||
<table id="archive-results">
|
||||
<table class="archive-results" v-if="localTasksShownLength > 0 || onlineTasksLength > 0">
|
||||
<thead>
|
||||
<tr class="row">
|
||||
<th class="col s1"></th>
|
||||
@@ -20,12 +27,25 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<TaskItem v-for="t in displayTasks" :key="t.task_id" :initial-task="t" />
|
||||
<TaskItem v-for="t in displayTasks" :key="t.id" :initial-task="t" taskType="local" />
|
||||
<TaskItem v-for="t in onlineTasks" :key="t.id" :initial-task="t" taskType="online" />
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="noSearchResults">
|
||||
No results... do you want to <a v-on:click="archiveFromSearch" href="#">archive</a>?
|
||||
</div>
|
||||
<p v-show="login">
|
||||
<a href="#" v-on:click="syncLocalTasks" class="tooltipped"
|
||||
data-tooltip="updates local database with entries submitted by the current user" data-position="top">Sync</a>
|
||||
my cloud archives.
|
||||
</p>
|
||||
<small>
|
||||
<span v-if="login">Hello {{ login }}!</span>
|
||||
<span class="right"><a href="https://github.com/bellingcat/auto-archiver-extension/issues" target="_blank">Issue
|
||||
tracker</a></span>
|
||||
</small>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import M from 'materialize-css';
|
||||
import TaskItem from './TaskItem.vue';
|
||||
@@ -33,59 +53,153 @@ import TaskItem from './TaskItem.vue';
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
tasks: [],
|
||||
isLoading: false,
|
||||
search: ''
|
||||
login: false,
|
||||
tasks: {},
|
||||
onlineTasks: [],
|
||||
isSearchingOnline: false,
|
||||
search: '',
|
||||
errorMessage: ''
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
archive: function () {
|
||||
// M.toast({html: 'DONE'})
|
||||
|
||||
// chrome.tabs.sendMessage
|
||||
this.isLoading = !this.isLoading;
|
||||
(async () => {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: "archive"
|
||||
});
|
||||
const response = await this.callBackground({ action: "archive" });
|
||||
if (!response) return;
|
||||
this.url = response.url;
|
||||
this.task_id = response.task_id;
|
||||
this.id = response.id;
|
||||
this.addTask(response)
|
||||
})();
|
||||
},
|
||||
searchF: function(){
|
||||
archiveFromSearch: function () {
|
||||
//TODO: how to deduplicate? calling archive(this.search) is bad because of default injections into archive
|
||||
(async () => {
|
||||
const response = await chrome.runtime.sendMessage({
|
||||
action: "search",
|
||||
query: "search query"
|
||||
});
|
||||
console.log(response)
|
||||
const response = await this.callBackground({ action: "archive", optionalUrl: this.search });
|
||||
if (!response) return;
|
||||
this.url = response.url;
|
||||
this.id = response.id;
|
||||
this.addTask(response)
|
||||
})();
|
||||
},
|
||||
checkArchive: function () {
|
||||
(async () => {
|
||||
const response = await this.callBackground({ action: "getCurrentUrl" });
|
||||
if (!response) return;
|
||||
this.search = response;
|
||||
this.$refs.search.focus();
|
||||
this.searchTasks();
|
||||
})();
|
||||
},
|
||||
displayAllTasks: function () {
|
||||
(async () => {
|
||||
const tasks = await chrome.runtime.sendMessage({
|
||||
action: "getTasks"
|
||||
});
|
||||
console.log(tasks)
|
||||
const response = await this.callBackground({ action: "getTasks" });
|
||||
if (!response) return;
|
||||
this.tasks = response;
|
||||
})();
|
||||
},
|
||||
syncLocalTasks: function () {
|
||||
(async () => {
|
||||
console.log("SYNC")
|
||||
const tasks = await this.callBackground({ action: "syncLocalTasks" });
|
||||
console.log(`TASKS: ${JSON.stringify(tasks)}`)
|
||||
if (!tasks) return;
|
||||
this.tasks = tasks;
|
||||
M.toast({ html: `sync complete: ${this.localTasksLength} task${this.localTasksLength != 1 ? 's' : ''} available`, classes: "green accent-4" });
|
||||
})();
|
||||
},
|
||||
displayLogin: function () {
|
||||
(async () => {
|
||||
const response = await this.callBackground({ action: "getProfileEmail" });
|
||||
if (!response) {
|
||||
this.login = false;
|
||||
} else {
|
||||
this.login = response.email;
|
||||
}
|
||||
})();
|
||||
},
|
||||
clearErrorMessage: function () {
|
||||
setTimeout(async () => {
|
||||
this.errorMessage = "";
|
||||
await this.callBackground({ action: "setErrorMessage", errorMessage: this.errorMessage });
|
||||
}, 3000)
|
||||
},
|
||||
displayErrorMessage: function () {
|
||||
(async () => {
|
||||
this.errorMessage = await this.callBackground({ action: "getErrorMessage" });
|
||||
this.clearErrorMessage()
|
||||
})();
|
||||
},
|
||||
oauthLogin: function () {
|
||||
(async () => {
|
||||
const loginSuccessful = await this.callBackground({ action: "oauthLogin" });
|
||||
if (loginSuccessful === null) return;
|
||||
M.toast({ html: loginSuccessful ? "login success" : "login failed", classes: loginSuccessful ? "green accent-4" : "red darken-1" });
|
||||
if (loginSuccessful) { this.displayLogin(); }
|
||||
})();
|
||||
},
|
||||
addTask: function (task) {
|
||||
this.tasks[task.task_id] = task;
|
||||
this.tasks[task.id] = task;
|
||||
},
|
||||
searchTasks: function () {
|
||||
console.log(`searching tasks? ${!this.isSearchingOnline}`);
|
||||
if (this.isSearchingOnline) {
|
||||
console.log(`skipping search, another is still active`);
|
||||
return;
|
||||
}
|
||||
if (this.search.length <= 3) {
|
||||
this.onlineTasks = [];
|
||||
return;
|
||||
}
|
||||
(async () => {
|
||||
this.isSearchingOnline = true; {
|
||||
const onlineTasks = await this.callBackground({ action: "search", query: this.search });
|
||||
if (!onlineTasks) return;
|
||||
this.onlineTasks = (onlineTasks || []).filter(id => !Object.keys(this.tasks).includes(id))
|
||||
}
|
||||
this.isSearchingOnline = false;
|
||||
})();
|
||||
},
|
||||
callBackground: async function (parameters) {
|
||||
try {
|
||||
const answer = await chrome.runtime.sendMessage(parameters);
|
||||
if (answer.status == "error") {
|
||||
console.error(`showing error to user: ${JSON.stringify(answer.result)}`)
|
||||
M.toast({ html: `Error: ${answer.result}`, classes: "red darken-1", completeCallback: this.clearErrorMessage })
|
||||
return null;
|
||||
} else {
|
||||
return answer.result;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
if (parameters.action == "search") this.isSearchingOnline = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
displayTasks() {
|
||||
let st = Object.values(this.tasks)
|
||||
return Object.values(this.tasks)
|
||||
.filter(t => t?.url.toLowerCase().includes(this.search.toLowerCase()))
|
||||
.sort((t1, t2) => (t1?.result?._processed_at || 0) - (t2?.result?._processed_at || 0)).slice(0, 25)
|
||||
return st
|
||||
}
|
||||
},
|
||||
noSearchResults() {
|
||||
return this.search.length > 3 && !this.isSearchingOnline && Object.keys(this.onlineTasks).length == 0;
|
||||
},
|
||||
localTasksShownLength() {
|
||||
return Object.keys(this.displayTasks).length > 0;
|
||||
},
|
||||
localTasksLength() {
|
||||
return Object.keys(this.tasks).length;
|
||||
},
|
||||
onlineTasksLength() {
|
||||
return Object.keys(this.onlineTasks).length > 0;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
M.AutoInit()
|
||||
this.displayAllTasks()
|
||||
M.AutoInit();
|
||||
this.displayAllTasks();
|
||||
this.displayLogin();
|
||||
this.displayErrorMessage();
|
||||
},
|
||||
created() { },
|
||||
components: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<tr class="row">
|
||||
<td class="col s1">
|
||||
<div v-if="task.status == 'PENDING'" class="preloader-wrapper small active">
|
||||
<div v-if="taskPending" class="preloader-wrapper small active">
|
||||
<div class="spinner-layer ">
|
||||
<div class="circle-clipper left">
|
||||
<div class="circle"></div>
|
||||
@@ -14,24 +14,39 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="task.status == 'SUCCESS'">
|
||||
<i class="material-icons small green-text darken-4">done</i>
|
||||
<div v-if="taskSucceeded">
|
||||
<i v-if="taskType == 'online'" title="found on the cloud"
|
||||
class="material-icons small green-text darken-4">cloud_done</i>
|
||||
<i v-if="taskType == 'local'" title="found locally"
|
||||
class="material-icons small green-text darken-4">done</i>
|
||||
</div>
|
||||
<div v-if="task.status == 'FAILURE' || task.status == 'REVOKED'">
|
||||
<div v-if="taskFailed">
|
||||
<i class="material-icons small red-text darken-4">clear</i>
|
||||
</div>
|
||||
</td>
|
||||
<td class="col s5"><a :href="task?.url" target="_blank">{{ task.url }}</a></td>
|
||||
<td class="col s2"><a v-if="archiveUrl.length" :href="archiveUrl" target="_blank">{{ task?.result?.status || "open"
|
||||
}}</a> </td>
|
||||
}}</a></td>
|
||||
<td class="col s3">{{ readbleDate }}</td>
|
||||
<td class="col s1" v-if="(taskFailed || taskSucceeded) && taskType == 'local'">
|
||||
<a class="delete-btn" href="#" v-on:click="deleteTask"><i class="material-icons small">delete</i></a>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<style>
|
||||
.delete-btn {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.delete-btn:hover {
|
||||
color: darkred;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'TaskItem',
|
||||
props: ['initialTask'],
|
||||
props: ['initialTask', 'taskType'],
|
||||
data() {
|
||||
return {
|
||||
task: this.initialTask
|
||||
@@ -39,37 +54,55 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
checkStatus: function () {
|
||||
console.log(this.task)
|
||||
console.log(`Checking status ${JSON.stringify(this.task)}`);
|
||||
if (this.taskFinished(this.task)) return
|
||||
this.intervalId = setInterval(function () {
|
||||
chrome.runtime.sendMessage({
|
||||
action: "status",
|
||||
task: this.task
|
||||
}).then(updated_task => {
|
||||
console.log(updated_task)
|
||||
this.callBackground(
|
||||
{ action: "status", task: this.task }
|
||||
).then(updated_task => {
|
||||
if (this.taskFinished(updated_task)) {
|
||||
clearInterval(this.intervalId);
|
||||
this.task = updated_task
|
||||
}
|
||||
})
|
||||
});
|
||||
}.bind(this), 2500);
|
||||
},
|
||||
taskFinished: function (task) {
|
||||
return task.status == 'SUCCESS' || task.status == 'FAILURE' || task.status == 'REVOKED';
|
||||
},
|
||||
callBackground: async function (parameters) {
|
||||
try {
|
||||
const answer = await chrome.runtime.sendMessage(parameters);
|
||||
if (answer.status == "error") {
|
||||
console.error(`error: ${answer.result}`)
|
||||
//TODO: modal/errors
|
||||
return null;
|
||||
} else {
|
||||
return answer.result;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
archiveUrl() {
|
||||
// return this.task?.result?.media?.urls.at(0) || '';
|
||||
console.log(this.task?.result?.media);
|
||||
console.log(this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media"));
|
||||
console.log(this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media")?.urls?.at(0));
|
||||
return this.task?.result?.media?.filter(m => m?.properties?.id == "_final_media")?.at(0)?.urls?.at(0) || '';
|
||||
},
|
||||
readbleDate() {
|
||||
if (this.task?.result?._processed_at) {
|
||||
return new Date(this.task.result._processed_at * 1e3).toISOString().slice(0, 19);
|
||||
}
|
||||
},
|
||||
taskPending() {
|
||||
return this.task.status == 'PENDING';
|
||||
},
|
||||
taskSucceeded() {
|
||||
return this.task.status == 'SUCCESS';
|
||||
},
|
||||
taskFailed() {
|
||||
return !this.taskSucceeded && !this.taskPending;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
Reference in New Issue
Block a user