Skip to content

Commit

Permalink
feat: started working on customizable views
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Oct 30, 2020
1 parent 6b5a67b commit 382308f
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 97 deletions.
22 changes: 22 additions & 0 deletions src/components/SelectableVisualization.vue
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ div
aw-categorytree(:events="$store.state.activity.category.top")
div(v-if="type == 'category_sunburst'")
aw-sunburst-categories(:data="top_categories_hierarchy", style="height: 20em")
div(v-if="type == 'timeline_barchart'")
aw-timeline-barchart(:datasets="datasets", style="height: 100")

</template>

Expand All @@ -70,8 +72,11 @@ div
</style>

<script>
import { split_by_hour_into_data } from '~/util/transforms';
// TODO: Move this somewhere else
import { build_category_hierarchy } from '~/util/classes';
function pick_subname_as_name(c) {
c.name = c.subname;
c.children = c.children.map(pick_subname_as_name);
Expand All @@ -97,6 +102,7 @@ export default {
'top_editor_files',
'top_editor_languages',
'top_editor_projects',
'timeline_barchart',
],
// TODO: Move this function somewhere else
top_editor_files_namefunc: e => {
Expand Down Expand Up @@ -163,6 +169,11 @@ export default {
title: 'Category Sunburst',
available: this.$store.state.activity.category.available,
},
timeline_barchart: {
title: 'Timeline (barchart)',
// TODO
//available: this.$store.state.activity.category.available,
},
};
},
top_categories_hierarchy: function () {
Expand All @@ -180,6 +191,17 @@ export default {
return null;
}
},
datasets: function () {
// TODO: Move elsewhere
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
return [
{
label: 'Total time',
backgroundColor: '#6699ff',
data,
},
];
},
},
};
</script>
10 changes: 9 additions & 1 deletion src/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const Home = () => import('./views/Home.vue');

// Activity views for desktop
const Activity = () => import('./views/activity/Activity.vue');
const ActivityView = () => import('./views/activity/ActivityView.vue');
const ActivitySummary = () => import('./views/activity/ActivitySummary.vue');
const ActivityWindow = () => import('./views/activity/ActivityWindow.vue');
const ActivityBrowser = () => import('./views/activity/ActivityBrowser.vue');
Expand All @@ -28,6 +29,13 @@ const router = new VueRouter({
component: Activity,
props: true,
children: [
{
path: 'view/:view_id?',
meta: { subview: 'view' },
name: 'activity-view',
component: ActivityView,
props: true,
},
{
path: 'summary',
meta: { subview: 'summary' },
Expand Down Expand Up @@ -58,7 +66,7 @@ const router = new VueRouter({
// (needs to be last since otherwise it'll always match first)
{
path: '',
redirect: 'summary',
redirect: 'view/',
},
],
},
Expand Down
42 changes: 42 additions & 0 deletions src/store/modules/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,40 @@ import {
build_category_hierarchy,
} from '~/util/classes';

const defaultViews = [
{
id: 'summary',
name: 'Summary',
elements: [
{ type: 'top_apps', size: 3 },
{ type: 'top_titles', size: 3 },
{ type: 'top_domains', size: 3 },
{ type: 'top_categories', size: 3 },
{ type: 'category_tree', size: 3 },
{ type: 'category_sunburst', size: 3 },
],
},
{
id: 'window',
name: 'Window',
elements: [
{ type: 'top_apps', size: 3 },
{ type: 'top_titles', size: 3 },
],
},
{
id: 'browser',
name: 'Browser',
elements: [
{ type: 'top_domains', size: 3 },
{ type: 'top_urls', size: 3 },
],
},
];

// initial state
const _state = {
views: [],
classes: [],
classes_unsaved_changes: false,
};
Expand Down Expand Up @@ -37,6 +69,7 @@ const getters = {
// actions
const actions = {
async load({ commit }) {
commit('loadViews');
commit('loadClasses', await loadClasses());
},
async save({ state, commit }) {
Expand All @@ -48,6 +81,15 @@ const actions = {

// mutations
const mutations = {
loadViews(state) {
const views_json = localStorage.views;
if (views_json && views_json.length >= 1) {
state.views = JSON.parse(views_json);
} else {
state.views = defaultViews;
}
console.log('Loaded views:', state.views);
},
loadClasses(state, classes) {
let i = 0;
state.classes = classes.map(c => Object.assign(c, { id: i++ }));
Expand Down
38 changes: 38 additions & 0 deletions src/util/transforms.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import _ from 'lodash';
import moment from 'moment';

// TODO: Move somewhere else, possibly turn into a serverside transform
export function split_by_hour_into_data(events) {
if (events === undefined || events === null || events.length == 0) return [];
const d = moment(events[0].timestamp).startOf('day');
return _.range(0, 24).map(h => {
let duration = 0;
const d_start = d.clone().hour(h);
const d_end = d.clone().hour(h + 1);
// 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.
events.map(e => {
const e_start = moment(e.timestamp);
const e_end = e_start.clone().add(e.duration, 'seconds');
if (
e_start.isBetween(d_start, d_end) ||
e_end.isBetween(d_start, d_end) ||
d_start.isBetween(e_start, e_end)
) {
if (d_start < e_start && e_end < d_end) {
// If entire event is contained within the hour
duration += e.duration;
} else if (d_start < e_start) {
// If start of event is within the hour, but not the end
duration += (d_end - e_start) / 1000;
} else if (e_end < d_end) {
// If end of event is within the hour, but not the start
duration += (e_end - d_start) / 1000;
} else {
// Happens if event covers entire hour and more
duration += 3600;
}
}
});
return duration / 60 / 60;
});
}
26 changes: 16 additions & 10 deletions src/views/activity/Activity.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,18 @@ div

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

ul.row.nav.nav-tabs.mt-3.pl-3
li.nav-item
router-link.nav-link(:to="{ name: 'activity-summary', params: $route.params }")
h6 Summary
li.nav-item
router-link.nav-link(:to="{ name: 'activity-window', params: $route.params }")
h6 Window
li.nav-item
router-link.nav-link(:to="{ name: 'activity-browser', params: $route.params }")
h6 Browser
ul.row.nav.nav-tabs.mt-3.px-3
li.nav-item(v-for="view in views")
router-link.nav-link(:to="{ name: 'activity-view', params: {...$route.params, view_id: view.id}}")
h6 {{view.name}}
li.nav-item
router-link.nav-link(:to="{ name: 'activity-editor', params: $route.params }")
h6 Editor
li.nav-item(style="margin-left: auto")
a.nav-link(@click="addView")
h6
icon(name="plus")
span New view

div
router-view
Expand Down Expand Up @@ -110,6 +109,7 @@ import _ from 'lodash';
import 'vue-awesome/icons/arrow-left';
import 'vue-awesome/icons/arrow-right';
import 'vue-awesome/icons/sync';
import 'vue-awesome/icons/plus';
export default {
name: 'Activity',
Expand All @@ -135,6 +135,9 @@ export default {
};
},
computed: {
views: function () {
return this.$store.state.settings.views;
},
_date: function () {
return this.date || get_today();
},
Expand Down Expand Up @@ -205,6 +208,9 @@ export default {
},
methods: {
addView: function () {
alert('Not implemented yet');
},
previousPeriod: function () {
return moment(this._date).subtract(1, `${this.periodLength}s`).format('YYYY-MM-DD');
},
Expand Down
87 changes: 1 addition & 86 deletions src/views/activity/ActivitySummary.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,57 +7,10 @@ div
aw-devonly(v-if="periodLength === 'day'" reason="Not ready for production, still experimenting")
div.row.mb-4
div.col-md-12
aw-timeline-barchart(:height="100", :datasets="datasets")
aw-selectable-vis(:id="index" type="timeline_barchart")
</template>

<script>
import _ from 'lodash';
import moment from 'moment';
import { build_category_hierarchy } from '~/util/classes';
function pick_subname_as_name(c) {
c.name = c.subname;
c.children = c.children.map(pick_subname_as_name);
return c;
}
// TODO: Move somewhere else, possibly turn into a serverside transform
function split_by_hour_into_data(events) {
if (events === undefined || events === null || events.length == 0) return [];
const d = moment(events[0].timestamp).startOf('day');
return _.range(0, 24).map(h => {
let duration = 0;
const d_start = d.clone().hour(h);
const d_end = d.clone().hour(h + 1);
// 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.
events.map(e => {
const e_start = moment(e.timestamp);
const e_end = e_start.clone().add(e.duration, 'seconds');
if (
e_start.isBetween(d_start, d_end) ||
e_end.isBetween(d_start, d_end) ||
d_start.isBetween(e_start, e_end)
) {
if (d_start < e_start && e_end < d_end) {
// If entire event is contained within the hour
duration += e.duration;
} else if (d_start < e_start) {
// If start of event is within the hour, but not the end
duration += (d_end - e_start) / 1000;
} else if (e_end < d_end) {
// If end of event is within the hour, but not the start
duration += (e_end - d_start) / 1000;
} else {
// Happens if event covers entire hour and more
duration += 3600;
}
}
});
return duration / 60 / 60;
});
}
export default {
name: 'Activity',
props: {
Expand All @@ -71,44 +24,6 @@ export default {
views: this.loadSummaryFavoriteViews(),
};
},
computed: {
top_apps: function () {
return this.$store.state.activity.window.top_apps;
},
top_titles: function () {
return this.$store.state.activity.window.top_titles;
},
top_categories: function () {
return this.$store.state.activity.category.top;
},
top_domains: function () {
return this.$store.state.activity.browser.top_domains;
},
top_categories_hierarchy: function () {
if (this.top_categories) {
const categories = this.top_categories.map(c => {
return { name: c.data.$category, size: c.duration };
});
return {
name: 'All',
children: build_category_hierarchy(categories).map(c => pick_subname_as_name(c)),
};
} else {
return null;
}
},
datasets: function () {
const data = split_by_hour_into_data(this.$store.state.activity.active.events);
return [
{
label: 'Total time',
backgroundColor: '#6699ff',
data,
},
];
},
},
methods: {
onTypeChange(id, type) {
this.views[id] = type;
Expand Down
41 changes: 41 additions & 0 deletions src/views/activity/ActivityView.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<template lang="pug">
div
div.row.mb-4
div.col-md-6.col-lg-4.p-3(v-for="el, index in view.elements")
aw-selectable-vis(:id="index" :type="el.type" @onTypeChange="onTypeChange")

div
icon(name="plus")
span Add visualization to view
</template>

<script>
export default {
name: 'ActivityView',
props: {
view_id: { type: String, default: 'default' },
periodLength: {
type: String,
default: 'day',
},
},
computed: {
view: function () {
if (this.view_id == 'default') {
return this.$store.state.settings.views[0];
} else {
return this.$store.state.settings.views.find(v => v.id == this.view_id);
}
},
},
methods: {
onTypeChange(id, type) {
// TODO: Use vuex store
console.log(this.$store.state.settings.views);
//this.elements[id] = type;
// Needed to emit the change to the child component
//this.$set(this.elements, this.elements);
},
},
};
</script>

0 comments on commit 382308f

Please sign in to comment.