mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
feat: add Exclude home world for activity tab
This commit is contained in:
@@ -142,6 +142,18 @@
|
|||||||
</span>
|
</span>
|
||||||
<Spinner v-if="topWorldsLoadingVisible" class="h-3.5 w-3.5" />
|
<Spinner v-if="topWorldsLoadingVisible" class="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<div
|
||||||
|
v-if="isSelf && currentHomeWorldId"
|
||||||
|
class="flex items-center gap-1.5 text-sm text-muted-foreground">
|
||||||
|
<Switch
|
||||||
|
:model-value="excludeHomeWorldEnabled"
|
||||||
|
class="scale-75"
|
||||||
|
@update:model-value="onExcludeHomeWorldToggle" />
|
||||||
|
<span class="whitespace-nowrap">
|
||||||
|
{{ t('dialog.user.activity.most_visited_worlds.exclude_home_world') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div v-if="topWorlds.length > 0" class="flex items-center gap-2">
|
<div v-if="topWorlds.length > 0" class="flex items-center gap-2">
|
||||||
<span class="text-muted-foreground text-sm">{{ t('common.sort_by') }}</span>
|
<span class="text-muted-foreground text-sm">{{ t('common.sort_by') }}</span>
|
||||||
<Select v-model="topWorldsSortBy" :disabled="topWorldsLoading">
|
<Select v-model="topWorldsSortBy" :disabled="topWorldsLoading">
|
||||||
@@ -159,6 +171,7 @@
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="topWorldsLoadingVisible && topWorlds.length === 0"
|
v-if="topWorldsLoadingVisible && topWorlds.length === 0"
|
||||||
class="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
class="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
||||||
@@ -244,7 +257,7 @@
|
|||||||
import configRepository from '../../../services/config';
|
import configRepository from '../../../services/config';
|
||||||
import { worldRequest } from '../../../api';
|
import { worldRequest } from '../../../api';
|
||||||
import { showWorldDialog } from '../../../coordinators/worldCoordinator';
|
import { showWorldDialog } from '../../../coordinators/worldCoordinator';
|
||||||
import { timeToText } from '../../../shared/utils';
|
import { parseLocation, timeToText } from '../../../shared/utils';
|
||||||
import { useActivityStore, useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
import { useActivityStore, useAppearanceSettingsStore, useUserStore } from '../../../stores';
|
||||||
import { useWorldStore } from '../../../stores/world';
|
import { useWorldStore } from '../../../stores/world';
|
||||||
import { buildHeatmapOption, toHeatmapSeriesData } from './activity/buildHeatmapOption';
|
import { buildHeatmapOption, toHeatmapSeriesData } from './activity/buildHeatmapOption';
|
||||||
@@ -270,6 +283,7 @@
|
|||||||
const topWorldsLoadingVisible = ref(false);
|
const topWorldsLoadingVisible = ref(false);
|
||||||
const topWorlds = ref([]);
|
const topWorlds = ref([]);
|
||||||
const topWorldsSortBy = ref('time');
|
const topWorldsSortBy = ref('time');
|
||||||
|
const excludeHomeWorldEnabled = ref(false);
|
||||||
const excludeHoursEnabled = ref(false);
|
const excludeHoursEnabled = ref(false);
|
||||||
const excludeStartHour = ref('1');
|
const excludeStartHour = ref('1');
|
||||||
const excludeEndHour = ref('6');
|
const excludeEndHour = ref('6');
|
||||||
@@ -295,6 +309,13 @@
|
|||||||
const OVERLAP_RENDER_DELAY = 80;
|
const OVERLAP_RENDER_DELAY = 80;
|
||||||
|
|
||||||
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
||||||
|
const currentHomeWorldId = computed(() => {
|
||||||
|
const homeLocation = currentUser.value.homeLocation;
|
||||||
|
if (!homeLocation) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return parseLocation(homeLocation).worldId || homeLocation;
|
||||||
|
});
|
||||||
const sortedTopWorlds = computed(() => topWorlds.value);
|
const sortedTopWorlds = computed(() => topWorlds.value);
|
||||||
const dayLabels = computed(() => [
|
const dayLabels = computed(() => [
|
||||||
t('dialog.user.activity.days.sun'),
|
t('dialog.user.activity.days.sun'),
|
||||||
@@ -330,6 +351,7 @@
|
|||||||
topWorldsLoading.value = false;
|
topWorldsLoading.value = false;
|
||||||
topWorldsLoadingVisible.value = false;
|
topWorldsLoadingVisible.value = false;
|
||||||
topWorlds.value = [];
|
topWorlds.value = [];
|
||||||
|
excludeHomeWorldEnabled.value = false;
|
||||||
isOverlapLoading.value = false;
|
isOverlapLoading.value = false;
|
||||||
isOverlapLoadingVisible.value = false;
|
isOverlapLoadingVisible.value = false;
|
||||||
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
@@ -427,7 +449,8 @@
|
|||||||
userId,
|
userId,
|
||||||
rangeDays,
|
rangeDays,
|
||||||
limit: 5,
|
limit: 5,
|
||||||
sortBy
|
sortBy,
|
||||||
|
excludeWorldId: excludeHomeWorldEnabled.value ? currentHomeWorldId.value : ''
|
||||||
});
|
});
|
||||||
if (
|
if (
|
||||||
requestId !== activeTopWorldsRequestId ||
|
requestId !== activeTopWorldsRequestId ||
|
||||||
@@ -445,6 +468,21 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function refreshTopWorldsOnly() {
|
||||||
|
const userId = userDialog.value.id;
|
||||||
|
if (!isSelf.value || !hasAnyData.value || !userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rangeDays = parseInt(selectedPeriod.value, 10) || 30;
|
||||||
|
await loadTopWorldsSection({
|
||||||
|
userId,
|
||||||
|
rangeDays,
|
||||||
|
sortBy: topWorldsSortBy.value,
|
||||||
|
period: selectedPeriod.value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshData({ silent = false, forceRefresh = false } = {}) {
|
async function refreshData({ silent = false, forceRefresh = false } = {}) {
|
||||||
const userId = userDialog.value.id;
|
const userId = userDialog.value.id;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
@@ -589,6 +627,11 @@
|
|||||||
await refreshOverlapOnly();
|
await refreshOverlapOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function onExcludeHomeWorldToggle(value) {
|
||||||
|
excludeHomeWorldEnabled.value = value;
|
||||||
|
await refreshTopWorldsOnly();
|
||||||
|
}
|
||||||
|
|
||||||
async function fetchMissingTopWorldThumbnails(worlds) {
|
async function fetchMissingTopWorldThumbnails(worlds) {
|
||||||
const missingWorldIds = worlds
|
const missingWorldIds = worlds
|
||||||
.map((world) => world.worldId)
|
.map((world) => world.worldId)
|
||||||
@@ -819,22 +862,16 @@
|
|||||||
);
|
);
|
||||||
watch(
|
watch(
|
||||||
() => topWorldsSortBy.value,
|
() => topWorldsSortBy.value,
|
||||||
async (newSortBy) => {
|
() => {
|
||||||
if (!isSelf.value || !hasAnyData.value) {
|
void refreshTopWorldsOnly();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const userId = userDialog.value.id;
|
);
|
||||||
if (!userId) {
|
watch(
|
||||||
return;
|
() => currentUser.value.homeLocation,
|
||||||
|
() => {
|
||||||
|
if (excludeHomeWorldEnabled.value) {
|
||||||
|
void refreshTopWorldsOnly();
|
||||||
}
|
}
|
||||||
const period = selectedPeriod.value;
|
|
||||||
const rangeDays = parseInt(period, 10) || 30;
|
|
||||||
await loadTopWorldsSection({
|
|
||||||
userId,
|
|
||||||
rangeDays,
|
|
||||||
sortBy: newSortBy,
|
|
||||||
period
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@@ -1463,6 +1463,7 @@
|
|||||||
"most_visited_worlds": {
|
"most_visited_worlds": {
|
||||||
"header": "Most Visited Worlds",
|
"header": "Most Visited Worlds",
|
||||||
"loading": "Loading most visited worlds...",
|
"loading": "Loading most visited worlds...",
|
||||||
|
"exclude_home_world": "Exclude home world",
|
||||||
"sort_by_time": "By Time",
|
"sort_by_time": "By Time",
|
||||||
"sort_by_count": "By Visits",
|
"sort_by_count": "By Visits",
|
||||||
"visit_count_label": "{count} visits"
|
"visit_count_label": "{count} visits"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
execute: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../sqlite.js', () => ({
|
||||||
|
default: {
|
||||||
|
execute: mocks.execute,
|
||||||
|
executeNonQuery: vi.fn()
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vi.mock('../index.js', () => ({
|
||||||
|
dbVars: {
|
||||||
|
maxTableSize: 500,
|
||||||
|
userPrefix: ''
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { gameLog } from '../gameLog.js';
|
||||||
|
|
||||||
|
describe('gameLog.getMyTopWorlds', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocks.execute.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adds an exclude clause when a home world id is provided', async () => {
|
||||||
|
mocks.execute.mockImplementation(async (callback, sql, params) => {
|
||||||
|
callback(['wrld_1', 'World One', 3, 9000]);
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await gameLog.getMyTopWorlds(30, 5, 'time', 'wrld_home');
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{
|
||||||
|
worldId: 'wrld_1',
|
||||||
|
worldName: 'World One',
|
||||||
|
visitCount: 3,
|
||||||
|
totalTime: 9000
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
expect(mocks.execute).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mocks.execute.mock.calls[0][1]).toContain('AND world_id != @excludeWorldId');
|
||||||
|
expect(mocks.execute.mock.calls[0][2]).toMatchObject({
|
||||||
|
'@limit': 5,
|
||||||
|
'@daysOffset': '-30 days',
|
||||||
|
'@excludeWorldId': 'wrld_home'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1449,18 +1449,23 @@ const gameLog = {
|
|||||||
* @param {number} [days] - Number of days to look back. Omit or 0 for all time.
|
* @param {number} [days] - Number of days to look back. Omit or 0 for all time.
|
||||||
* @param {number} [limit=5] - Maximum number of worlds to return.
|
* @param {number} [limit=5] - Maximum number of worlds to return.
|
||||||
* @param {'time'|'count'} [sortBy='time'] - Sort by total time or visit count.
|
* @param {'time'|'count'} [sortBy='time'] - Sort by total time or visit count.
|
||||||
|
* @param {string} [excludeWorldId=''] - Optional world ID to exclude from results.
|
||||||
* @returns {Promise<Array<{worldId: string, worldName: string, visitCount: number, totalTime: number}>>}
|
* @returns {Promise<Array<{worldId: string, worldName: string, visitCount: number, totalTime: number}>>}
|
||||||
*/
|
*/
|
||||||
async getMyTopWorlds(days = 0, limit = 5, sortBy = 'time') {
|
async getMyTopWorlds(days = 0, limit = 5, sortBy = 'time', excludeWorldId = '') {
|
||||||
const results = [];
|
const results = [];
|
||||||
const whereClause =
|
const whereClause =
|
||||||
days > 0 ? `AND created_at >= datetime('now', @daysOffset)` : '';
|
days > 0 ? `AND created_at >= datetime('now', @daysOffset)` : '';
|
||||||
|
const excludeClause = excludeWorldId ? 'AND world_id != @excludeWorldId' : '';
|
||||||
const orderBy =
|
const orderBy =
|
||||||
sortBy === 'count' ? 'visit_count DESC' : 'total_time DESC';
|
sortBy === 'count' ? 'visit_count DESC' : 'total_time DESC';
|
||||||
const params = { '@limit': limit };
|
const params = { '@limit': limit };
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
params['@daysOffset'] = `-${days} days`;
|
params['@daysOffset'] = `-${days} days`;
|
||||||
}
|
}
|
||||||
|
if (excludeWorldId) {
|
||||||
|
params['@excludeWorldId'] = excludeWorldId;
|
||||||
|
}
|
||||||
await sqliteService.execute(
|
await sqliteService.execute(
|
||||||
(dbRow) => {
|
(dbRow) => {
|
||||||
results.push({
|
results.push({
|
||||||
@@ -1480,6 +1485,7 @@ const gameLog = {
|
|||||||
AND world_id != ''
|
AND world_id != ''
|
||||||
AND world_id LIKE 'wrld_%'
|
AND world_id LIKE 'wrld_%'
|
||||||
${whereClause}
|
${whereClause}
|
||||||
|
${excludeClause}
|
||||||
GROUP BY world_id
|
GROUP BY world_id
|
||||||
ORDER BY ${orderBy}
|
ORDER BY ${orderBy}
|
||||||
LIMIT @limit`,
|
LIMIT @limit`,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
|
||||||
|
const mocks = vi.hoisted(() => ({
|
||||||
|
getMyTopWorlds: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('../../services/database', () => ({
|
||||||
|
database: {
|
||||||
|
getMyTopWorlds: mocks.getMyTopWorlds
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
vi.mock('../../workers/activityWorkerRunner', () => ({
|
||||||
|
runActivityWorkerTask: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { useActivityStore } from '../activity';
|
||||||
|
|
||||||
|
describe('useActivityStore', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createPinia());
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('forwards excludeWorldId to top worlds query', async () => {
|
||||||
|
mocks.getMyTopWorlds.mockResolvedValue([{ worldId: 'wrld_1' }]);
|
||||||
|
const store = useActivityStore();
|
||||||
|
|
||||||
|
const result = await store.loadTopWorldsView({
|
||||||
|
userId: 'usr_me',
|
||||||
|
rangeDays: 30,
|
||||||
|
limit: 5,
|
||||||
|
sortBy: 'time',
|
||||||
|
excludeWorldId: 'wrld_home'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual([{ worldId: 'wrld_1' }]);
|
||||||
|
expect(mocks.getMyTopWorlds).toHaveBeenCalledWith(30, 5, 'time', 'wrld_home');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -296,10 +296,10 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
|
|
||||||
async function loadTopWorlds(
|
async function loadTopWorlds(
|
||||||
userId,
|
userId,
|
||||||
{ rangeDays = 30, limit = 5, sortBy = 'time' }
|
{ rangeDays = 30, limit = 5, sortBy = 'time', excludeWorldId = '' }
|
||||||
) {
|
) {
|
||||||
void userId;
|
void userId;
|
||||||
return database.getMyTopWorlds(rangeDays, limit, sortBy);
|
return database.getMyTopWorlds(rangeDays, limit, sortBy, excludeWorldId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshActivity(userId, options) {
|
async function refreshActivity(userId, options) {
|
||||||
@@ -358,12 +358,14 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
userId,
|
userId,
|
||||||
rangeDays = 30,
|
rangeDays = 30,
|
||||||
limit = 5,
|
limit = 5,
|
||||||
sortBy = 'time'
|
sortBy = 'time',
|
||||||
|
excludeWorldId = ''
|
||||||
}) {
|
}) {
|
||||||
return loadTopWorlds(userId, {
|
return loadTopWorlds(userId, {
|
||||||
rangeDays,
|
rangeDays,
|
||||||
limit,
|
limit,
|
||||||
sortBy,
|
sortBy,
|
||||||
|
excludeWorldId,
|
||||||
isSelf: true
|
isSelf: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user