Skip to content

Commit 382308f

Browse files
committed
feat: started working on customizable views
1 parent 6b5a67b commit 382308f

7 files changed

Lines changed: 169 additions & 97 deletions

File tree

src/components/SelectableVisualization.vue

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ div
5454
aw-categorytree(:events="$store.state.activity.category.top")
5555
div(v-if="type == 'category_sunburst'")
5656
aw-sunburst-categories(:data="top_categories_hierarchy", style="height: 20em")
57+
div(v-if="type == 'timeline_barchart'")
58+
aw-timeline-barchart(:datasets="datasets", style="height: 100")
5759

5860
</template>
5961

@@ -70,8 +72,11 @@ div
7072
</style>
7173

7274
<script>
75+
import { split_by_hour_into_data } from '~/util/transforms';
76+
7377
// TODO: Move this somewhere else
7478
import { build_category_hierarchy } from '~/util/classes';
79+
7580
function pick_subname_as_name(c) {
7681
c.name = c.subname;
7782
c.children = c.children.map(pick_subname_as_name);
@@ -97,6 +102,7 @@ export default {
97102
'top_editor_files',
98103
'top_editor_languages',
99104
'top_editor_projects',
105+
'timeline_barchart',
100106
],
101107
// TODO: Move this function somewhere else
102108
top_editor_files_namefunc: e => {
@@ -163,6 +169,11 @@ export default {
163169
title: 'Category Sunburst',
164170
available: this.$store.state.activity.category.available,
165171
},
172+
timeline_barchart: {
173+
title: 'Timeline (barchart)',
174+
// TODO
175+
//available: this.$store.state.activity.category.available,
176+
},
166177
};
167178
},
168179
top_categories_hierarchy: function () {
@@ -180,6 +191,17 @@ export default {
180191
return null;
181192
}
182193
},
194+
datasets: function () {
195+
// TODO: Move elsewhere
196+
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
197+
return [
198+
{
199+
label: 'Total time',
200+
backgroundColor: '#6699ff',
201+
data,
202+
},
203+
];
204+
},
183205
},
184206
};
185207
</script>

src/route.js

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

66
// Activity views for desktop
77
const Activity = () => import('./views/activity/Activity.vue');
8+
const ActivityView = () => import('./views/activity/ActivityView.vue');
89
const ActivitySummary = () => import('./views/activity/ActivitySummary.vue');
910
const ActivityWindow = () => import('./views/activity/ActivityWindow.vue');
1011
const ActivityBrowser = () => import('./views/activity/ActivityBrowser.vue');
@@ -28,6 +29,13 @@ const router = new VueRouter({
2829
component: Activity,
2930
props: true,
3031
children: [
32+
{
33+
path: 'view/:view_id?',
34+
meta: { subview: 'view' },
35+
name: 'activity-view',
36+
component: ActivityView,
37+
props: true,
38+
},
3139
{
3240
path: 'summary',
3341
meta: { subview: 'summary' },
@@ -58,7 +66,7 @@ const router = new VueRouter({
5866
// (needs to be last since otherwise it'll always match first)
5967
{
6068
path: '',
61-
redirect: 'summary',
69+
redirect: 'view/',
6270
},
6371
],
6472
},

src/store/modules/settings.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,40 @@ import {
66
build_category_hierarchy,
77
} from '~/util/classes';
88

9+
const defaultViews = [
10+
{
11+
id: 'summary',
12+
name: 'Summary',
13+
elements: [
14+
{ type: 'top_apps', size: 3 },
15+
{ type: 'top_titles', size: 3 },
16+
{ type: 'top_domains', size: 3 },
17+
{ type: 'top_categories', size: 3 },
18+
{ type: 'category_tree', size: 3 },
19+
{ type: 'category_sunburst', size: 3 },
20+
],
21+
},
22+
{
23+
id: 'window',
24+
name: 'Window',
25+
elements: [
26+
{ type: 'top_apps', size: 3 },
27+
{ type: 'top_titles', size: 3 },
28+
],
29+
},
30+
{
31+
id: 'browser',
32+
name: 'Browser',
33+
elements: [
34+
{ type: 'top_domains', size: 3 },
35+
{ type: 'top_urls', size: 3 },
36+
],
37+
},
38+
];
39+
940
// initial state
1041
const _state = {
42+
views: [],
1143
classes: [],
1244
classes_unsaved_changes: false,
1345
};
@@ -37,6 +69,7 @@ const getters = {
3769
// actions
3870
const actions = {
3971
async load({ commit }) {
72+
commit('loadViews');
4073
commit('loadClasses', await loadClasses());
4174
},
4275
async save({ state, commit }) {
@@ -48,6 +81,15 @@ const actions = {
4881

4982
// mutations
5083
const mutations = {
84+
loadViews(state) {
85+
const views_json = localStorage.views;
86+
if (views_json && views_json.length >= 1) {
87+
state.views = JSON.parse(views_json);
88+
} else {
89+
state.views = defaultViews;
90+
}
91+
console.log('Loaded views:', state.views);
92+
},
5193
loadClasses(state, classes) {
5294
let i = 0;
5395
state.classes = classes.map(c => Object.assign(c, { id: i++ }));

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+
}

src/views/activity/Activity.vue

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,19 +36,18 @@ div
3636

3737
aw-periodusage(:periodusage_arr="periodusage", @update="setDate")
3838

39-
ul.row.nav.nav-tabs.mt-3.pl-3
40-
li.nav-item
41-
router-link.nav-link(:to="{ name: 'activity-summary', params: $route.params }")
42-
h6 Summary
43-
li.nav-item
44-
router-link.nav-link(:to="{ name: 'activity-window', params: $route.params }")
45-
h6 Window
46-
li.nav-item
47-
router-link.nav-link(:to="{ name: 'activity-browser', params: $route.params }")
48-
h6 Browser
39+
ul.row.nav.nav-tabs.mt-3.px-3
40+
li.nav-item(v-for="view in views")
41+
router-link.nav-link(:to="{ name: 'activity-view', params: {...$route.params, view_id: view.id}}")
42+
h6 {{view.name}}
4943
li.nav-item
5044
router-link.nav-link(:to="{ name: 'activity-editor', params: $route.params }")
5145
h6 Editor
46+
li.nav-item(style="margin-left: auto")
47+
a.nav-link(@click="addView")
48+
h6
49+
icon(name="plus")
50+
span New view
5251

5352
div
5453
router-view
@@ -110,6 +109,7 @@ import _ from 'lodash';
110109
import 'vue-awesome/icons/arrow-left';
111110
import 'vue-awesome/icons/arrow-right';
112111
import 'vue-awesome/icons/sync';
112+
import 'vue-awesome/icons/plus';
113113
114114
export default {
115115
name: 'Activity',
@@ -135,6 +135,9 @@ export default {
135135
};
136136
},
137137
computed: {
138+
views: function () {
139+
return this.$store.state.settings.views;
140+
},
138141
_date: function () {
139142
return this.date || get_today();
140143
},
@@ -205,6 +208,9 @@ export default {
205208
},
206209
207210
methods: {
211+
addView: function () {
212+
alert('Not implemented yet');
213+
},
208214
previousPeriod: function () {
209215
return moment(this._date).subtract(1, `${this.periodLength}s`).format('YYYY-MM-DD');
210216
},

src/views/activity/ActivitySummary.vue

Lines changed: 1 addition & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -7,57 +7,10 @@ div
77
aw-devonly(v-if="periodLength === 'day'" reason="Not ready for production, still experimenting")
88
div.row.mb-4
99
div.col-md-12
10-
aw-timeline-barchart(:height="100", :datasets="datasets")
10+
aw-selectable-vis(:id="index" type="timeline_barchart")
1111
</template>
1212

1313
<script>
14-
import _ from 'lodash';
15-
import moment from 'moment';
16-
17-
import { build_category_hierarchy } from '~/util/classes';
18-
19-
function pick_subname_as_name(c) {
20-
c.name = c.subname;
21-
c.children = c.children.map(pick_subname_as_name);
22-
return c;
23-
}
24-
25-
// TODO: Move somewhere else, possibly turn into a serverside transform
26-
function split_by_hour_into_data(events) {
27-
if (events === undefined || events === null || events.length == 0) return [];
28-
const d = moment(events[0].timestamp).startOf('day');
29-
return _.range(0, 24).map(h => {
30-
let duration = 0;
31-
const d_start = d.clone().hour(h);
32-
const d_end = d.clone().hour(h + 1);
33-
// 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.
34-
events.map(e => {
35-
const e_start = moment(e.timestamp);
36-
const e_end = e_start.clone().add(e.duration, 'seconds');
37-
if (
38-
e_start.isBetween(d_start, d_end) ||
39-
e_end.isBetween(d_start, d_end) ||
40-
d_start.isBetween(e_start, e_end)
41-
) {
42-
if (d_start < e_start && e_end < d_end) {
43-
// If entire event is contained within the hour
44-
duration += e.duration;
45-
} else if (d_start < e_start) {
46-
// If start of event is within the hour, but not the end
47-
duration += (d_end - e_start) / 1000;
48-
} else if (e_end < d_end) {
49-
// If end of event is within the hour, but not the start
50-
duration += (e_end - d_start) / 1000;
51-
} else {
52-
// Happens if event covers entire hour and more
53-
duration += 3600;
54-
}
55-
}
56-
});
57-
return duration / 60 / 60;
58-
});
59-
}
60-
6114
export default {
6215
name: 'Activity',
6316
props: {
@@ -71,44 +24,6 @@ export default {
7124
views: this.loadSummaryFavoriteViews(),
7225
};
7326
},
74-
computed: {
75-
top_apps: function () {
76-
return this.$store.state.activity.window.top_apps;
77-
},
78-
top_titles: function () {
79-
return this.$store.state.activity.window.top_titles;
80-
},
81-
top_categories: function () {
82-
return this.$store.state.activity.category.top;
83-
},
84-
top_domains: function () {
85-
return this.$store.state.activity.browser.top_domains;
86-
},
87-
top_categories_hierarchy: function () {
88-
if (this.top_categories) {
89-
const categories = this.top_categories.map(c => {
90-
return { name: c.data.$category, size: c.duration };
91-
});
92-
93-
return {
94-
name: 'All',
95-
children: build_category_hierarchy(categories).map(c => pick_subname_as_name(c)),
96-
};
97-
} else {
98-
return null;
99-
}
100-
},
101-
datasets: function () {
102-
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
103-
return [
104-
{
105-
label: 'Total time',
106-
backgroundColor: '#6699ff',
107-
data,
108-
},
109-
];
110-
},
111-
},
11227
methods: {
11328
onTypeChange(id, type) {
11429
this.views[id] = type;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<template lang="pug">
2+
div
3+
div.row.mb-4
4+
div.col-md-6.col-lg-4.p-3(v-for="el, index in view.elements")
5+
aw-selectable-vis(:id="index" :type="el.type" @onTypeChange="onTypeChange")
6+
7+
div
8+
icon(name="plus")
9+
span Add visualization to view
10+
</template>
11+
12+
<script>
13+
export default {
14+
name: 'ActivityView',
15+
props: {
16+
view_id: { type: String, default: 'default' },
17+
periodLength: {
18+
type: String,
19+
default: 'day',
20+
},
21+
},
22+
computed: {
23+
view: function () {
24+
if (this.view_id == 'default') {
25+
return this.$store.state.settings.views[0];
26+
} else {
27+
return this.$store.state.settings.views.find(v => v.id == this.view_id);
28+
}
29+
},
30+
},
31+
methods: {
32+
onTypeChange(id, type) {
33+
// TODO: Use vuex store
34+
console.log(this.$store.state.settings.views);
35+
//this.elements[id] = type;
36+
// Needed to emit the change to the child component
37+
//this.$set(this.elements, this.elements);
38+
},
39+
},
40+
};
41+
</script>

0 commit comments

Comments
 (0)