125 lines
3.5 KiB
JavaScript
125 lines
3.5 KiB
JavaScript
|
const fs = require('fs')
|
||
|
const PouchDB = require('pouchdb')
|
||
|
const {DateTime, Interval, Duration} = require('luxon')
|
||
|
const util = require('util')
|
||
|
PouchDB.plugin(require('pouchdb-find'))
|
||
|
|
||
|
const fetch = require('node-fetch')
|
||
|
|
||
|
Duration.zero = Duration.fromMillis(0)
|
||
|
|
||
|
class Marvin {
|
||
|
constructor(credentials=Marvin.parseCredentials()) {
|
||
|
const c = credentials
|
||
|
let u = new URL(c.syncServer)
|
||
|
Object.assign(u, {
|
||
|
username: c.syncUser,
|
||
|
password: c.syncPassword,
|
||
|
pathname: "/" + c.syncDatabase
|
||
|
})
|
||
|
this.remote = new PouchDB(u.href)
|
||
|
this.db = new PouchDB("marvin")
|
||
|
this.synced = new Promise((resolve, reject) => {
|
||
|
this.remote.replicate.to(this.db)
|
||
|
.on('complete', resolve)
|
||
|
.on('error', reject)
|
||
|
})
|
||
|
this.synced.then(() => {
|
||
|
console.log("sync complete!")
|
||
|
})
|
||
|
this.root = new Taskset(this, 'root', {})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function chunk(xs, n) {
|
||
|
if ((xs.length % n) != 0) console.error("wrong chunk length")
|
||
|
const res = new Array(xs.length / n)
|
||
|
for(let i = 0; i < xs.length; i += n) {
|
||
|
res[i/n] = xs.slice(i, i+n)
|
||
|
}
|
||
|
return res
|
||
|
}
|
||
|
|
||
|
function toInterval([start, end]) {
|
||
|
return Interval.fromDateTimes(DateTime.fromMillis(start), DateTime.fromMillis(end))
|
||
|
}
|
||
|
|
||
|
Duration.sum = function sumDurations(durs) {
|
||
|
return durs.reduce((p, c) => p.plus(c), Duration.zero)
|
||
|
}
|
||
|
|
||
|
class Task {
|
||
|
constructor(task) {
|
||
|
this._task = task
|
||
|
Object.assign(this, {
|
||
|
title: task.title,
|
||
|
day: task.day,
|
||
|
dueDate: task.dueDate,
|
||
|
done: !!task.done,
|
||
|
doneAt: task.doneAt ? DateTime.fromMillis(task.doneAt) : null,
|
||
|
createdAt: DateTime.fromMillis(task.createdAt),
|
||
|
times: task.times ? chunk(task.times, 2).map(toInterval) : []
|
||
|
})
|
||
|
}
|
||
|
timeSpentOn(interval) {
|
||
|
return Duration.sum(this.times.map(time => time.overlaps(interval) ? time.intersection(interval).toDuration() : Duration.zero))
|
||
|
}
|
||
|
[util.inspect.custom](depth, opts) {
|
||
|
return {
|
||
|
title: this.title,
|
||
|
day: this.day,
|
||
|
dueDate: this.dueDate,
|
||
|
done: this.done,
|
||
|
doneAt: this.doneAt ? this.doneAt.toISO() : null,
|
||
|
createdAt: this.createdAt.toISO(),
|
||
|
times: this.times
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class Taskset {
|
||
|
constructor(marv, _id, rest) {
|
||
|
Object.assign(this, {marv, _id}, rest)
|
||
|
}
|
||
|
async category(n) {
|
||
|
await this.marv.synced
|
||
|
const d = await this.marv.db.find({
|
||
|
selector: {
|
||
|
db: "Categories",
|
||
|
title: n,
|
||
|
parentId: this._id
|
||
|
}
|
||
|
})
|
||
|
if (d && d.docs) return new Taskset(this.marv, d.docs[0]._id, d.docs[0])
|
||
|
}
|
||
|
async tasks() {
|
||
|
await this.marv.synced
|
||
|
const d = await this.marv.db.find({
|
||
|
selector: {
|
||
|
db: "Tasks",
|
||
|
parentId: this._id
|
||
|
},
|
||
|
})
|
||
|
if (d) {
|
||
|
return d.docs.map(x => new Task(x))
|
||
|
}
|
||
|
}
|
||
|
async tasksOverlapping(interval) {
|
||
|
const tasks = await this.tasks()
|
||
|
return tasks.filter(task =>
|
||
|
task.times.some(x => x.overlaps(interval))
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Marvin.parseCredentials = (fileContents = fs.readFileSync("credentials", "utf8")) => {
|
||
|
const res = {}
|
||
|
for(const line of fileContents.split('\n')) {
|
||
|
if (line.startsWith('# ') || !line) continue
|
||
|
const [k,v] = line.split(': ', 2)
|
||
|
res[k] = v
|
||
|
}
|
||
|
return res
|
||
|
}
|
||
|
module.exports = Marvin
|