mirror of
https://github.com/cupcakearmy/ora.git
synced 2025-09-06 08:10:40 +00:00
progress
This commit is contained in:
@@ -3,7 +3,7 @@ import dayjs from 'dayjs'
|
||||
|
||||
import { dashboard } from '../shared/utils'
|
||||
import { insertLog, normalizeTimestamp, DB } from '../shared/db'
|
||||
import { getUsageForHost, percentagesToBool } from '../shared/lib'
|
||||
import { getSettingsWithDefaults, getUsageForHost, percentagesToBool } from '../shared/lib'
|
||||
|
||||
browser.browserAction.onClicked.addListener(() => browser.tabs.create({ url: dashboard, active: true }))
|
||||
|
||||
@@ -18,22 +18,33 @@ async function log() {
|
||||
const window = windows.find((window) => window.id === tab.windowId)
|
||||
return tab.active && window.focused
|
||||
})
|
||||
.map(({ id, title, url }) => {
|
||||
.map(({ url, audible, mutedInfo }) => {
|
||||
const { host } = new URL(url)
|
||||
return { id, title, host }
|
||||
return { host, audio: audible && !mutedInfo.muted }
|
||||
})
|
||||
.filter((x) => x.host)
|
||||
|
||||
await Promise.all(
|
||||
active.map(({ host }) => {
|
||||
if (host)
|
||||
return insertLog({
|
||||
timestamp: normalizeTimestamp(new Date()),
|
||||
host,
|
||||
seconds: (frequency / 1000) | 0,
|
||||
})
|
||||
if (active.length === 0) return
|
||||
|
||||
const settings = await getSettingsWithDefaults()
|
||||
let idle = false
|
||||
if (settings.idleTimeout > 0) {
|
||||
idle = dayjs(settings.lastActivity).add(settings.idleTimeout, 'minutes').isBefore(dayjs())
|
||||
}
|
||||
|
||||
const inserted = active
|
||||
.filter((tab) => !idle || tab.audio)
|
||||
.map((tab) => {
|
||||
return insertLog({
|
||||
timestamp: normalizeTimestamp(new Date()),
|
||||
host: tab.host,
|
||||
seconds: (frequency / 1000) | 0,
|
||||
})
|
||||
})
|
||||
)
|
||||
} catch {}
|
||||
await Promise.all(inserted)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteOldLogs() {
|
||||
|
@@ -8,11 +8,26 @@
|
||||
height: 100vh;
|
||||
background-color: #eee;
|
||||
z-index: 999999999;
|
||||
font-size: 1rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ora--wrapper {
|
||||
background-color: #000000;
|
||||
background-color: #111;
|
||||
color: #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.ora--wrapper div {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
margin: 3rem auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ora--wrapper div h1 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
@@ -9,12 +9,6 @@ function init() {
|
||||
wrapper.classList.add('hidden')
|
||||
|
||||
const inner = window.document.createElement('div')
|
||||
Object.assign(inner.style, {
|
||||
fontFamily: `-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif`,
|
||||
margin: '3em auto',
|
||||
width: '100%',
|
||||
maxWidth: '20em',
|
||||
})
|
||||
inner.innerHTML = `
|
||||
<h1>Overtime ⏱</h1>
|
||||
<p>You have no time left on this website 🥺</p>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import Router, { link } from 'svelte-spa-router'
|
||||
|
||||
import Toasts from './components/Toasts.svelte'
|
||||
import Dev from './components/Dev.svelte'
|
||||
import Dashboard from './pages/Dashboard.svelte'
|
||||
import Limits from './pages/Limits.svelte'
|
||||
@@ -30,7 +31,7 @@
|
||||
</div>
|
||||
|
||||
<Router {routes} />
|
||||
|
||||
<Toasts />
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
|
@@ -1,35 +1,18 @@
|
||||
<script>
|
||||
let text = 'Select File'
|
||||
|
||||
export let value = undefined
|
||||
|
||||
export let file
|
||||
let input
|
||||
let error
|
||||
|
||||
function validate() {
|
||||
if (!input || !input.files.length) return
|
||||
|
||||
const file = input.files[0]
|
||||
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>
|
||||
|
||||
<label class="btn">
|
||||
{#if error}Invalid file{:else}{text}{/if}
|
||||
{text}
|
||||
<input bind:this={input} on:change={validate} class="input" accept="application/json" type="file" />
|
||||
</label>
|
||||
|
||||
|
19
src/dashboard/components/Toast.svelte
Normal file
19
src/dashboard/components/Toast.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
import { scale } from 'svelte/transition'
|
||||
|
||||
export let toast
|
||||
|
||||
let show = true
|
||||
onMount(() => {
|
||||
setTimeout(() => {
|
||||
show = false
|
||||
}, 3000)
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if show}
|
||||
<div class="toast toast-{toast.type}" transition:scale>
|
||||
{toast.message}
|
||||
</div>
|
||||
{/if}
|
24
src/dashboard/components/Toasts.svelte
Normal file
24
src/dashboard/components/Toasts.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import { toasts } from '../toasts'
|
||||
import Toast from './Toast.svelte'
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#each $toasts as toast}
|
||||
<div class="mt-2">
|
||||
<Toast {toast} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
</style>
|
7
src/dashboard/toasts.js
Normal file
7
src/dashboard/toasts.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { writable } from 'svelte/store'
|
||||
|
||||
export const toasts = writable([])
|
||||
|
||||
export function notify(message, type = 'success') {
|
||||
toasts.update((toasts) => [...toasts, { message, type }])
|
||||
}
|
@@ -2,20 +2,20 @@
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
import { DB } from '../../shared/db'
|
||||
import { SettingsValidator } from '../../shared/validation'
|
||||
import { getSettingsWithDefaults } from '../../shared/lib'
|
||||
import { notify } from '../toasts'
|
||||
|
||||
let settings = null
|
||||
|
||||
async function load() {
|
||||
const values = await DB.settings.toArray()
|
||||
const fromDB = Object.fromEntries(values.map((v) => [v.key, v.value]))
|
||||
settings = SettingsValidator.validate(fromDB).value
|
||||
settings = await getSettingsWithDefaults()
|
||||
}
|
||||
|
||||
async function save() {
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
await DB.settings.put({ key, value })
|
||||
}
|
||||
notify('Saved')
|
||||
}
|
||||
|
||||
onMount(load)
|
||||
|
@@ -4,17 +4,23 @@
|
||||
|
||||
import FileUpload from '../components/FileUpload.svelte'
|
||||
|
||||
import { dump, load, clear } from '../../shared/db'
|
||||
import { checkForErrors, DBValidator } from '../../shared/validation'
|
||||
import { clear, DB } from '../../shared/db'
|
||||
import { longPress } from '../../shared/lib'
|
||||
import { notify } from '../toasts'
|
||||
|
||||
let uploaded
|
||||
let file
|
||||
let loading = false
|
||||
|
||||
async function exportDB() {
|
||||
const data = await dump()
|
||||
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)
|
||||
try {
|
||||
loading = true
|
||||
const blob = await DB.export()
|
||||
const filename = `Ora [${dj().format('YYYY-MM-DD HH-mm-ss')}].json`
|
||||
saveAs(blob, filename)
|
||||
notify('Exported')
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function clearDB() {
|
||||
@@ -24,26 +30,24 @@
|
||||
|
||||
async function importDB() {
|
||||
try {
|
||||
await load(uploaded)
|
||||
alert('Imported')
|
||||
} catch {
|
||||
alert('Error importing')
|
||||
loading = true
|
||||
await clear()
|
||||
await DB.import(file)
|
||||
notify('Imported')
|
||||
} catch (e) {
|
||||
notify('Error importing', 'error')
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
$: disabled = uploaded && !checkForErrors(DBValidator, uploaded)
|
||||
</script>
|
||||
|
||||
<h2 class="mt-8 text-2xl">Your Data</h2>
|
||||
<div class="mt-2">
|
||||
<FileUpload bind:value={uploaded} />
|
||||
<button class="btn btn-primary" on:click={importDB} {disabled}>
|
||||
{#if uploaded && disabled}
|
||||
Invalid data
|
||||
{:else}
|
||||
Import
|
||||
{/if}
|
||||
<FileUpload bind:file />
|
||||
<button class="btn btn-primary" class:loading on:click={importDB} disabled={!file}>Import</button>
|
||||
<button class="btn btn-primary" class:loading on:click={exportDB}>Export</button>
|
||||
<button class="btn btn-error tooltip" class:loading data-tooltip="Hold to delete" use:longPress={clearDB}>
|
||||
Delete all data
|
||||
</button>
|
||||
<button class="btn btn-primary" on:click={exportDB}>Export</button>
|
||||
<button class="btn btn-error tooltip" data-tooltip="Hold to delete" use:longPress={clearDB}>Delete all data</button>
|
||||
</div>
|
||||
|
@@ -1,9 +1,8 @@
|
||||
import dj from 'dayjs'
|
||||
import Dexie from 'dexie'
|
||||
import RelativeTime from 'dayjs/plugin/relativeTime'
|
||||
import Duration from 'dayjs/plugin/duration'
|
||||
|
||||
import { checkForErrors, DBValidator } from './validation'
|
||||
import Dexie from 'dexie'
|
||||
import 'dexie-export-import'
|
||||
|
||||
dj.extend(Duration)
|
||||
dj.extend(RelativeTime)
|
||||
@@ -38,31 +37,5 @@ export async function insertLog({ timestamp, host, seconds }) {
|
||||
}
|
||||
|
||||
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 async function load(data) {
|
||||
if (!checkForErrors(DBValidator, 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),
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
export async function updateOrSet(table, key, value) {
|
||||
// const updated = await table.update(key, value)
|
||||
// if(updated === 0) await table.
|
||||
return Promise.allSettled(DB.tables.map((table) => DB.table(table.name).clear()))
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ import { groupBy, orderBy, sum } from 'lodash'
|
||||
import dj from 'dayjs'
|
||||
|
||||
import { DB } from './db.js'
|
||||
import { SettingsValidator } from './validation.js'
|
||||
|
||||
export async function data({ start, end }) {
|
||||
const logs = await getLogsBetweenDates({ start, end })
|
||||
@@ -60,3 +61,9 @@ export function percentagesToBool(percentages) {
|
||||
const blocked = percentages.map((p) => p >= 100).includes(true)
|
||||
return blocked
|
||||
}
|
||||
|
||||
export async function getSettingsWithDefaults() {
|
||||
const values = await DB.settings.toArray()
|
||||
const fromDB = Object.fromEntries(values.map((v) => [v.key, v.value]))
|
||||
return SettingsValidator.validate(fromDB).value
|
||||
}
|
||||
|
Reference in New Issue
Block a user