Skip to content

Commit ad06461

Browse files
feat(timeline): add AFK filter toggle to Timeline view (#776)
The Timeline view previously displayed raw events without any AFK filtering, meaning "treat as active" patterns and AFK status had no effect. Other views (Activity, Search) used query-based filtering via canonicalEvents() but Timeline was the exception. This adds a "Filter AFK" toggle to the Timeline filter panel. When enabled: - Window bucket events are filtered through the aw query engine using canonicalEvents() with filter_afk=true - The user's always_active_pattern setting is respected - AFK status buckets are hidden (used for filtering, not display) - Other buckets (editor, browser, etc.) remain unchanged Closes ActivityWatch/aw-watcher-afk#63
1 parent fdd2c4a commit ad06461

1 file changed

Lines changed: 84 additions & 0 deletions

File tree

src/views/Timeline.vue

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ div
3232
select(v-model="filter_client")
3333
option(:value='null') All
3434
option(v-for="client in clients", :value="client") {{ client }}
35+
tr
36+
th.pt-2.pr-3
37+
label AFK:
38+
td
39+
b-form-checkbox(v-model="filter_afk" size="sm" switch)
40+
| Filter AFK
3541
div.d-inline-block.border.rounded.p-2.mr-2(v-if="num_events !== 0")
3642
| Events shown: {{ num_events }}
3743
b-alert.d-inline-block.p-2.mb-0.mt-2(v-if="num_events === 0", variant="warning", show)
@@ -69,8 +75,11 @@ div
6975

7076
<script lang="ts">
7177
import _ from 'lodash';
78+
import { mapState } from 'pinia';
7279
import { useSettingsStore } from '~/stores/settings';
7380
import { useBucketsStore } from '~/stores/buckets';
81+
import { getClient } from '~/util/awclient';
82+
import { canonicalEvents } from '~/queries';
7483
import { seconds_to_duration } from '~/util/time';
7584
7685
export default {
@@ -86,11 +95,13 @@ export default {
8695
filter_hostname: null,
8796
filter_client: null,
8897
filter_duration: null,
98+
filter_afk: false,
8999
swimlane: null,
90100
updateTimelineWindow: true,
91101
};
92102
},
93103
computed: {
104+
...mapState(useSettingsStore, ['always_active_pattern']),
94105
timeintervalDefaultDuration() {
95106
const settingsStore = useSettingsStore();
96107
return Number(settingsStore.durationDefault);
@@ -110,6 +121,9 @@ export default {
110121
if (this.filter_duration > 0) {
111122
desc.push(seconds_to_duration(this.filter_duration));
112123
}
124+
if (this.filter_afk) {
125+
desc.push('AFK filtered');
126+
}
113127
114128
if (desc.length > 0) {
115129
return desc.join(', ');
@@ -134,6 +148,10 @@ export default {
134148
this.updateTimelineWindow = false;
135149
this.getBuckets();
136150
},
151+
filter_afk() {
152+
this.updateTimelineWindow = false;
153+
this.getBuckets();
154+
},
137155
swimlane() {
138156
this.updateTimelineWindow = false;
139157
this.getBuckets();
@@ -171,8 +189,74 @@ export default {
171189
}
172190
}
173191
192+
// AFK filtering: use query engine to filter window events by AFK status
193+
if (this.filter_afk) {
194+
buckets = await this._applyAfkFilter(buckets);
195+
}
196+
174197
this.buckets = buckets;
175198
},
199+
200+
// Replaces raw window bucket events with AFK-filtered events via aw query engine.
201+
// Also hides AFK status buckets since they're used for filtering, not display.
202+
_applyAfkFilter: async function (buckets) {
203+
const bucketsStore = useBucketsStore();
204+
const result = [];
205+
206+
for (const bucket of buckets) {
207+
// Hide AFK status buckets when AFK filtering is active
208+
if (bucket.type === 'afkstatus') {
209+
continue;
210+
}
211+
212+
// For window buckets, replace events with AFK-filtered query results
213+
if (bucket.type === 'currentwindow' && bucket.hostname) {
214+
const afkBucketIds = bucketsStore.bucketsAFK(bucket.hostname);
215+
if (afkBucketIds.length > 0) {
216+
try {
217+
const filteredEvents = await this._queryAfkFilteredEvents(bucket.id, afkBucketIds[0]);
218+
// Create a copy with filtered events to avoid mutating frozen all_buckets
219+
result.push({ ...bucket, events: filteredEvents });
220+
continue;
221+
} catch (e) {
222+
console.warn('AFK filter query failed, falling back to raw events:', e);
223+
}
224+
}
225+
}
226+
227+
// Keep other buckets unchanged
228+
result.push(bucket);
229+
}
230+
231+
return result;
232+
},
233+
234+
// Runs a canonicalEvents query to get window events filtered by AFK status,
235+
// respecting the user's always_active_pattern setting.
236+
_queryAfkFilteredEvents: async function (windowBucketId, afkBucketId) {
237+
const queryCode =
238+
canonicalEvents({
239+
bid_window: windowBucketId,
240+
bid_afk: afkBucketId,
241+
filter_afk: true,
242+
always_active_pattern: this.always_active_pattern || undefined,
243+
categories: [],
244+
filter_categories: null,
245+
}) + '\nRETURN = events;';
246+
247+
const queryArray = queryCode
248+
.split(';')
249+
.map(s => s.trim())
250+
.filter(s => s)
251+
.map(s => s + ';');
252+
253+
const start = this.daterange[0].format();
254+
const end = this.daterange[1].format();
255+
const timeperiods = [`${start}/${end}`];
256+
257+
const data = await getClient().query(timeperiods, queryArray);
258+
return data[0] || [];
259+
},
176260
},
177261
};
178262
</script>

0 commit comments

Comments
 (0)