diff --git a/src/queries.ts b/src/queries.ts index f8d5c8ab..573a2158 100644 --- a/src/queries.ts +++ b/src/queries.ts @@ -39,6 +39,7 @@ interface DesktopQueryParams extends BaseQueryParams { bid_window: string; bid_afk: string; filter_afk: boolean; + always_active_pattern: string; } interface AndroidQueryParams extends BaseQueryParams { @@ -48,6 +49,7 @@ interface AndroidQueryParams extends BaseQueryParams { interface MultiQueryParams extends BaseQueryParams { hosts: string[]; filter_afk: boolean; + always_active_pattern: string; // This can be used to override params on a per-host basis host_params: { [host: string]: DesktopQueryParams | AndroidQueryParams }; } @@ -102,6 +104,9 @@ function isMultiParams(object: any): object is MultiQueryParams { export function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams): string { // Needs escaping for regex patterns like '\w' to work (JSON.stringify adds extra unecessary escaping) const categories_str = JSON.stringify(params.categories).replace(/\\\\/g, '\\'); + const always_active_pattern_str = isDesktopParams(params) + ? params.always_active_pattern.replace(/\\\\/g, '\\') + : undefined; const cat_filter_str = JSON.stringify(params.filter_categories); // For simplicity, we assume that bid_window and bid_android are exchangeable (note however it needs special treatment) @@ -115,7 +120,13 @@ export function canonicalEvents(params: DesktopQueryParams | AndroidQueryParams) // Fetch not-afk events isDesktopParams(params) ? `not_afk = flood(query_bucket("${params.bid_afk}")); - not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);` + not_afk = filter_keyvals(not_afk, "status", ["not-afk"]);` + + (always_active_pattern_str + ? `not_treat_as_afk = filter_keyvals_regex(events, "app", "${always_active_pattern_str}"); + not_afk = period_union(not_afk, not_treat_as_afk); + not_treat_as_afk = filter_keyvals_regex(events, "title", "${always_active_pattern_str}"); + not_afk = period_union(not_afk, not_treat_as_afk);` + : '') : '', // Fetch browser events isDesktopParams(params) && params.bid_browsers diff --git a/src/stores/activity.ts b/src/stores/activity.ts index d8078e59..72a2f98e 100644 --- a/src/stores/activity.ts +++ b/src/stores/activity.ts @@ -58,6 +58,7 @@ export interface QueryOptions { filter_categories?: string[][]; dont_query_inactive?: boolean; force?: boolean; + always_active_pattern?: string; } interface State { @@ -301,7 +302,7 @@ export const useActivityStore = defineStore('activity', { }, async query_multidevice_full( - { timeperiod, filter_categories, filter_afk }: QueryOptions, + { timeperiod, filter_categories, filter_afk, always_active_pattern }: QueryOptions, hosts: string[] ) { const periods = [timeperiodToStr(timeperiod)]; @@ -313,6 +314,7 @@ export const useActivityStore = defineStore('activity', { categories, filter_categories, host_params: {}, + always_active_pattern, }); const data = await getClient().query(periods, q); const data_window = data[0].window; @@ -329,6 +331,7 @@ export const useActivityStore = defineStore('activity', { filter_afk, include_audible, include_stopwatch, + always_active_pattern, }: QueryOptions) { const periods = [timeperiodToStr(timeperiod)]; const categories = useCategoryStore().classes_for_query; @@ -345,6 +348,7 @@ export const useActivityStore = defineStore('activity', { categories, filter_categories, include_audible, + always_active_pattern, }); const data = await getClient().query(periods, q); const data_window = data[0].window; @@ -382,6 +386,7 @@ export const useActivityStore = defineStore('activity', { filter_afk, include_stopwatch, dontQueryInactive, + always_active_pattern, }: QueryOptions & { dontQueryInactive: boolean }) { // TODO: Needs to be adapted for Android let periods: string[]; @@ -454,6 +459,7 @@ export const useActivityStore = defineStore('activity', { categories, filter_categories, filter_afk, + always_active_pattern, }) ); data = data.concat(result); diff --git a/src/stores/settings.ts b/src/stores/settings.ts index cb173747..6f90c861 100644 --- a/src/stores/settings.ts +++ b/src/stores/settings.ts @@ -18,6 +18,7 @@ interface State { newReleaseCheckData: Record; userSatisfactionPollData: Record; + always_active_pattern: string; // Whether to show certain WIP features devmode: boolean; @@ -49,6 +50,8 @@ export const useSettingsStore = defineStore('settings', { }, userSatisfactionPollData: {}, + always_active_pattern: '', + // Developer settings // NOTE: PRODUCTION might be undefined (in tests, for example) devmode: typeof PRODUCTION === 'undefined' ? true : !PRODUCTION, diff --git a/src/views/activity/Activity.vue b/src/views/activity/Activity.vue index 2b9d361a..31726cb2 100644 --- a/src/views/activity/Activity.vue +++ b/src/views/activity/Activity.vue @@ -203,6 +203,7 @@ export default { computed: { ...mapState(useViewsStore, ['views']), ...mapState(useSettingsStore, ['devmode']), + ...mapState(useSettingsStore, ['always_active_pattern']), // number of filters currently set (different from defaults) filters_set() { @@ -406,6 +407,7 @@ export default { include_audible: this.include_audible, include_stopwatch: this.include_stopwatch, filter_categories: this.filter_categories, + always_active_pattern: this.always_active_pattern, }; await this.activityStore.ensure_loaded(queryOptions); }, diff --git a/src/views/settings/ActivePatternSettings.vue b/src/views/settings/ActivePatternSettings.vue new file mode 100644 index 00000000..8707e3d7 --- /dev/null +++ b/src/views/settings/ActivePatternSettings.vue @@ -0,0 +1,65 @@ + + + diff --git a/src/views/settings/Settings.vue b/src/views/settings/Settings.vue index 14e3c409..b8868349 100644 --- a/src/views/settings/Settings.vue +++ b/src/views/settings/Settings.vue @@ -30,6 +30,10 @@ div hr + ActivePatternSettings + + hr + CategorizationSettings hr @@ -48,6 +52,7 @@ import LandingPageSettings from '~/views/settings/LandingPageSettings.vue'; import DeveloperSettings from '~/views/settings/DeveloperSettings.vue'; import Theme from '~/views/settings/Theme.vue'; import ColorSettings from '~/views/settings/ColorSettings.vue'; +import ActivePatternSettings from '~/views/settings/ActivePatternSettings.vue'; export default { name: 'Settings', @@ -60,6 +65,7 @@ export default { Theme, ColorSettings, DeveloperSettings, + ActivePatternSettings, }, async created() { await this.init(); diff --git a/test/unit/__snapshots__/queries.test.node.js.snap b/test/unit/__snapshots__/queries.test.node.js.snap index 2d55ae3b..a5d0451e 100644 --- a/test/unit/__snapshots__/queries.test.node.js.snap +++ b/test/unit/__snapshots__/queries.test.node.js.snap @@ -4,6 +4,10 @@ exports[`generate fullDesktopQuery 1`] = ` "events = flood(query_bucket(\\"\\")); not_afk = flood(query_bucket(\\"\\")); not_afk = filter_keyvals(not_afk, \\"status\\", [\\"not-afk\\"]); +not_treat_as_afk = filter_keyvals_regex(events, \\"app\\", \\"meow|nyaan\\"); +not_afk = period_union(not_afk, not_treat_as_afk); +not_treat_as_afk = filter_keyvals_regex(events, \\"title\\", \\"meow|nyaan\\"); +not_afk = period_union(not_afk, not_treat_as_afk); browser_events = []; audible_events = filter_keyvals(browser_events, \\"audible\\", [true]); not_afk = period_union(not_afk, audible_events); diff --git a/test/unit/queries.test.node.js b/test/unit/queries.test.node.js index 3f03c073..4fd3e397 100644 --- a/test/unit/queries.test.node.js +++ b/test/unit/queries.test.node.js @@ -8,6 +8,7 @@ test('generate fullDesktopQuery', () => { const categories = []; const filter_categories = true; const include_audible = true; + const always_active_pattern = 'meow|nyaan'; const query_lines = queries.fullDesktopQuery({ bid_window, bid_afk, @@ -16,6 +17,7 @@ test('generate fullDesktopQuery', () => { categories, filter_categories, include_audible, + always_active_pattern, }); // join query lines into a single string