Skip to content

Commit d18d09f

Browse files
committed
feat(ios): add support for iOS data visualization
- buckets: detect 'aw-import-screentime' as android-compatible buckets - queries: preserve 'title' attribute during android event merging - activity: add post-processing to support iOS attributes (mapping 'title' to 'app' for readability) - visualization: add 'Bundle IDs' view for technical detail while keeping 'Top Apps' human-readable
1 parent 691c57b commit d18d09f

File tree

4 files changed

+53
-5
lines changed

4 files changed

+53
-5
lines changed

src/components/SelectableVisualization.vue

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ div
3333
:namefunc="e => e.data.title",
3434
:colorfunc="e => e.data.title",
3535
with_limit)
36+
div(v-if="type == 'top_bundle_ids'")
37+
aw-summary(:fields="activityStore.window.top_titles",
38+
:namefunc="e => e.data.classname",
39+
:colorfunc="e => e.data.app",
40+
with_limit)
3641
div(v-if="type == 'top_domains'")
3742
aw-summary(:fields="activityStore.browser.top_domains",
3843
:namefunc="e => e.data.$domain",
@@ -143,6 +148,7 @@ export default {
143148
types: [
144149
'top_apps',
145150
'top_titles',
151+
'top_bundle_ids',
146152
'top_domains',
147153
'top_urls',
148154
'top_browser_titles',
@@ -189,7 +195,11 @@ export default {
189195
},
190196
top_titles: {
191197
title: 'Top Window Titles',
192-
available: this.activityStore.window.available,
198+
available: this.activityStore.window.available || this.activityStore.android.available,
199+
},
200+
top_bundle_ids: {
201+
title: 'Bundle IDs',
202+
available: this.activityStore.window.available || this.activityStore.android.available,
193203
},
194204
top_domains: {
195205
title: 'Top Browser Domains',

src/queries.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams)
118118
// Fetch window/app events
119119
`events = flood(query_bucket(find_bucket("${bid_window}")));`,
120120
// On Android, merge events to avoid overload of events
121-
isAndroidParams(params) ? 'events = merge_events_by_keys(events, ["app"]);' : '',
121+
isAndroidParams(params) ? 'events = merge_events_by_keys(events, ["app", "title"]);' : '',
122122
// Fetch not-afk events
123123
isDesktopParams(params)
124124
? `not_afk = flood(query_bucket(find_bucket("${params.bid_afk}")));
@@ -200,7 +200,7 @@ export function appQuery(
200200
const code = `
201201
${canonicalEvents(params)}
202202
203-
title_events = sort_by_duration(merge_events_by_keys(events, ["app", "classname"]));
203+
title_events = sort_by_duration(merge_events_by_keys(events, ["app", "classname", "title"]));
204204
app_events = sort_by_duration(merge_events_by_keys(title_events, ["app"]));
205205
cat_events = sort_by_duration(merge_events_by_keys(events, ["$category"]));
206206

src/stores/activity.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,39 @@ export const useActivityStore = defineStore('activity', {
317317
filter_categories
318318
);
319319
const data = await getClient().query(periods, q).catch(this.errorHandler);
320+
321+
// Post-process for iOS compatibility (swap app <-> title)
322+
const androidBucket = this.buckets.android[0];
323+
const isIos = androidBucket && androidBucket.startsWith('aw-import-screentime');
324+
325+
if (isIos && data && data[0] && data[0].title_events) {
326+
data[0].title_events.forEach((e: IEvent) => {
327+
// iOS events have 'app' (bundleID) and 'title'. We swap them.
328+
// Check if title exists to avoid overwriting with undefined
329+
if (e.data.title) {
330+
const originalApp = e.data.app;
331+
e.data.classname = originalApp; // Bundle ID (e.g. com.google.ios.youtube)
332+
e.data.app = e.data.title; // Human Name (e.g. YouTube)
333+
}
334+
});
335+
336+
// Re-aggregate app_events from the modified title_events
337+
const new_app_events_map: Record<string, IEvent> = {};
338+
data[0].title_events.forEach((e: IEvent) => {
339+
const app = e.data.app;
340+
if (!new_app_events_map[app]) {
341+
// Clone event to avoid reference issues
342+
new_app_events_map[app] = { ...e, duration: 0, data: { ...e.data } };
343+
// Ensure we only keep the 'app' key for app_events to match standard structure
344+
new_app_events_map[app].data = { app: app, $category: e.data.$category };
345+
}
346+
new_app_events_map[app].duration += e.duration;
347+
});
348+
349+
// Sort by duration desc
350+
data[0].app_events = _.orderBy(_.values(new_app_events_map), ['duration'], ['desc']);
351+
}
352+
320353
this.query_window_completed(data[0]);
321354
},
322355

src/stores/buckets.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,15 @@ export const useBucketsStore = defineStore('buckets', {
9898
);
9999
},
100100
bucketsAndroid(): (host: string) => string[] {
101-
return host =>
102-
this.bucketsByType(host, 'currentwindow').filter((id: string) =>
101+
return host => {
102+
const android = this.bucketsByType(host, 'currentwindow').filter((id: string) =>
103103
id.startsWith('aw-watcher-android')
104104
);
105+
const ios = this.bucketsByType(host, 'app').filter((id: string) =>
106+
id.startsWith('aw-import-screentime')
107+
);
108+
return [...android, ...ios];
109+
};
105110
},
106111
bucketsEditor(): (host: string) => string[] {
107112
// fallback to a bucket with 'unknown' host, if one exists.

0 commit comments

Comments
 (0)