diff --git a/src/components/dialogs/UserDialog/UserDialogActivityTab.vue b/src/components/dialogs/UserDialog/UserDialogActivityTab.vue
index 82f039ef..dbdf24e5 100644
--- a/src/components/dialogs/UserDialog/UserDialogActivityTab.vue
+++ b/src/components/dialogs/UserDialog/UserDialogActivityTab.vue
@@ -142,21 +142,34 @@
-
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 dayLabels = computed(() => [
t('dialog.user.activity.days.sun'),
@@ -330,6 +351,7 @@
topWorldsLoading.value = false;
topWorldsLoadingVisible.value = false;
topWorlds.value = [];
+ excludeHomeWorldEnabled.value = false;
isOverlapLoading.value = false;
isOverlapLoadingVisible.value = false;
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
@@ -427,7 +449,8 @@
userId,
rangeDays,
limit: 5,
- sortBy
+ sortBy,
+ excludeWorldId: excludeHomeWorldEnabled.value ? currentHomeWorldId.value : ''
});
if (
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 } = {}) {
const userId = userDialog.value.id;
if (!userId) {
@@ -589,6 +627,11 @@
await refreshOverlapOnly();
}
+ async function onExcludeHomeWorldToggle(value) {
+ excludeHomeWorldEnabled.value = value;
+ await refreshTopWorldsOnly();
+ }
+
async function fetchMissingTopWorldThumbnails(worlds) {
const missingWorldIds = worlds
.map((world) => world.worldId)
@@ -819,22 +862,16 @@
);
watch(
() => topWorldsSortBy.value,
- async (newSortBy) => {
- if (!isSelf.value || !hasAnyData.value) {
- return;
+ () => {
+ void refreshTopWorldsOnly();
+ }
+ );
+ watch(
+ () => currentUser.value.homeLocation,
+ () => {
+ if (excludeHomeWorldEnabled.value) {
+ void refreshTopWorldsOnly();
}
- const userId = userDialog.value.id;
- if (!userId) {
- return;
- }
- const period = selectedPeriod.value;
- const rangeDays = parseInt(period, 10) || 30;
- await loadTopWorldsSection({
- userId,
- rangeDays,
- sortBy: newSortBy,
- period
- });
}
);
watch(
diff --git a/src/localization/en.json b/src/localization/en.json
index 44d11b30..6f59cb93 100644
--- a/src/localization/en.json
+++ b/src/localization/en.json
@@ -1463,6 +1463,7 @@
"most_visited_worlds": {
"header": "Most Visited Worlds",
"loading": "Loading most visited worlds...",
+ "exclude_home_world": "Exclude home world",
"sort_by_time": "By Time",
"sort_by_count": "By Visits",
"visit_count_label": "{count} visits"
diff --git a/src/services/database/__tests__/gameLog.test.js b/src/services/database/__tests__/gameLog.test.js
new file mode 100644
index 00000000..22bf0b48
--- /dev/null
+++ b/src/services/database/__tests__/gameLog.test.js
@@ -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'
+ });
+ });
+});
diff --git a/src/services/database/gameLog.js b/src/services/database/gameLog.js
index 7e997c2e..c03dc88e 100644
--- a/src/services/database/gameLog.js
+++ b/src/services/database/gameLog.js
@@ -1449,18 +1449,23 @@ const gameLog = {
* @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 {'time'|'count'} [sortBy='time'] - Sort by total time or visit count.
+ * @param {string} [excludeWorldId=''] - Optional world ID to exclude from results.
* @returns {Promise
>}
*/
- async getMyTopWorlds(days = 0, limit = 5, sortBy = 'time') {
+ async getMyTopWorlds(days = 0, limit = 5, sortBy = 'time', excludeWorldId = '') {
const results = [];
const whereClause =
days > 0 ? `AND created_at >= datetime('now', @daysOffset)` : '';
+ const excludeClause = excludeWorldId ? 'AND world_id != @excludeWorldId' : '';
const orderBy =
sortBy === 'count' ? 'visit_count DESC' : 'total_time DESC';
const params = { '@limit': limit };
if (days > 0) {
params['@daysOffset'] = `-${days} days`;
}
+ if (excludeWorldId) {
+ params['@excludeWorldId'] = excludeWorldId;
+ }
await sqliteService.execute(
(dbRow) => {
results.push({
@@ -1480,6 +1485,7 @@ const gameLog = {
AND world_id != ''
AND world_id LIKE 'wrld_%'
${whereClause}
+ ${excludeClause}
GROUP BY world_id
ORDER BY ${orderBy}
LIMIT @limit`,
diff --git a/src/stores/__tests__/activity.test.js b/src/stores/__tests__/activity.test.js
new file mode 100644
index 00000000..e9fc4ec1
--- /dev/null
+++ b/src/stores/__tests__/activity.test.js
@@ -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');
+ });
+});
diff --git a/src/stores/activity.js b/src/stores/activity.js
index 6ef961a3..a75f627c 100644
--- a/src/stores/activity.js
+++ b/src/stores/activity.js
@@ -296,10 +296,10 @@ export const useActivityStore = defineStore('Activity', () => {
async function loadTopWorlds(
userId,
- { rangeDays = 30, limit = 5, sortBy = 'time' }
+ { rangeDays = 30, limit = 5, sortBy = 'time', excludeWorldId = '' }
) {
void userId;
- return database.getMyTopWorlds(rangeDays, limit, sortBy);
+ return database.getMyTopWorlds(rangeDays, limit, sortBy, excludeWorldId);
}
async function refreshActivity(userId, options) {
@@ -358,12 +358,14 @@ export const useActivityStore = defineStore('Activity', () => {
userId,
rangeDays = 30,
limit = 5,
- sortBy = 'time'
+ sortBy = 'time',
+ excludeWorldId = ''
}) {
return loadTopWorlds(userId, {
rangeDays,
limit,
sortBy,
+ excludeWorldId,
isSelf: true
});
}