Skip to content

Commit bfc24c9

Browse files
authored
Merge pull request #211 from ActivityWatch/dev/calendar
2 parents a0f0ae5 + 4e7303a commit bfc24c9

File tree

5 files changed

+196
-0
lines changed

5 files changed

+196
-0
lines changed

package-lock.json

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@
8383
"dependencies": {
8484
"@babel/polyfill": "^7.10.4",
8585
"@egjs/hammerjs": "^2.0.17",
86+
"@fullcalendar/timegrid": "^5.1.0",
87+
"@fullcalendar/vue": "^5.1.0",
8688
"@types/chart.js": "^2.9.23",
8789
"@types/chrome": "0.0.89",
8890
"@types/jest": "^24.9.1",

src/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ Vue.component('aw-timeline', () => import('./visualizations/TimelineSimple.vue')
5555
Vue.component('vis-timeline', () => import('./visualizations/VisTimeline.vue'));
5656
Vue.component('aw-categorytree', () => import('./visualizations/CategoryTree.vue'));
5757
Vue.component('aw-timeline-barchart', () => import('./visualizations/TimelineBarChart.vue'));
58+
Vue.component('aw-calendar', () => import('./visualizations/Calendar.vue'));
5859

5960
// A mixin to make async method errors propagate
6061
Vue.mixin(require('~/mixins/asyncErrorCaptured.js'));

src/views/Timeline.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ div
1212
| Drag to pan and scroll to zoom.
1313
div(style="clear: both")
1414
vis-timeline(:buckets="buckets", :showRowLabels='true', :queriedInterval="daterange")
15+
16+
aw-devonly(reason="Not ready for production, still experimenting")
17+
aw-calendar(:buckets="buckets")
1518
div(v-show="!(buckets !== null && num_events)")
1619
h1 Loading...
1720
</template>

src/visualizations/Calendar.vue

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<template lang="pug">
2+
div.mx-3
3+
b-form
4+
b-form-group(label="Bucket:")
5+
select(v-model="selectedBucket")
6+
option(v-for="bucket in buckets", :value="bucket.id") {{ bucket.id }}
7+
b-form-group(label="Show:")
8+
select(v-model="view")
9+
option(value="timeGridDay") Day
10+
option(value="timeGridWeek") Week
11+
b-form-group
12+
b-checkbox(v-model="fitToActive")
13+
| Fit to active
14+
FullCalendar(ref="fullCalendar", :options="calendarOptions")
15+
</template>
16+
17+
<script>
18+
import { getTitleAttr, getColorFromString } from '../util/color';
19+
import moment from 'moment';
20+
import _ from 'lodash';
21+
import FullCalendar from '@fullcalendar/vue';
22+
import timeGridPlugin from '@fullcalendar/timegrid';
23+
24+
// TODO: Use canonical timeline query, with flooding and categorization
25+
// TODO: Checkbox for toggling category-view, where adjacent events with same category are merged and the events are labeled by category
26+
// TODO: Use the recommended way of dynamically getting events: https://fullcalendar.io/docs/events-function
27+
export default {
28+
components: {
29+
FullCalendar, // make the <FullCalendar> tag available
30+
},
31+
props: {
32+
buckets: { type: Array },
33+
},
34+
data() {
35+
return { fitToActive: false, selectedBucket: null, view: 'timeGridDay' };
36+
},
37+
computed: {
38+
calendarOptions: function () {
39+
const events = this.events;
40+
const first = _.minBy(events, e => e.start);
41+
const last = _.maxBy(events, e => e.end);
42+
// FIXME: end must be at least one slot (1 hour) after start, otherwise it fails hard
43+
let start, end;
44+
if (this.fitToActive && events.length > 0) {
45+
console.log(first.start);
46+
start = moment(first.start).startOf('hour').format().slice(11, 16);
47+
end = moment(last.end).endOf('hour').format().slice(11, 16);
48+
} else {
49+
start = '00:00:00';
50+
end = '24:00:00';
51+
}
52+
return {
53+
plugins: [timeGridPlugin],
54+
initialView: this.view,
55+
eventClick: this.onEventClick,
56+
events: events,
57+
allDaySlot: false,
58+
slotMinTime: start,
59+
slotMaxTime: end,
60+
nowIndicator: true,
61+
expandRows: true,
62+
slotLabelFormat: {
63+
hour: '2-digit',
64+
minute: '2-digit',
65+
//second: '2-digit',
66+
hour12: false,
67+
},
68+
};
69+
},
70+
events: function () {
71+
// NOTE: This returns FullCalendar events, not ActivityWatch events.
72+
if (this.buckets == null) return [];
73+
74+
const bucket = _.find(this.buckets, b => b.id == this.selectedBucket);
75+
if (bucket == null) {
76+
return;
77+
}
78+
let events = bucket.events;
79+
events = _.filter(events, e => e.duration > 10);
80+
events = _.map(events, e => {
81+
return {
82+
title: getTitleAttr(bucket, e),
83+
start: moment(e.timestamp).format(),
84+
end: moment(e.timestamp).add(e.duration, 'seconds').format(),
85+
backgroundColor: getColorFromString(getTitleAttr(bucket, e)),
86+
};
87+
});
88+
return events;
89+
},
90+
},
91+
watch: {
92+
view: function (to) {
93+
const calendar = this.$refs.fullCalendar.getApi();
94+
calendar.changeView(to);
95+
},
96+
},
97+
methods: {
98+
onEventClick: function (arg) {
99+
// TODO: Open event inspector/editor here
100+
alert('event click! ' + JSON.stringify(arg.event));
101+
},
102+
},
103+
};
104+
</script>

0 commit comments

Comments
 (0)