new features

This commit is contained in:
msramalho
2023-02-26 20:22:37 +01:00
parent 30d21c07b7
commit 45491157f5
12 changed files with 459 additions and 144 deletions

11
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
]
}
},

View File

@@ -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;
} */

View File

@@ -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> -->

View File

@@ -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);
}

View File

@@ -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"
});

View File

@@ -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();

View File

@@ -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
View 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;
// }
// }

View File

@@ -15,7 +15,7 @@
"128": "img/ben-archiver.png"
},
"permissions": [
"storage", "tabs", "identity"
"storage", "tabs", "identity", "identity.email"
],
"host_permissions": [
],

View File

@@ -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: {

View File

@@ -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() {