Skip to content

Commit

Permalink
Merge pull request #233 from ActivityWatch/dev/customizable-views
Browse files Browse the repository at this point in the history
  • Loading branch information
ErikBjare committed Nov 2, 2020
2 parents 6b5a67b + d4f2b3c commit 8e886b4
Show file tree
Hide file tree
Showing 15 changed files with 399 additions and 404 deletions.
8 changes: 4 additions & 4 deletions src/components/CategoryEditTree.vue
Expand Up @@ -78,7 +78,7 @@ export default {
},
computed: {
allCategories: function () {
const categories = this.$store.getters['settings/all_categories'];
const categories = this.$store.getters['categories/all_categories'];
const entries = categories.map(c => {
return { text: c.join('->'), value: c };
});
Expand All @@ -94,7 +94,7 @@ export default {
},
methods: {
addSubclass: function (parent) {
this.$store.commit('settings/addClass', {
this.$store.commit('categories/addClass', {
name: parent.name.concat(['New class']),
rule: { type: 'regex', regex: 'FILL ME' },
});
Expand All @@ -103,7 +103,7 @@ export default {
// TODO: Show a confirmation dialog
// TODO: Remove children as well?
// TODO: Move button to edit modal?
this.$store.commit('settings/removeClass', _class);
this.$store.commit('categories/removeClass', _class);
},
showEditModal() {
this.$refs.edit.show();
Expand All @@ -130,7 +130,7 @@ export default {
name: this.editing.parent.concat(this.editing.name),
rule: this.editing.rule.type !== null ? this.editing.rule : { type: null },
};
this.$store.commit('settings/updateClass', new_class);
this.$store.commit('categories/updateClass', new_class);
// Hide the modal manually
this.$nextTick(() => {
Expand Down
58 changes: 52 additions & 6 deletions src/components/SelectableVisualization.vue
@@ -1,12 +1,20 @@
<template lang="pug">
div
h5 {{ visualizations[type].title }}
div
b-dropdown.vis-style-dropdown-btn(size="sm" variant="outline-secondary")
div(v-if="editable").vis-style-dropdown-btn
b-dropdown.mr-1(size="sm" variant="outline-secondary")
template(v-slot:button-content)
icon(name="cog")
b-dropdown-item(v-for="t in types" :key="t" variant="outline-secondary" @click="$emit('onTypeChange', id, t)" v-bind:disabled="!visualizations[t].available")
| {{ visualizations[t].title }}
b-dropdown-item(v-for="t in types" :key="t" variant="outline-secondary" @click="$emit('onTypeChange', id, t)")
| {{ visualizations[t].title }} #[span.small(v-if="!visualizations[t].available" style="color: #A50") (no data)]
b-button.p-0(size="sm", variant="outline-danger" @click="$emit('onRemove', id)")
icon(name="times")

// Check data prerequisites
div(v-if="!has_prerequisites")
b-alert.small.px-2.py-1(show variant="warning")
| This feature is missing data from a required watcher.
| You can find a list of all watchers in #[a(href="https://activitywatch.readthedocs.io/en/latest/watchers.html") the documentation].

div(v-if="type == 'top_apps'")
aw-summary(:fields="$store.state.activity.window.top_apps",
Expand Down Expand Up @@ -54,7 +62,11 @@ 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")
// TODO: Broke when we switched to customizable views (since it doesn't use vuex to request data)
//div(v-if="type == 'sunburst_clock'")
aw-sunburst-clock(:date="date", :afkBucketId="bucket_id_afk", :windowBucketId="bucket_id_window")
</template>

<style lang="scss">
Expand All @@ -63,15 +75,21 @@ div
top: 0.8em;
right: 0.8em;
> .btn {
.btn {
border: 0px;
}
}
</style>

<script>
import 'vue-awesome/icons/cog';
import 'vue-awesome/icons/times';
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 @@ -83,6 +101,7 @@ export default {
props: {
id: Number,
type: String,
editable: { type: Boolean, default: true },
},
data: function () {
return {
Expand All @@ -97,6 +116,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,8 +183,23 @@ export default {
title: 'Category Sunburst',
available: this.$store.state.activity.category.available,
},
timeline_barchart: {
title: 'Timeline (barchart)',
// TODO
available: true,
},
/*
sunburst_clock: {
title: 'Sunburst clock',
// TODO
available: true,
},
*/
};
},
has_prerequisites() {
return this.visualizations[this.type].available;
},
top_categories_hierarchy: function () {
const top_categories = this.$store.state.activity.category.top;
if (top_categories) {
Expand All @@ -180,6 +215,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>
34 changes: 6 additions & 28 deletions src/route.js
Expand Up @@ -5,10 +5,7 @@ const Home = () => import('./views/Home.vue');

// Activity views for desktop
const Activity = () => import('./views/activity/Activity.vue');
const ActivitySummary = () => import('./views/activity/ActivitySummary.vue');
const ActivityWindow = () => import('./views/activity/ActivityWindow.vue');
const ActivityBrowser = () => import('./views/activity/ActivityBrowser.vue');
const ActivityEditor = () => import('./views/activity/ActivityEditor.vue');
const ActivityView = () => import('./views/activity/ActivityView.vue');

const Buckets = () => import('./views/Buckets.vue');
const Bucket = () => import('./views/Bucket.vue');
Expand All @@ -29,36 +26,17 @@ const router = new VueRouter({
props: true,
children: [
{
path: 'summary',
meta: { subview: 'summary' },
name: 'activity-summary',
component: ActivitySummary,
path: 'view/:view_id?',
meta: { subview: 'view' },
name: 'activity-view',
component: ActivityView,
props: true,
},
{
path: 'window',
meta: { subview: 'window' },
name: 'activity-window',
component: ActivityWindow,
props: true,
},
{
path: 'browser',
meta: { subview: 'browser' },
name: 'activity-browser',
component: ActivityBrowser,
},
{
path: 'editor',
meta: { subview: 'editor' },
name: 'activity-editor',
component: ActivityEditor,
},
// Unspecified should redirect to summary view is the summary view
// (needs to be last since otherwise it'll always match first)
{
path: '',
redirect: 'summary',
redirect: 'view/',
},
],
},
Expand Down
6 changes: 4 additions & 2 deletions src/store/index.js
Expand Up @@ -2,7 +2,8 @@ import Vue from 'vue';
import Vuex from 'vuex';
import activity from './modules/activity';
import buckets from './modules/buckets';
import settings from './modules/settings';
import categories from './modules/categories';
import views from './modules/views';
//import createLogger from '../../../src/plugins/logger';

Vue.use(Vuex);
Expand All @@ -13,7 +14,8 @@ export default new Vuex.Store({
modules: {
activity,
buckets,
settings,
categories,
views,
},
strict: debug,
// plugins: debug ? [createLogger()] : [],
Expand Down
File renamed without changes.
102 changes: 102 additions & 0 deletions src/store/modules/views.js
@@ -0,0 +1,102 @@
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 },
],
},
{
id: 'editor',
name: 'Editor',
elements: [
{ type: 'top_editor_files', size: 3 },
{ type: 'top_editor_projects', size: 3 },
{ type: 'top_editor_languages', size: 3 },
],
},
];

// initial state
const _state = {
views: [],
};

// getters
const getters = {};

// actions
const actions = {
async load({ commit }) {
commit('loadViews');
},
async save({ state, commit }) {
localStorage.views = JSON.stringify(state.views);
// After save, reload views
commit('loadViews');
},
};

// 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);
},
restoreDefaults(state) {
state.views = defaultViews;
},
addView(state, view) {
state.views.push({ ...view, elements: [] });
},
removeView(state, { view_id }) {
const idx = state.views.map(v => v.id).indexOf(view_id);
console.log(idx);
state.views.splice(idx, 1);
},
editView(state, { view_id, el_id, type }) {
console.log(view_id, el_id, type);
console.log(state.views);
state.views.find(v => v.id == view_id).elements[el_id].type = type;
},
addVisualization(state, { view_id, type }) {
state.views.find(v => v.id == view_id).elements.push({ type: type });
},
removeVisualization(state, { view_id, el_id }) {
state.views.find(v => v.id == view_id).elements.splice(el_id, 1);
},
};

export default {
namespaced: true,
state: _state,
getters,
actions,
mutations,
};
38 changes: 38 additions & 0 deletions src/util/transforms.js
@@ -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;
});
}

0 comments on commit 8e886b4

Please sign in to comment.