mirror of
https://github.com/tjhorner/archivebox-exporter.git
synced 2023-06-17 01:17:54 +03:00
Initial commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
node_modules
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 TJ Horner
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# 🗃 ArchiveBox Exporter Browser Extension
|
||||
|
||||
This is a browser extension (works in Chrome, Firefox, and Chrome-like browsers) that lets you automatically send pages from domains you specify to your ArchiveBox instance. This has a couple of benefits:
|
||||
|
||||
- You have a fulltext search of your browsing history ready at your fingertips
|
||||
- Prevent link rot for important information!
|
||||
- Access important information even if you're offline
|
||||
|
||||
**Warning:** This extension is not yet complete! I am waiting for ArchiveBox to implement an API that can accept URLs to archive. But at the moment, everything else is ready! You can check the status in [this GitHub issue](https://github.com/ArchiveBox/ArchiveBox/issues/577#issuecomment-870974915).
|
||||
|
||||
## Features
|
||||
|
||||
- Different archive modes
|
||||
- Allowlist mode doesn't archive pages by default, and lets you specify domains or regexes to archive
|
||||
- Blocklist mode archives all visited pages by default, but lets you specify domains or regexes to not archive
|
||||
- Archive any arbitrary page with the "Archive Current Page" context menu item
|
||||
- Archive any link with the "Archive Link" context menu item
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
3923
package-lock.json
generated
Normal file
3923
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
package.json
Normal file
30
package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "archivebox-exporter",
|
||||
"version": "1.0.0",
|
||||
"description": "Automatically or manually send pages to your ArchiveBox for archival.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"dev": "NODE_ENV=development webpack --watch"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@tsconfig/svelte": "^2.0.1",
|
||||
"@types/chrome": "^0.0.145",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"clean-webpack-plugin": "^4.0.0-alpha.0",
|
||||
"copy-webpack-plugin": "^9.0.1",
|
||||
"svelte": "^3.38.3",
|
||||
"svelte-loader": "^3.1.2",
|
||||
"ts-loader": "^9.2.3",
|
||||
"typescript": "^4.3.4",
|
||||
"webpack": "^5.41.1",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"uuid": "^8.3.2",
|
||||
"wildcard-domain-matcher": "^1.0.0"
|
||||
}
|
||||
}
|
||||
34
src/action/App.svelte
Normal file
34
src/action/App.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
import DomainLists from "./components/DomainLists.svelte"
|
||||
import Config from "./components/Config.svelte"
|
||||
|
||||
let archiveMode
|
||||
|
||||
function onArchiveModeChanged(newMode) {
|
||||
archiveMode = newMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="main">
|
||||
<Config {onArchiveModeChanged} />
|
||||
<hr>
|
||||
<DomainLists {archiveMode} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main {
|
||||
width: 400px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
color: #2d2d2d;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 10px 0;
|
||||
border: none;
|
||||
border-top: 1px solid lightgray;
|
||||
}
|
||||
</style>
|
||||
51
src/action/components/Config.svelte
Normal file
51
src/action/components/Config.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
export let onArchiveModeChanged
|
||||
|
||||
import { GlobalConfigKey } from "../../common/interfaces/config"
|
||||
import SyncedConfig from "../../common/services/config"
|
||||
import ChromeSyncStorage from "../../common/services/storage"
|
||||
|
||||
const config = new SyncedConfig(new ChromeSyncStorage())
|
||||
|
||||
let selectedArchiveMode
|
||||
|
||||
async function changeArchiveMode() {
|
||||
await config.set(GlobalConfigKey.ArchiveMode, selectedArchiveMode)
|
||||
onArchiveModeChanged(selectedArchiveMode)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
selectedArchiveMode = await config.get(GlobalConfigKey.ArchiveMode, "allowlist")
|
||||
onArchiveModeChanged(selectedArchiveMode)
|
||||
}
|
||||
|
||||
load()
|
||||
</script>
|
||||
|
||||
<div class="config">
|
||||
<h2>Config</h2>
|
||||
|
||||
<div>
|
||||
<label for={GlobalConfigKey.ArchiveMode}>Archive Mode</label><br>
|
||||
<!-- svelte-ignore a11y-no-onchange -->
|
||||
<select id={GlobalConfigKey.ArchiveMode} bind:value={selectedArchiveMode} on:change={changeArchiveMode}>
|
||||
<option value="allowlist">Allowlist (don't archive by default)</option>
|
||||
<option value="blocklist">Blocklist (archive by default)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
select {
|
||||
margin-top: 3px;
|
||||
}
|
||||
</style>
|
||||
72
src/action/components/DomainList.svelte
Normal file
72
src/action/components/DomainList.svelte
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts">
|
||||
export let options = [ ]
|
||||
export let onAdded
|
||||
export let onRemoved
|
||||
export let onCleared
|
||||
export let id
|
||||
|
||||
let isRegex
|
||||
let domainInput = ""
|
||||
let selectedEntries
|
||||
|
||||
function addDomain() {
|
||||
if (domainInput.trim() === "") return
|
||||
onAdded(domainInput, isRegex)
|
||||
domainInput = ""
|
||||
}
|
||||
|
||||
function removeDomain() {
|
||||
onRemoved(selectedEntries)
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (e.code === "Enter") addDomain()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="list">
|
||||
<div class="add-remove">
|
||||
<input type="checkbox" id="isRegex_{id}" bind:checked={isRegex}> <label for="isRegex_{id}">Regex?</label>
|
||||
<input
|
||||
type="text"
|
||||
class="domain-input"
|
||||
placeholder="{isRegex ? "Regex" : "Domain"} to add"
|
||||
bind:value={domainInput}
|
||||
on:keydown={handleKeydown}>
|
||||
<button on:click={addDomain}>+</button>
|
||||
<button on:click={removeDomain}>-</button>
|
||||
<button on:click={onCleared}>Clear</button>
|
||||
</div>
|
||||
|
||||
<select multiple bind:value={selectedEntries}>
|
||||
{#each options as { id, value, type } }
|
||||
<option value="{id}">{type === "regex" ? "[regex] " : ""}{value}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
select {
|
||||
width: 100%;
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.add-remove {
|
||||
margin-bottom: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.domain-input {
|
||||
flex-grow: 1;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
button:not(:last-of-type) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
96
src/action/components/DomainLists.svelte
Normal file
96
src/action/components/DomainLists.svelte
Normal file
@@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
export let archiveMode
|
||||
|
||||
import { v4 } from "uuid"
|
||||
import DomainList from "./DomainList.svelte"
|
||||
import DomainListStorage from "../../common/services/domainList"
|
||||
import { ListType } from "../../common/interfaces/domainList"
|
||||
import ChromeSyncStorage from "../../common/services/storage"
|
||||
|
||||
const storage = new DomainListStorage(new ChromeSyncStorage())
|
||||
|
||||
let allowlist = [ ]
|
||||
let blocklist = [ ]
|
||||
|
||||
async function load() {
|
||||
allowlist = await storage.getList(ListType.Allowlist)
|
||||
blocklist = await storage.getList(ListType.Blocklist)
|
||||
}
|
||||
|
||||
function onAdded(listType) {
|
||||
return async function(domain, isRegex) {
|
||||
await storage.addEntry({
|
||||
id: v4(),
|
||||
type: isRegex? "regex" : "domain",
|
||||
value: domain
|
||||
}, listType)
|
||||
|
||||
await load()
|
||||
}
|
||||
}
|
||||
|
||||
function onRemoved(listType) {
|
||||
return async function(ids) {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await storage.removeEntry(ids[i], listType)
|
||||
}
|
||||
|
||||
await load()
|
||||
}
|
||||
}
|
||||
|
||||
function onCleared(listType) {
|
||||
return async function() {
|
||||
await storage.clearEntries(listType)
|
||||
await load()
|
||||
}
|
||||
}
|
||||
|
||||
load()
|
||||
</script>
|
||||
|
||||
<div class="lists">
|
||||
<div class="list" class:hidden="{archiveMode !== 'allowlist'}">
|
||||
<h2>Archived Domains</h2>
|
||||
<p>
|
||||
Pages you visit on these domains will be sent to your ArchiveBox.
|
||||
Wildcard subdomains are allowed, e.g. <code>*.google.com</code>
|
||||
</p>
|
||||
|
||||
<DomainList
|
||||
id="allowlist"
|
||||
options={allowlist}
|
||||
onAdded={onAdded(ListType.Allowlist)}
|
||||
onRemoved={onRemoved(ListType.Allowlist)}
|
||||
onCleared={onCleared(ListType.Allowlist)} />
|
||||
</div>
|
||||
|
||||
<div class="list" class:hidden="{archiveMode !== 'blocklist'}">
|
||||
<h2>Ignored Domains</h2>
|
||||
<p>
|
||||
Every page you visit <strong>except on these domains</strong> will be sent to your ArchiveBox.
|
||||
Wildcard subdomains are allowed, e.g. <code>*.google.com</code>
|
||||
</p>
|
||||
|
||||
<DomainList
|
||||
id="blocklist"
|
||||
options={blocklist}
|
||||
onAdded={onAdded(ListType.Blocklist)}
|
||||
onRemoved={onRemoved(ListType.Blocklist)}
|
||||
onCleared={onCleared(ListType.Blocklist)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
7
src/action/index.ts
Normal file
7
src/action/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import App from "./App.svelte"
|
||||
|
||||
const app = new App({
|
||||
target: document.body
|
||||
})
|
||||
|
||||
export default app
|
||||
51
src/background/index.ts
Normal file
51
src/background/index.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import ArchiveBoxArchiver from "../common/services/archiver"
|
||||
import SyncedConfig from "../common/services/config"
|
||||
import ChromeSyncStorage from "../common/services/storage"
|
||||
import DomainList from "../common/services/domainList"
|
||||
|
||||
async function main() {
|
||||
const storage = new ChromeSyncStorage()
|
||||
const config = new SyncedConfig(storage)
|
||||
const domainList = new DomainList(storage)
|
||||
|
||||
const archiver = new ArchiveBoxArchiver(domainList, config)
|
||||
|
||||
chrome.history.onVisited.addListener(async historyItem => {
|
||||
const shouldArchive = await archiver.shouldArchive(historyItem.url)
|
||||
if (!shouldArchive) return
|
||||
|
||||
await archiver.queueForArchival(historyItem.url)
|
||||
})
|
||||
|
||||
await chrome.alarms.clearAll()
|
||||
|
||||
chrome.alarms.create({
|
||||
periodInMinutes: 15
|
||||
})
|
||||
|
||||
chrome.alarms.onAlarm.addListener(async () => {
|
||||
await archiver.submitQueue()
|
||||
})
|
||||
|
||||
chrome.contextMenus.create({
|
||||
id: "archivePage",
|
||||
title: "Archive Current Page",
|
||||
contexts: [ "all" ],
|
||||
onclick: async (info) => {
|
||||
await archiver.archiveImmediately(info.pageUrl)
|
||||
}
|
||||
})
|
||||
|
||||
chrome.contextMenus.create({
|
||||
id: "archiveLink",
|
||||
title: "Archive Link",
|
||||
contexts: [ "link" ],
|
||||
onclick: async (info) => {
|
||||
await archiver.archiveImmediately(info.linkUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
export default main
|
||||
6
src/common/interfaces/archiver.ts
Normal file
6
src/common/interfaces/archiver.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export default interface IArchiver {
|
||||
shouldArchive(url: string): Promise<boolean>
|
||||
queueForArchival(url: string): Promise<void>
|
||||
submitQueue(): Promise<void>
|
||||
archiveImmediately(url: string): Promise<void>
|
||||
}
|
||||
8
src/common/interfaces/config.ts
Normal file
8
src/common/interfaces/config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export enum GlobalConfigKey {
|
||||
ArchiveMode = "archiveMode"
|
||||
}
|
||||
|
||||
export default interface IConfig {
|
||||
get<T>(key: string, defaultValue: T): Promise<T>
|
||||
set<T>(key: string, value: T): Promise<void>
|
||||
}
|
||||
18
src/common/interfaces/domainList.ts
Normal file
18
src/common/interfaces/domainList.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface DomainEntry {
|
||||
id: string
|
||||
type: "domain" | "regex"
|
||||
value: string
|
||||
}
|
||||
|
||||
export enum ListType {
|
||||
Allowlist = "allowlist",
|
||||
Blocklist = "blocklist"
|
||||
}
|
||||
|
||||
export default interface IDomainList {
|
||||
urlMatchesList(url: string, list: ListType): Promise<boolean>
|
||||
getList(list: ListType): Promise<DomainEntry[]>
|
||||
addEntry(entry: DomainEntry, list: ListType): Promise<void>
|
||||
removeEntry(entryId: string, list: ListType): Promise<void>
|
||||
clearEntries(list: ListType): Promise<void>
|
||||
}
|
||||
5
src/common/interfaces/storage.ts
Normal file
5
src/common/interfaces/storage.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export default interface IStorage {
|
||||
get<T>(key: string, defaultValue: T): Promise<T>
|
||||
set<T>(key: string, value: T): Promise<void>
|
||||
remove(key: string): Promise<void>
|
||||
}
|
||||
45
src/common/services/archiver.ts
Normal file
45
src/common/services/archiver.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import IArchiver from "../interfaces/archiver"
|
||||
import IDomainList, { ListType } from "../interfaces/domainList"
|
||||
import IConfig, { GlobalConfigKey } from "../interfaces/config"
|
||||
|
||||
export enum ConfigKey {
|
||||
ArchiveBoxUrl = "archiveBoxUrl",
|
||||
ArchiveBoxKey = "archiveBoxKey"
|
||||
}
|
||||
|
||||
export default class ArchiveBoxArchiver implements IArchiver {
|
||||
private domainList: IDomainList
|
||||
private config: IConfig
|
||||
|
||||
private urlQueue: string[] = [ ]
|
||||
|
||||
constructor(domainList: IDomainList, config: IConfig) {
|
||||
this.domainList = domainList
|
||||
this.config = config
|
||||
}
|
||||
|
||||
async shouldArchive(url: string): Promise<boolean> {
|
||||
const mode = await this.config.get(GlobalConfigKey.ArchiveMode, "allowlist")
|
||||
|
||||
if (mode === "allowlist")
|
||||
return await this.domainList.urlMatchesList(url, ListType.Allowlist)
|
||||
else if (mode === "blocklist")
|
||||
return !(await this.domainList.urlMatchesList(url, ListType.Blocklist))
|
||||
}
|
||||
|
||||
async queueForArchival(url: string): Promise<void> {
|
||||
if (this.urlQueue.indexOf(url) !== -1) return
|
||||
this.urlQueue.push(url)
|
||||
|
||||
console.log(url)
|
||||
}
|
||||
|
||||
async submitQueue(): Promise<void> {
|
||||
console.warn("Queue submittal not actually implemented yet!")
|
||||
this.urlQueue = [ ]
|
||||
}
|
||||
|
||||
async archiveImmediately(url: string): Promise<void> {
|
||||
console.log("Archiving url immediately:", url)
|
||||
}
|
||||
}
|
||||
22
src/common/services/config.ts
Normal file
22
src/common/services/config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import IConfig from "../interfaces/config"
|
||||
import IStorage from "../interfaces/storage"
|
||||
|
||||
export default class SyncedConfig implements IConfig {
|
||||
private storage: IStorage
|
||||
|
||||
constructor(storage: IStorage) {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
get<T>(key: string, defaultValue: T): Promise<T> {
|
||||
return this.storage.get<T>(this.keyFor(key), defaultValue)
|
||||
}
|
||||
|
||||
set<T>(key: string, value: T): Promise<void> {
|
||||
return this.storage.set(this.keyFor(key), value)
|
||||
}
|
||||
|
||||
private keyFor(key: string): string {
|
||||
return `config_${key}`
|
||||
}
|
||||
}
|
||||
59
src/common/services/domainList.ts
Normal file
59
src/common/services/domainList.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import IDomainList, { DomainEntry, ListType } from "../interfaces/domainList"
|
||||
import IStorage from "../interfaces/storage"
|
||||
import Matcher from "wildcard-domain-matcher"
|
||||
|
||||
export default class DomainList implements IDomainList {
|
||||
private storage: IStorage
|
||||
private matcher: Matcher = new Matcher()
|
||||
|
||||
constructor(storage: IStorage) {
|
||||
this.storage = storage
|
||||
}
|
||||
|
||||
async urlMatchesList(url: string, list: ListType): Promise<boolean> {
|
||||
const entries = await this.getList(list)
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i]
|
||||
if (this.urlMatchesEntry(url, entry)) return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
async getList(list: ListType): Promise<DomainEntry[]> {
|
||||
return await this.storage.get<DomainEntry[]>(this.keyFor(list), [ ])
|
||||
}
|
||||
|
||||
async addEntry(entry: DomainEntry, list: ListType): Promise<void> {
|
||||
const oldList = await this.getList(list)
|
||||
oldList.push(entry)
|
||||
await this.storage.set(this.keyFor(list), oldList)
|
||||
}
|
||||
|
||||
async removeEntry(entryId: string, list: ListType): Promise<void> {
|
||||
const oldList = await this.getList(list)
|
||||
const idx = oldList.findIndex(entry => entry.id === entryId)
|
||||
if (idx === -1) return
|
||||
|
||||
oldList.splice(idx, 1)
|
||||
await this.storage.set(this.keyFor(list), oldList)
|
||||
}
|
||||
|
||||
async clearEntries(list: ListType): Promise<void> {
|
||||
await this.storage.set(this.keyFor(list), [ ])
|
||||
}
|
||||
|
||||
private urlMatchesEntry(url: string, entry: DomainEntry): boolean {
|
||||
if (entry.type === "domain") {
|
||||
const whatwgUrl = new URL(url)
|
||||
return this.matcher.test(whatwgUrl.host.toLowerCase(), entry.value.toLowerCase())
|
||||
} else {
|
||||
const regex = new RegExp(entry.value, "i")
|
||||
return regex.test(url)
|
||||
}
|
||||
}
|
||||
|
||||
private keyFor(list: ListType): string {
|
||||
return `domainList_${list}`
|
||||
}
|
||||
}
|
||||
32
src/common/services/storage.ts
Normal file
32
src/common/services/storage.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import IStorage from "../interfaces/storage"
|
||||
|
||||
export default class ChromeSyncStorage implements IStorage {
|
||||
async get<T>(key: string, defaultValue: T): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.sync.get(key, items => {
|
||||
if (chrome.runtime.lastError) return reject(chrome.runtime.lastError)
|
||||
resolve(items[key] || defaultValue)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async set<T>(key: string, value: T): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.sync.set({
|
||||
[key]: value
|
||||
}, () => {
|
||||
if (chrome.runtime.lastError) return reject(chrome.runtime.lastError)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async remove(key: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.storage.sync.remove(key, () => {
|
||||
if (chrome.runtime.lastError) return reject(chrome.runtime.lastError)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
33
src/manifest.json
Normal file
33
src/manifest.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "ArchiveBox Exporter",
|
||||
"description": "",
|
||||
"version": "0.0.0",
|
||||
"manifest_version": 2,
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "archivebox@tjhorner.dev",
|
||||
"strict_min_version": "42.0"
|
||||
}
|
||||
},
|
||||
|
||||
"icons": {
|
||||
"128": "static/128.png"
|
||||
},
|
||||
|
||||
"permissions": [
|
||||
"history",
|
||||
"contextMenus",
|
||||
"storage",
|
||||
"alarms"
|
||||
],
|
||||
|
||||
"browser_action": {
|
||||
"default_title": "ArchiveBox",
|
||||
"default_popup": "static/action.html"
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": [ "background.js" ]
|
||||
}
|
||||
}
|
||||
BIN
static/128.png
Normal file
BIN
static/128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
11
static/action.html
Normal file
11
static/action.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script defer src="../action.js"></script>
|
||||
<title>ArchiveBox Exporter</title>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
BIN
static/archive.png
Normal file
BIN
static/archive.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/",
|
||||
"module": "esnext",
|
||||
"allowJs": true,
|
||||
"types": [ "svelte", "chrome" ],
|
||||
"lib": [ "es6", "dom" ],
|
||||
"esModuleInterop": true,
|
||||
"importsNotUsedAsValues": "remove"
|
||||
}
|
||||
}
|
||||
70
webpack.config.js
Normal file
70
webpack.config.js
Normal file
@@ -0,0 +1,70 @@
|
||||
const path = require('path')
|
||||
const CopyPlugin = require('copy-webpack-plugin')
|
||||
const { CleanWebpackPlugin: CleanPlugin } = require('clean-webpack-plugin')
|
||||
const package = require('./package.json')
|
||||
|
||||
module.exports = {
|
||||
devtool: false,
|
||||
mode: process.env.NODE_ENV || "production",
|
||||
entry: {
|
||||
background: "./src/background/index.ts",
|
||||
action: "./src/action/index.ts"
|
||||
},
|
||||
output: {
|
||||
filename: "[name].js",
|
||||
path: path.resolve(__dirname, "dist")
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: "ts-loader",
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.(html|svelte)$/,
|
||||
use: "svelte-loader"
|
||||
},
|
||||
{
|
||||
test: /node_modules\/svelte\/.*\.mjs$/,
|
||||
resolve: {
|
||||
fullySpecified: false
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
optimization: {
|
||||
usedExports: true
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
svelte: path.resolve("node_modules", "svelte")
|
||||
},
|
||||
extensions: [ ".tsx", ".ts", ".js", ".mjs", ".svelte" ],
|
||||
mainFields: [ "svelte", "browser", "module", "main" ]
|
||||
},
|
||||
plugins: [
|
||||
new CleanPlugin({
|
||||
cleanStaleWebpackAssets: false
|
||||
}),
|
||||
new CopyPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "./src/manifest.json",
|
||||
to: "manifest.json",
|
||||
transform(content) {
|
||||
const manifest = JSON.parse(content.toString())
|
||||
manifest.version = package.version
|
||||
manifest.description = package.description
|
||||
|
||||
return JSON.stringify(manifest)
|
||||
}
|
||||
},
|
||||
{
|
||||
from: "./static/**",
|
||||
to: "."
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user