Skip to content

Commit 8e886b4

Browse files
authored
Merge pull request #233 from ActivityWatch/dev/customizable-views
2 parents 6b5a67b + d4f2b3c commit 8e886b4

15 files changed

Lines changed: 399 additions & 404 deletions

src/components/CategoryEditTree.vue

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export default {
7878
},
7979
computed: {
8080
allCategories: function () {
81-
const categories = this.$store.getters['settings/all_categories'];
81+
const categories = this.$store.getters['categories/all_categories'];
8282
const entries = categories.map(c => {
8383
return { text: c.join('->'), value: c };
8484
});
@@ -94,7 +94,7 @@ export default {
9494
},
9595
methods: {
9696
addSubclass: function (parent) {
97-
this.$store.commit('settings/addClass', {
97+
this.$store.commit('categories/addClass', {
9898
name: parent.name.concat(['New class']),
9999
rule: { type: 'regex', regex: 'FILL ME' },
100100
});
@@ -103,7 +103,7 @@ export default {
103103
// TODO: Show a confirmation dialog
104104
// TODO: Remove children as well?
105105
// TODO: Move button to edit modal?
106-
this.$store.commit('settings/removeClass', _class);
106+
this.$store.commit('categories/removeClass', _class);
107107
},
108108
showEditModal() {
109109
this.$refs.edit.show();
@@ -130,7 +130,7 @@ export default {
130130
name: this.editing.parent.concat(this.editing.name),
131131
rule: this.editing.rule.type !== null ? this.editing.rule : { type: null },
132132
};
133-
this.$store.commit('settings/updateClass', new_class);
133+
this.$store.commit('categories/updateClass', new_class);
134134
135135
// Hide the modal manually
136136
this.$nextTick(() => {

src/components/SelectableVisualization.vue

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
<template lang="pug">
22
div
33
h5 {{ visualizations[type].title }}
4-
div
5-
b-dropdown.vis-style-dropdown-btn(size="sm" variant="outline-secondary")
4+
div(v-if="editable").vis-style-dropdown-btn
5+
b-dropdown.mr-1(size="sm" variant="outline-secondary")
66
template(v-slot:button-content)
77
icon(name="cog")
8-
b-dropdown-item(v-for="t in types" :key="t" variant="outline-secondary" @click="$emit('onTypeChange', id, t)" v-bind:disabled="!visualizations[t].available")
9-
| {{ visualizations[t].title }}
8+
b-dropdown-item(v-for="t in types" :key="t" variant="outline-secondary" @click="$emit('onTypeChange', id, t)")
9+
| {{ visualizations[t].title }} #[span.small(v-if="!visualizations[t].available" style="color: #A50") (no data)]
10+
b-button.p-0(size="sm", variant="outline-danger" @click="$emit('onRemove', id)")
11+
icon(name="times")
12+
13+
// Check data prerequisites
14+
div(v-if="!has_prerequisites")
15+
b-alert.small.px-2.py-1(show variant="warning")
16+
| This feature is missing data from a required watcher.
17+
| You can find a list of all watchers in #[a(href="https://activitywatch.readthedocs.io/en/latest/watchers.html") the documentation].
1018

1119
div(v-if="type == 'top_apps'")
1220
aw-summary(:fields="$store.state.activity.window.top_apps",
@@ -54,7 +62,11 @@ div
5462
aw-categorytree(:events="$store.state.activity.category.top")
5563
div(v-if="type == 'category_sunburst'")
5664
aw-sunburst-categories(:data="top_categories_hierarchy", style="height: 20em")
57-
65+
div(v-if="type == 'timeline_barchart'")
66+
aw-timeline-barchart(:datasets="datasets", style="height: 100")
67+
// TODO: Broke when we switched to customizable views (since it doesn't use vuex to request data)
68+
//div(v-if="type == 'sunburst_clock'")
69+
aw-sunburst-clock(:date="date", :afkBucketId="bucket_id_afk", :windowBucketId="bucket_id_window")
5870
</template>
5971

6072
<style lang="scss">
@@ -63,15 +75,21 @@ div
6375
top: 0.8em;
6476
right: 0.8em;
6577
66-
> .btn {
78+
.btn {
6779
border: 0px;
6880
}
6981
}
7082
</style>
7183

7284
<script>
85+
import 'vue-awesome/icons/cog';
86+
import 'vue-awesome/icons/times';
87+
88+
import { split_by_hour_into_data } from '~/util/transforms';
89+
7390
// TODO: Move this somewhere else
7491
import { build_category_hierarchy } from '~/util/classes';
92+
7593
function pick_subname_as_name(c) {
7694
c.name = c.subname;
7795
c.children = c.children.map(pick_subname_as_name);
@@ -83,6 +101,7 @@ export default {
83101
props: {
84102
id: Number,
85103
type: String,
104+
editable: { type: Boolean, default: true },
86105
},
87106
data: function () {
88107
return {
@@ -97,6 +116,7 @@ export default {
97116
'top_editor_files',
98117
'top_editor_languages',
99118
'top_editor_projects',
119+
'timeline_barchart',
100120
],
101121
// TODO: Move this function somewhere else
102122
top_editor_files_namefunc: e => {
@@ -163,8 +183,23 @@ export default {
163183
title: 'Category Sunburst',
164184
available: this.$store.state.activity.category.available,
165185
},
186+
timeline_barchart: {
187+
title: 'Timeline (barchart)',
188+
// TODO
189+
available: true,
190+
},
191+
/*
192+
sunburst_clock: {
193+
title: 'Sunburst clock',
194+
// TODO
195+
available: true,
196+
},
197+
*/
166198
};
167199
},
200+
has_prerequisites() {
201+
return this.visualizations[this.type].available;
202+
},
168203
top_categories_hierarchy: function () {
169204
const top_categories = this.$store.state.activity.category.top;
170205
if (top_categories) {
@@ -180,6 +215,17 @@ export default {
180215
return null;
181216
}
182217
},
218+
datasets: function () {
219+
// TODO: Move elsewhere
220+
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
221+
return [
222+
{
223+
label: 'Total time',
224+
backgroundColor: '#6699ff',
225+
data,
226+
},
227+
];
228+
},
183229
},
184230
};
185231
</script>

src/route.js

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ const Home = () => import('./views/Home.vue');
55

66
// Activity views for desktop
77
const Activity = () => import('./views/activity/Activity.vue');
8-
const ActivitySummary = () => import('./views/activity/ActivitySummary.vue');
9-
const ActivityWindow = () => import('./views/activity/ActivityWindow.vue');
10-
const ActivityBrowser = () => import('./views/activity/ActivityBrowser.vue');
11-
const ActivityEditor = () => import('./views/activity/ActivityEditor.vue');
8+
const ActivityView = () => import('./views/activity/ActivityView.vue');
129

1310
const Buckets = () => import('./views/Buckets.vue');
1411
const Bucket = () => import('./views/Bucket.vue');
@@ -29,36 +26,17 @@ const router = new VueRouter({
2926
props: true,
3027
children: [
3128
{
32-
path: 'summary',
33-
meta: { subview: 'summary' },
34-
name: 'activity-summary',
35-
component: ActivitySummary,
29+
path: 'view/:view_id?',
30+
meta: { subview: 'view' },
31+
name: 'activity-view',
32+
component: ActivityView,
3633
props: true,
3734
},
38-
{
39-
path: 'window',
40-
meta: { subview: 'window' },
41-
name: 'activity-window',
42-
component: ActivityWindow,
43-
props: true,
44-
},
45-
{
46-
path: 'browser',
47-
meta: { subview: 'browser' },
48-
name: 'activity-browser',
49-
component: ActivityBrowser,
50-
},
51-
{
52-
path: 'editor',
53-
meta: { subview: 'editor' },
54-
name: 'activity-editor',
55-
component: ActivityEditor,
56-
},
5735
// Unspecified should redirect to summary view is the summary view
5836
// (needs to be last since otherwise it'll always match first)
5937
{
6038
path: '',
61-
redirect: 'summary',
39+
redirect: 'view/',
6240
},
6341
],
6442
},

src/store/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import Vue from 'vue';
22
import Vuex from 'vuex';
33
import activity from './modules/activity';
44
import buckets from './modules/buckets';
5-
import settings from './modules/settings';
5+
import categories from './modules/categories';
6+
import views from './modules/views';
67
//import createLogger from '../../../src/plugins/logger';
78

89
Vue.use(Vuex);
@@ -13,7 +14,8 @@ export default new Vuex.Store({
1314
modules: {
1415
activity,
1516
buckets,
16-
settings,
17+
categories,
18+
views,
1719
},
1820
strict: debug,
1921
// plugins: debug ? [createLogger()] : [],

src/store/modules/views.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
const defaultViews = [
2+
{
3+
id: 'summary',
4+
name: 'Summary',
5+
elements: [
6+
{ type: 'top_apps', size: 3 },
7+
{ type: 'top_titles', size: 3 },
8+
{ type: 'top_domains', size: 3 },
9+
{ type: 'top_categories', size: 3 },
10+
{ type: 'category_tree', size: 3 },
11+
{ type: 'category_sunburst', size: 3 },
12+
],
13+
},
14+
{
15+
id: 'window',
16+
name: 'Window',
17+
elements: [
18+
{ type: 'top_apps', size: 3 },
19+
{ type: 'top_titles', size: 3 },
20+
],
21+
},
22+
{
23+
id: 'browser',
24+
name: 'Browser',
25+
elements: [
26+
{ type: 'top_domains', size: 3 },
27+
{ type: 'top_urls', size: 3 },
28+
],
29+
},
30+
{
31+
id: 'editor',
32+
name: 'Editor',
33+
elements: [
34+
{ type: 'top_editor_files', size: 3 },
35+
{ type: 'top_editor_projects', size: 3 },
36+
{ type: 'top_editor_languages', size: 3 },
37+
],
38+
},
39+
];
40+
41+
// initial state
42+
const _state = {
43+
views: [],
44+
};
45+
46+
// getters
47+
const getters = {};
48+
49+
// actions
50+
const actions = {
51+
async load({ commit }) {
52+
commit('loadViews');
53+
},
54+
async save({ state, commit }) {
55+
localStorage.views = JSON.stringify(state.views);
56+
// After save, reload views
57+
commit('loadViews');
58+
},
59+
};
60+
61+
// mutations
62+
const mutations = {
63+
loadViews(state) {
64+
const views_json = localStorage.views;
65+
if (views_json && views_json.length >= 1) {
66+
state.views = JSON.parse(views_json);
67+
} else {
68+
state.views = defaultViews;
69+
}
70+
console.log('Loaded views:', state.views);
71+
},
72+
restoreDefaults(state) {
73+
state.views = defaultViews;
74+
},
75+
addView(state, view) {
76+
state.views.push({ ...view, elements: [] });
77+
},
78+
removeView(state, { view_id }) {
79+
const idx = state.views.map(v => v.id).indexOf(view_id);
80+
console.log(idx);
81+
state.views.splice(idx, 1);
82+
},
83+
editView(state, { view_id, el_id, type }) {
84+
console.log(view_id, el_id, type);
85+
console.log(state.views);
86+
state.views.find(v => v.id == view_id).elements[el_id].type = type;
87+
},
88+
addVisualization(state, { view_id, type }) {
89+
state.views.find(v => v.id == view_id).elements.push({ type: type });
90+
},
91+
removeVisualization(state, { view_id, el_id }) {
92+
state.views.find(v => v.id == view_id).elements.splice(el_id, 1);
93+
},
94+
};
95+
96+
export default {
97+
namespaced: true,
98+
state: _state,
99+
getters,
100+
actions,
101+
mutations,
102+
};

src/util/transforms.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import _ from 'lodash';
2+
import moment from 'moment';
3+
4+
// TODO: Move somewhere else, possibly turn into a serverside transform
5+
export function split_by_hour_into_data(events) {
6+
if (events === undefined || events === null || events.length == 0) return [];
7+
const d = moment(events[0].timestamp).startOf('day');
8+
return _.range(0, 24).map(h => {
9+
let duration = 0;
10+
const d_start = d.clone().hour(h);
11+
const d_end = d.clone().hour(h + 1);
12+
// This can be made faster by not checking every event per hour, but since number of events is small anyway this and this is a lot shorter and easier to read it doesn't really matter.
13+
events.map(e => {
14+
const e_start = moment(e.timestamp);
15+
const e_end = e_start.clone().add(e.duration, 'seconds');
16+
if (
17+
e_start.isBetween(d_start, d_end) ||
18+
e_end.isBetween(d_start, d_end) ||
19+
d_start.isBetween(e_start, e_end)
20+
) {
21+
if (d_start < e_start && e_end < d_end) {
22+
// If entire event is contained within the hour
23+
duration += e.duration;
24+
} else if (d_start < e_start) {
25+
// If start of event is within the hour, but not the end
26+
duration += (d_end - e_start) / 1000;
27+
} else if (e_end < d_end) {
28+
// If end of event is within the hour, but not the start
29+
duration += (e_end - d_start) / 1000;
30+
} else {
31+
// Happens if event covers entire hour and more
32+
duration += 3600;
33+
}
34+
}
35+
});
36+
return duration / 60 / 60;
37+
});
38+
}

0 commit comments

Comments
 (0)