mirror of
https://github.com/cupcakearmy/ora.git
synced 2024-12-22 08:06:28 +00:00
v0.3
This commit is contained in:
parent
771bb18e94
commit
aa75f42098
@ -1,3 +1,5 @@
|
|||||||
# ora
|
# ora
|
||||||
|
|
||||||
|
Work in progress. Come back soon for more 🚀
|
||||||
|
|
||||||
<div>Icons made by <a href="https://www.flaticon.com/authors/pixel-perfect" title="Pixel perfect">Pixel perfect</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
|
<div>Icons made by <a href="https://www.flaticon.com/authors/pixel-perfect" title="Pixel perfect">Pixel perfect</a> from <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a></div>
|
||||||
|
@ -2,14 +2,11 @@
|
|||||||
|
|
||||||
## Current
|
## Current
|
||||||
|
|
||||||
- Max time for website -> block.
|
|
||||||
|
|
||||||
## Backlog
|
## Backlog
|
||||||
|
|
||||||
- Dark mode support
|
- Dark mode support
|
||||||
- Options
|
- Options
|
||||||
- Dashboard
|
- Dashboard
|
||||||
- Better icon
|
|
||||||
- Add footer
|
- Add footer
|
||||||
- Build With ♥️
|
- Build With ♥️
|
||||||
- Github link
|
- Github link
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"name": "Ora",
|
"name": "Ora",
|
||||||
"version": "0.2",
|
"version": "0.3",
|
||||||
|
|
||||||
"description": "See how much time you spend on each website and set limits",
|
"description": "See how much time you spend on each website and set limits",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/cupcakearmy/ora",
|
"homepage": "https://github.com/cupcakearmy/ora",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -rf dist .cache",
|
"clean": "rm -rf dist .cache web-ext-artifacts node_modules",
|
||||||
"dev": "parcel watch --no-hmr manifest.json src/dashboard/index.html",
|
"dev": "parcel watch --no-hmr manifest.json src/dashboard/index.html",
|
||||||
"build": "parcel build --no-content-hash --no-source-maps --no-minify manifest.json src/dashboard/index.html",
|
"build": "parcel build --no-content-hash --no-source-maps --no-minify manifest.json src/dashboard/index.html",
|
||||||
"dist": "rm -rf dist && yarn run build && web-ext build -s dist --overwrite-dest"
|
"dist": "rm -rf dist && yarn run build && web-ext build -s dist --overwrite-dest"
|
||||||
@ -23,6 +23,8 @@
|
|||||||
"dayjs": "^1.8.36",
|
"dayjs": "^1.8.36",
|
||||||
"dexie": "^3.0.2",
|
"dexie": "^3.0.2",
|
||||||
"faker": "^5.1.0",
|
"faker": "^5.1.0",
|
||||||
|
"file-saver": "^2.0.2",
|
||||||
|
"joi": "^17.2.1",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"spectre.css": "^0.5.9",
|
"spectre.css": "^0.5.9",
|
||||||
"svelte-spa-router": "^2.2.0",
|
"svelte-spa-router": "^2.2.0",
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
|
import dayjs from 'dayjs'
|
||||||
|
|
||||||
import { dashboard } from '../shared/utils'
|
import { dashboard } from '../shared/utils'
|
||||||
import { insertLog, normalizeTimestamp, Limits } from '../shared/db'
|
import { insertLog, normalizeTimestamp, Limits, DB } from '../shared/db'
|
||||||
import { getUsageForHost, percentagesToBool } from '../shared/lib'
|
import { getUsageForHost, percentagesToBool } from '../shared/lib'
|
||||||
|
|
||||||
browser.browserAction.onClicked.addListener(() => browser.tabs.create({ url: dashboard, active: true }))
|
browser.browserAction.onClicked.addListener(() => browser.tabs.create({ url: dashboard, active: true }))
|
||||||
|
|
||||||
const frequency = 1000
|
const frequency = 1000
|
||||||
|
|
||||||
async function getAllTabs() {
|
async function log() {
|
||||||
try {
|
try {
|
||||||
const tabs = await browser.tabs.query({})
|
const tabs = await browser.tabs.query({})
|
||||||
const windows = await browser.windows.getAll()
|
const windows = await browser.windows.getAll()
|
||||||
@ -35,9 +36,16 @@ async function getAllTabs() {
|
|||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
async function deleteOldLogs() {
|
||||||
getAllTabs()
|
const { retention } = await browser.storage.local.get()
|
||||||
}, frequency)
|
const maxAge = dayjs().startOf('day').subtract(retention, 'days').toDate()
|
||||||
|
const toDelete = await DB.logs.where('timestamp').below(maxAge).toArray()
|
||||||
|
const ids = toDelete.map((log) => log.id)
|
||||||
|
await DB.logs.bulkDelete(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(deleteOldLogs, 5 * 60 * 1000) // Delete old logs every 5 minutes
|
||||||
|
setInterval(log, frequency)
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
return getUsageForHost(message).then((percentages) => percentagesToBool(percentages))
|
return getUsageForHost(message).then((percentages) => percentagesToBool(percentages))
|
||||||
|
@ -5,6 +5,8 @@
|
|||||||
import Dashboard from './pages/Dashboard.svelte'
|
import Dashboard from './pages/Dashboard.svelte'
|
||||||
import Limits from './pages/Limits.svelte'
|
import Limits from './pages/Limits.svelte'
|
||||||
|
|
||||||
|
import { isDev } from '../shared/utils'
|
||||||
|
|
||||||
const routes = {
|
const routes = {
|
||||||
'/': Dashboard,
|
'/': Dashboard,
|
||||||
|
|
||||||
@ -21,7 +23,9 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<Dev />
|
{#if isDev}
|
||||||
|
<Dev />
|
||||||
|
{/if}
|
||||||
<main>
|
<main>
|
||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<a href="../options/index.html"><button class="btn">Options</button></a>
|
<a href="../options/index.html"><button class="btn">Options</button></a>
|
||||||
|
@ -1,38 +1,24 @@
|
|||||||
<script>
|
<script>
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
import { map, min, max } from 'lodash'
|
import { map, max } from 'lodash'
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
|
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
export let data = [
|
export let data = []
|
||||||
// { lang: 'ts', popularity: 10 },
|
|
||||||
// { lang: 'js', popularity: 7 },
|
|
||||||
// { lang: 'py', popularity: 9 },
|
|
||||||
// { lang: 'rs', popularity: 8 },
|
|
||||||
|
|
||||||
// { year: 2018, value: 8 },
|
function render() {
|
||||||
// { year: 2019, value: 9 },
|
if (!wrapper) return
|
||||||
// { year: 2020, value: 3 },
|
|
||||||
|
|
||||||
{ cat: 'Phillip', value: 10 },
|
|
||||||
{ cat: 'Rita', value: 12 },
|
|
||||||
{ cat: 'Tom', value: 20 },
|
|
||||||
{ cat: 'Oscar', value: 19 },
|
|
||||||
{ cat: 'Lulu', value: 8 },
|
|
||||||
{ cat: 'Keko', value: 14 },
|
|
||||||
{ cat: 'Lena', value: 9 },
|
|
||||||
]
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
// Dynamic left padding depending on the labels
|
// Dynamic left padding depending on the labels
|
||||||
const longestKey = max(map(data, (d) => d.name.length))
|
const longestKey = max(map(data, (d) => d.name.length))
|
||||||
const mt = Math.min(longestKey * 6, 120)
|
const baseMargin = 35
|
||||||
const margin = { left: mt, top: 50, bottom: 50, right: 50 }
|
const ml = Math.min(longestKey * 6.3, 120) + baseMargin
|
||||||
|
const margin = { left: ml, top: baseMargin, bottom: baseMargin, right: baseMargin }
|
||||||
const styles = window.getComputedStyle(wrapper)
|
const styles = window.getComputedStyle(wrapper)
|
||||||
const barHeight = 20
|
const barHeight = 20
|
||||||
const width = parseInt(styles.width)
|
const width = parseInt(styles.width)
|
||||||
const height = Math.ceil(data.length * 1.5 * barHeight)
|
const height = Math.ceil(data.length * 1.25 * barHeight + (margin.top + margin.bottom))
|
||||||
|
|
||||||
const svg = d3.select(wrapper).attr('viewBox', [0, 0, width, height])
|
const svg = d3.select(wrapper).attr('viewBox', [0, 0, width, height])
|
||||||
|
|
||||||
@ -64,7 +50,7 @@
|
|||||||
// Bars
|
// Bars
|
||||||
svg
|
svg
|
||||||
.append('g')
|
.append('g')
|
||||||
.attr('fill', 'steelblue')
|
.attr('fill', '#17bbac')
|
||||||
.selectAll('rect')
|
.selectAll('rect')
|
||||||
.data(data)
|
.data(data)
|
||||||
.join('rect')
|
.join('rect')
|
||||||
@ -89,7 +75,7 @@
|
|||||||
.text((d) => d.human)
|
.text((d) => d.human)
|
||||||
.call((text) =>
|
.call((text) =>
|
||||||
text
|
text
|
||||||
.filter((d) => x(d.value) - x(0) < d.human.length * 7) // short bars
|
.filter((d) => x(d.value) - x(0) < d.human.length * 8) // short bars
|
||||||
.attr('dx', +4)
|
.attr('dx', +4)
|
||||||
.attr('fill', 'black')
|
.attr('fill', 'black')
|
||||||
.attr('text-anchor', 'start')
|
.attr('text-anchor', 'start')
|
||||||
@ -98,13 +84,18 @@
|
|||||||
svg.append('g').call(xAxis)
|
svg.append('g').call(xAxis)
|
||||||
|
|
||||||
svg.append('g').call(yAxis)
|
svg.append('g').call(yAxis)
|
||||||
})
|
}
|
||||||
|
|
||||||
|
onMount(render)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
svg {
|
svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* height: 25em; */
|
}
|
||||||
|
|
||||||
|
svg :global(*) {
|
||||||
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
@ -3,14 +3,14 @@
|
|||||||
import day from 'dayjs'
|
import day from 'dayjs'
|
||||||
import { range, random } from 'lodash'
|
import { range, random } from 'lodash'
|
||||||
|
|
||||||
import { insertLog, normalizeTimestamp, DB } from '../../shared/db'
|
import { insertLog, normalizeTimestamp, DB, clear as clearDB } from '../../shared/db'
|
||||||
|
|
||||||
let loading = false
|
let loading = false
|
||||||
|
|
||||||
async function fill() {
|
async function fill() {
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = true
|
||||||
const start = day().subtract('7', 'days').valueOf()
|
const start = day().subtract(2, 'weeks').valueOf()
|
||||||
const end = Date.now()
|
const end = Date.now()
|
||||||
for (const n of range(20)) {
|
for (const n of range(20)) {
|
||||||
const host = faker.internet.domainName()
|
const host = faker.internet.domainName()
|
||||||
@ -29,8 +29,7 @@
|
|||||||
async function clear() {
|
async function clear() {
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = true
|
||||||
await DB.limits.clear()
|
await clearDB()
|
||||||
await DB.logs.clear()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,6 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<!-- <div class="flex flex-col"> -->
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button class="btn btn-sm" on:click={all}>All</button>
|
<button class="btn btn-sm" on:click={all}>All</button>
|
||||||
|
@ -7,10 +7,11 @@
|
|||||||
import { data, countInGroup } from '../../shared/lib'
|
import { data, countInGroup } from '../../shared/lib'
|
||||||
|
|
||||||
let top = 15
|
let top = 15
|
||||||
let full = 50
|
let full = 100
|
||||||
|
|
||||||
let loading = true
|
let loading = true
|
||||||
let counted = []
|
let counted = []
|
||||||
|
let table = []
|
||||||
let timeout
|
let timeout
|
||||||
|
|
||||||
let start
|
let start
|
||||||
@ -37,13 +38,37 @@
|
|||||||
timeout = setTimeout(calculate, 5)
|
timeout = setTimeout(calculate, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
let lastHuman = null
|
||||||
|
table = counted.map((entry) => {
|
||||||
|
const same = lastHuman === entry.human
|
||||||
|
if (!same) lastHuman = entry.human
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
same,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
onMount(calculate)
|
onMount(calculate)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
table td,
|
||||||
|
table th {
|
||||||
|
padding: 0.25rem 0.06rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td.same {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td :global(a:visited) {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="flex justify-between items-center mb-4">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<h2 class="text-2xl">Dashboard</h2>
|
<h2 class="text-2xl">Dashboard</h2>
|
||||||
<RangeChooser bind:start bind:end />
|
<RangeChooser bind:start bind:end />
|
||||||
</div>
|
</div>
|
||||||
@ -52,16 +77,16 @@
|
|||||||
{:else if counted}
|
{:else if counted}
|
||||||
<h2 class="text-lg">Top {top}</h2>
|
<h2 class="text-lg">Top {top}</h2>
|
||||||
<Chart data={topData} />
|
<Chart data={topData} />
|
||||||
<h2 class="text-lg mt-4">Top {full}</h2>
|
<h2 class="text-lg mt-4 mb-2">Top {full}</h2>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Time Spent</th>
|
<th>Time Spent</th>
|
||||||
<th>Host</th>
|
<th>Host</th>
|
||||||
</tr>
|
</tr>
|
||||||
{#each counted.slice(0, 100) as { host, total, human }}
|
{#each table.slice(0, full) as { host, total, human, same }}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{human}</td>
|
<td class:same>{human}</td>
|
||||||
<td class="link"><a href={'https://' + host}>{host}</a></td>
|
<td class="link"><a target="_blank" rel="noreferrer" href={'https://' + host}>{host}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{/each}
|
{/each}
|
||||||
</table>
|
</table>
|
||||||
|
@ -1,43 +1,85 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte'
|
import { onMount } from 'svelte'
|
||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
|
import { saveAs } from 'file-saver'
|
||||||
|
import dj from 'dayjs'
|
||||||
|
|
||||||
import { dashboard } from '../shared/utils'
|
import FileUpload from './FileUpload.svelte'
|
||||||
|
|
||||||
|
import { dashboard, isDev } from '../shared/utils'
|
||||||
|
import { dump as dumpDB, load as loadDB, clear as clearDB, validate } from '../shared/db'
|
||||||
|
import { longPress } from '../shared/lib'
|
||||||
|
|
||||||
const DEFAULT = {
|
const DEFAULT = {
|
||||||
retention: 90,
|
retention: 90,
|
||||||
}
|
}
|
||||||
|
|
||||||
let settings = DEFAULT
|
let settings = DEFAULT
|
||||||
|
let uploaded
|
||||||
|
let disabled = true
|
||||||
|
|
||||||
onMount(async () => {
|
async function read() {
|
||||||
load()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function load() {
|
|
||||||
settings = {
|
settings = {
|
||||||
...DEFAULT,
|
...DEFAULT,
|
||||||
...(await browser.storage.local.get()),
|
...(await browser.storage.local.get()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function save() {
|
function write() {
|
||||||
return browser.storage.local.set(settings)
|
return browser.storage.local.set(settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
await browser.storage.local.clear()
|
await browser.storage.local.clear()
|
||||||
await load()
|
await read()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function dump() {
|
||||||
|
const data = await dumpDB()
|
||||||
|
const blob = new Blob([JSON.stringify(data)], { type: 'application/json;charset=utf-8' })
|
||||||
|
const filename = `Ora [${dj().format('YYYY-MM-DD HH-mm-ss')}].json`
|
||||||
|
saveAs(blob, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clear() {
|
||||||
|
await clearDB()
|
||||||
|
alert('Done')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
try {
|
||||||
|
await loadDB(uploaded)
|
||||||
|
alert('Imported')
|
||||||
|
} catch {
|
||||||
|
alert('Error importing')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
disabled = !validate(uploaded)
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(read)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="p-4">
|
<style>
|
||||||
<a href={dashboard} target="_blank"><button class="btn btn-primary btn-lg">Dashboard</button></a>
|
main {
|
||||||
|
padding: 1em;
|
||||||
|
margin: auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 50em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<form class="max-w-sm mt-5" on:submit|preventDefault={save}>
|
<main>
|
||||||
|
<a href={dashboard} target={isDev ? '' : '_blank'}><button class="btn">Dashboard</button></a>
|
||||||
|
|
||||||
|
<h2 class="mt-8 text-2xl">Settings</h2>
|
||||||
|
<form class="mt-2" on:submit|preventDefault={write}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
Retention <small>(Days)</small>
|
Retention
|
||||||
|
<small>(Days)</small>
|
||||||
<input
|
<input
|
||||||
id="retention"
|
id="retention"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
@ -47,8 +89,18 @@
|
|||||||
step="1"
|
step="1"
|
||||||
bind:value={settings.retention} />
|
bind:value={settings.retention} />
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
<button type="reset" class="btn" on:click={reset}>Reset</button>
|
<button type="reset" class="btn" on:click={reset}>Reset</button>
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<h2 class="mt-8 text-2xl">Your Data</h2>
|
||||||
|
<div class="mt-2">
|
||||||
|
<FileUpload bind:value={uploaded} />
|
||||||
|
<button class="btn btn-primary" on:click={load} {disabled}>Import</button>
|
||||||
|
<button class="btn btn-primary" on:click={dump}>Export</button>
|
||||||
|
<button class="btn btn-error tooltip" data-tooltip="Hold to delete" use:longPress={clear}>Delete all data</button>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
47
src/options/FileUpload.svelte
Normal file
47
src/options/FileUpload.svelte
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<script>
|
||||||
|
import { isEqual } from 'lodash'
|
||||||
|
|
||||||
|
let text = 'Select File'
|
||||||
|
|
||||||
|
export let value = undefined
|
||||||
|
|
||||||
|
let input
|
||||||
|
let error
|
||||||
|
|
||||||
|
function validate() {
|
||||||
|
if (!input || !input.files.length) return
|
||||||
|
|
||||||
|
const file = input.files[0]
|
||||||
|
text = file.name
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = (data) => {
|
||||||
|
try {
|
||||||
|
error = false
|
||||||
|
const text = data.target.result
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
value = parsed
|
||||||
|
} catch {
|
||||||
|
error = true
|
||||||
|
value = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.readAsText(file)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
label {
|
||||||
|
width: 18em;
|
||||||
|
max-width: 100%;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
label input[type='file'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<label class="btn">
|
||||||
|
{#if error}Invalid file{:else}{text}{/if}
|
||||||
|
<input bind:this={input} on:change={validate} class="input" accept="application/json" type="file" />
|
||||||
|
</label>
|
@ -2,6 +2,7 @@ import dj from 'dayjs'
|
|||||||
import Dexie from 'dexie'
|
import Dexie from 'dexie'
|
||||||
import RelativeTime from 'dayjs/plugin/relativeTime'
|
import RelativeTime from 'dayjs/plugin/relativeTime'
|
||||||
import Duration from 'dayjs/plugin/duration'
|
import Duration from 'dayjs/plugin/duration'
|
||||||
|
import Joi from 'joi'
|
||||||
|
|
||||||
dj.extend(Duration)
|
dj.extend(Duration)
|
||||||
dj.extend(RelativeTime)
|
dj.extend(RelativeTime)
|
||||||
@ -29,3 +30,56 @@ export async function insertLog({ timestamp, host, seconds }) {
|
|||||||
data.seconds += seconds
|
data.seconds += seconds
|
||||||
await DB.logs.put(data)
|
await DB.logs.put(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function clear() {
|
||||||
|
await DB.limits.clear()
|
||||||
|
await DB.logs.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function dump() {
|
||||||
|
return {
|
||||||
|
limits: await DB.limits.toArray(),
|
||||||
|
logs: await DB.logs.toArray(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validate(data) {
|
||||||
|
const schema = Joi.object({
|
||||||
|
limits: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
host: Joi.string(),
|
||||||
|
id: Joi.number(),
|
||||||
|
rules: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
limit: Joi.array().items(Joi.string(), Joi.number()),
|
||||||
|
every: Joi.array().items(Joi.string(), Joi.number()),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
logs: Joi.array().items(
|
||||||
|
Joi.object({
|
||||||
|
host: Joi.string(),
|
||||||
|
id: Joi.number(),
|
||||||
|
seconds: Joi.number(),
|
||||||
|
timestamp: Joi.string(),
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const validated = schema.validate(data, { presence: 'required' })
|
||||||
|
return !validated.error
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load(data) {
|
||||||
|
if (!validate(data)) throw new Error('Invalid data')
|
||||||
|
|
||||||
|
await clear()
|
||||||
|
await DB.limits.bulkAdd(data.limits)
|
||||||
|
await DB.logs.bulkAdd(
|
||||||
|
data.logs.map((log) => ({
|
||||||
|
...log,
|
||||||
|
timestamp: new Date(log.timestamp),
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link href="../../node_modules/spectre.css/dist/spectre.min.css" rel="stylesheet" />
|
<link href="../../node_modules/spectre.css/dist/spectre.min.css" rel="stylesheet" />
|
||||||
<link href="../../node_modules/tailwindcss/dist/tailwind.css" rel="stylesheet" />
|
<link href="../../node_modules/tailwindcss/dist/tailwind.min.css" rel="stylesheet" />
|
||||||
<style>
|
<style>
|
||||||
#root {
|
#root {
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
import browser from 'webextension-polyfill'
|
import browser from 'webextension-polyfill'
|
||||||
|
|
||||||
export const dashboard = browser.runtime.getURL('./src/dashboard/index.html')
|
export const dashboard = browser.runtime.getURL('./src/dashboard/index.html')
|
||||||
|
|
||||||
|
export const isDev = process.env.NODE_ENV !== 'production'
|
45
yarn.lock
45
yarn.lock
@ -950,6 +950,35 @@
|
|||||||
postcss "7.0.32"
|
postcss "7.0.32"
|
||||||
purgecss "^2.3.0"
|
purgecss "^2.3.0"
|
||||||
|
|
||||||
|
"@hapi/address@^4.1.0":
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-4.1.0.tgz#d60c5c0d930e77456fdcde2598e77302e2955e1d"
|
||||||
|
integrity sha512-SkszZf13HVgGmChdHo/PxchnSaCJ6cetVqLzyciudzZRT0jcOouIF/Q93mgjw8cce+D+4F4C1Z/WrfFN+O3VHQ==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
|
"@hapi/formula@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/formula/-/formula-2.0.0.tgz#edade0619ed58c8e4f164f233cda70211e787128"
|
||||||
|
integrity sha512-V87P8fv7PI0LH7LiVi8Lkf3x+KCO7pQozXRssAHNXXL9L1K+uyu4XypLXwxqVDKgyQai6qj3/KteNlrqDx4W5A==
|
||||||
|
|
||||||
|
"@hapi/hoek@^9.0.0":
|
||||||
|
version "9.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.1.0.tgz#6c9eafc78c1529248f8f4d92b0799a712b6052c6"
|
||||||
|
integrity sha512-i9YbZPN3QgfighY/1X1Pu118VUz2Fmmhd6b2n0/O8YVgGGfw0FbUYoA97k7FkpGJ+pLCFEDLUmAPPV4D1kpeFw==
|
||||||
|
|
||||||
|
"@hapi/pinpoint@^2.0.0":
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/pinpoint/-/pinpoint-2.0.0.tgz#805b40d4dbec04fc116a73089494e00f073de8df"
|
||||||
|
integrity sha512-vzXR5MY7n4XeIvLpfl3HtE3coZYO4raKXW766R6DZw/6aLqR26iuZ109K7a0NtF2Db0jxqh7xz2AxkUwpUFybw==
|
||||||
|
|
||||||
|
"@hapi/topo@^5.0.0":
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@hapi/topo/-/topo-5.0.0.tgz#c19af8577fa393a06e9c77b60995af959be721e7"
|
||||||
|
integrity sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
|
||||||
"@iarna/toml@^2.2.0":
|
"@iarna/toml@^2.2.0":
|
||||||
version "2.2.5"
|
version "2.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
|
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
|
||||||
@ -3547,6 +3576,11 @@ file-entry-cache@^5.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
flat-cache "^2.0.1"
|
flat-cache "^2.0.1"
|
||||||
|
|
||||||
|
file-saver@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a"
|
||||||
|
integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
file-uri-to-path@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||||
@ -4541,6 +4575,17 @@ jetpack-id@1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/jetpack-id/-/jetpack-id-1.0.0.tgz#2cf9fbae46d8074fc16b7de0071c8efebca473a6"
|
resolved "https://registry.yarnpkg.com/jetpack-id/-/jetpack-id-1.0.0.tgz#2cf9fbae46d8074fc16b7de0071c8efebca473a6"
|
||||||
integrity sha1-LPn7rkbYB0/Ba33gBxyO/rykc6Y=
|
integrity sha1-LPn7rkbYB0/Ba33gBxyO/rykc6Y=
|
||||||
|
|
||||||
|
joi@^17.2.1:
|
||||||
|
version "17.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/joi/-/joi-17.2.1.tgz#e5140fdf07e8fecf9bc977c2832d1bdb1e3f2a0a"
|
||||||
|
integrity sha512-YT3/4Ln+5YRpacdmfEfrrKh50/kkgX3LgBltjqnlMPIYiZ4hxXZuVJcxmsvxsdeHg9soZfE3qXxHC2tMpCCBOA==
|
||||||
|
dependencies:
|
||||||
|
"@hapi/address" "^4.1.0"
|
||||||
|
"@hapi/formula" "^2.0.0"
|
||||||
|
"@hapi/hoek" "^9.0.0"
|
||||||
|
"@hapi/pinpoint" "^2.0.0"
|
||||||
|
"@hapi/topo" "^5.0.0"
|
||||||
|
|
||||||
js-select@~0.6.0:
|
js-select@~0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-select/-/js-select-0.6.0.tgz#c284e22824d5927aec962dcdf247174aefb0d190"
|
resolved "https://registry.yarnpkg.com/js-select/-/js-select-0.6.0.tgz#c284e22824d5927aec962dcdf247174aefb0d190"
|
||||||
|
Loading…
Reference in New Issue
Block a user