mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-07 06:56:04 +02:00
refactor top worlds
This commit is contained in:
@@ -136,14 +136,43 @@
|
|||||||
|
|
||||||
<div v-if="isSelf && hasAnyData" class="mt-4 border-t border-border pt-3">
|
<div v-if="isSelf && hasAnyData" class="mt-4 border-t border-border pt-3">
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<span class="text-sm font-medium">{{ t('dialog.user.activity.most_visited_worlds.header') }}</span>
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-medium">
|
||||||
|
{{ t('dialog.user.activity.most_visited_worlds.header') }}
|
||||||
|
</span>
|
||||||
|
<Spinner v-if="topWorldsLoading" class="h-3.5 w-3.5" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="topWorlds.length === 0 && !isLoading" class="text-sm text-muted-foreground py-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>
|
||||||
|
<Select v-model="topWorldsSortBy" :disabled="topWorldsLoading">
|
||||||
|
<SelectTrigger size="sm" class="w-32" @click.stop>
|
||||||
|
<SelectValue />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="time">{{
|
||||||
|
t('dialog.user.activity.most_visited_worlds.sort_by_time')
|
||||||
|
}}</SelectItem>
|
||||||
|
<SelectItem value="count">{{
|
||||||
|
t('dialog.user.activity.most_visited_worlds.sort_by_count')
|
||||||
|
}}</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="topWorldsLoading && topWorlds.length === 0"
|
||||||
|
class="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
||||||
|
<Spinner class="h-4 w-4" />
|
||||||
|
<span>{{ t('dialog.user.activity.most_visited_worlds.loading') }}</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="topWorlds.length === 0 && !isLoading"
|
||||||
|
class="text-sm text-muted-foreground py-2">
|
||||||
{{ t('dialog.user.activity.no_data_in_period') }}
|
{{ t('dialog.user.activity.no_data_in_period') }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-0.5">
|
<div v-else class="flex flex-col gap-0.5">
|
||||||
<button
|
<button
|
||||||
v-for="(world, index) in topWorlds"
|
v-for="(world, index) in sortedTopWorlds"
|
||||||
:key="world.worldId"
|
:key="world.worldId"
|
||||||
type="button"
|
type="button"
|
||||||
class="group flex w-full items-start gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-accent"
|
class="group flex w-full items-start gap-3 rounded-lg px-3 py-2 text-left transition-colors hover:bg-accent"
|
||||||
@@ -169,7 +198,13 @@
|
|||||||
<div class="flex items-baseline justify-between gap-2">
|
<div class="flex items-baseline justify-between gap-2">
|
||||||
<span class="truncate text-sm font-medium">{{ world.worldName }}</span>
|
<span class="truncate text-sm font-medium">{{ world.worldName }}</span>
|
||||||
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">
|
<span class="shrink-0 text-xs tabular-nums text-muted-foreground">
|
||||||
{{ formatWorldTime(world.totalTime) }}
|
{{
|
||||||
|
topWorldsSortBy === 'time'
|
||||||
|
? formatWorldTime(world.totalTime)
|
||||||
|
: t('dialog.user.activity.most_visited_worlds.visit_count_label', {
|
||||||
|
count: world.visitCount
|
||||||
|
})
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -178,7 +213,11 @@
|
|||||||
<div
|
<div
|
||||||
class="h-full rounded-full transition-all duration-500"
|
class="h-full rounded-full transition-all duration-500"
|
||||||
:class="isDarkMode ? 'bg-white/45' : 'bg-black/25'"
|
:class="isDarkMode ? 'bg-white/45' : 'bg-black/25'"
|
||||||
:style="{ width: getTopWorldBarWidth(world.totalTime) }" />
|
:style="{
|
||||||
|
width: getTopWorldBarWidth(
|
||||||
|
topWorldsSortBy === 'time' ? world.totalTime : world.visitCount
|
||||||
|
)
|
||||||
|
}" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -226,7 +265,9 @@
|
|||||||
const hasOverlapData = ref(false);
|
const hasOverlapData = ref(false);
|
||||||
const overlapPercent = ref(0);
|
const overlapPercent = ref(0);
|
||||||
const bestOverlapTime = ref('');
|
const bestOverlapTime = ref('');
|
||||||
|
const topWorldsLoading = ref(false);
|
||||||
const topWorlds = ref([]);
|
const topWorlds = ref([]);
|
||||||
|
const topWorldsSortBy = ref('time');
|
||||||
const excludeHoursEnabled = ref(false);
|
const excludeHoursEnabled = ref(false);
|
||||||
const excludeStartHour = ref('1');
|
const excludeStartHour = ref('1');
|
||||||
const excludeEndHour = ref('6');
|
const excludeEndHour = ref('6');
|
||||||
@@ -241,10 +282,12 @@
|
|||||||
|
|
||||||
let activeRequestId = 0;
|
let activeRequestId = 0;
|
||||||
let activeOverlapRequestId = 0;
|
let activeOverlapRequestId = 0;
|
||||||
|
let activeTopWorldsRequestId = 0;
|
||||||
let lastLoadedUserId = '';
|
let lastLoadedUserId = '';
|
||||||
const pendingWorldThumbnailFetches = new Set();
|
const pendingWorldThumbnailFetches = new Set();
|
||||||
|
|
||||||
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
const isSelf = computed(() => userDialog.value.id === currentUser.value.id);
|
||||||
|
const sortedTopWorlds = computed(() => topWorlds.value);
|
||||||
const dayLabels = computed(() => [
|
const dayLabels = computed(() => [
|
||||||
t('dialog.user.activity.days.sun'),
|
t('dialog.user.activity.days.sun'),
|
||||||
t('dialog.user.activity.days.mon'),
|
t('dialog.user.activity.days.mon'),
|
||||||
@@ -277,14 +320,45 @@
|
|||||||
hasOverlapData.value = false;
|
hasOverlapData.value = false;
|
||||||
overlapPercent.value = 0;
|
overlapPercent.value = 0;
|
||||||
bestOverlapTime.value = '';
|
bestOverlapTime.value = '';
|
||||||
|
topWorldsLoading.value = false;
|
||||||
topWorlds.value = [];
|
topWorlds.value = [];
|
||||||
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
|
||||||
activeRequestId++;
|
activeRequestId++;
|
||||||
activeOverlapRequestId++;
|
activeOverlapRequestId++;
|
||||||
|
activeTopWorldsRequestId++;
|
||||||
lastLoadedUserId = '';
|
lastLoadedUserId = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadTopWorldsSection({ userId, rangeDays, sortBy, period }) {
|
||||||
|
const requestId = ++activeTopWorldsRequestId;
|
||||||
|
topWorldsLoading.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await activityStore.loadTopWorldsView({
|
||||||
|
userId,
|
||||||
|
rangeDays,
|
||||||
|
limit: 5,
|
||||||
|
sortBy
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
requestId !== activeTopWorldsRequestId ||
|
||||||
|
userDialog.value.id !== userId ||
|
||||||
|
topWorldsSortBy.value !== sortBy ||
|
||||||
|
selectedPeriod.value !== period
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
topWorlds.value = result;
|
||||||
|
void fetchMissingTopWorldThumbnails(topWorlds.value);
|
||||||
|
} finally {
|
||||||
|
if (requestId === activeTopWorldsRequestId) {
|
||||||
|
topWorldsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@@ -323,17 +397,20 @@
|
|||||||
if (!hasAnyData.value) {
|
if (!hasAnyData.value) {
|
||||||
hasOverlapData.value = false;
|
hasOverlapData.value = false;
|
||||||
topWorlds.value = [];
|
topWorlds.value = [];
|
||||||
|
topWorldsLoading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelf.value) {
|
if (isSelf.value) {
|
||||||
topWorlds.value = await activityStore.loadTopWorldsView({
|
await loadTopWorldsSection({
|
||||||
userId,
|
userId,
|
||||||
rangeDays,
|
rangeDays,
|
||||||
limit: 5,
|
sortBy: topWorldsSortBy.value,
|
||||||
forceRefresh
|
period: selectedPeriod.value
|
||||||
});
|
});
|
||||||
void fetchMissingTopWorldThumbnails(topWorlds.value);
|
if (requestId !== activeRequestId || userDialog.value.id !== userId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
hasOverlapData.value = false;
|
hasOverlapData.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -486,15 +563,16 @@
|
|||||||
return timeToText(ms);
|
return timeToText(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTopWorldBarWidth(totalTime) {
|
function getTopWorldBarWidth(value) {
|
||||||
if (topWorlds.value.length === 0) {
|
if (sortedTopWorlds.value.length === 0) {
|
||||||
return '0%';
|
return '0%';
|
||||||
}
|
}
|
||||||
const maxTime = Math.max(...topWorlds.value.map((world) => world.totalTime || 0), 0);
|
const key = topWorldsSortBy.value === 'count' ? 'visitCount' : 'totalTime';
|
||||||
if (maxTime <= 0) {
|
const maxVal = Math.max(...sortedTopWorlds.value.map((world) => world[key] || 0), 0);
|
||||||
|
if (maxVal <= 0) {
|
||||||
return '0%';
|
return '0%';
|
||||||
}
|
}
|
||||||
return `${Math.max((totalTime / maxTime) * 100, 8)}%`;
|
return `${Math.max((value / maxVal) * 100, 8)}%`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const activityChartRef = ref(null);
|
const activityChartRef = ref(null);
|
||||||
@@ -664,6 +742,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
watch(
|
||||||
|
() => topWorldsSortBy.value,
|
||||||
|
async (newSortBy) => {
|
||||||
|
if (!isSelf.value || !hasAnyData.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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(
|
watch(
|
||||||
() => mainHeatmapView.value,
|
() => mainHeatmapView.value,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"reset": "Reset",
|
"reset": "Reset",
|
||||||
"view_details": "View Details"
|
"view_details": "View Details"
|
||||||
},
|
},
|
||||||
|
"sort_by": "Sort by:",
|
||||||
"time_units": {
|
"time_units": {
|
||||||
"y": "y",
|
"y": "y",
|
||||||
"d": "d",
|
"d": "d",
|
||||||
@@ -1460,7 +1461,11 @@
|
|||||||
"minutes_overlap": "min overlap"
|
"minutes_overlap": "min overlap"
|
||||||
},
|
},
|
||||||
"most_visited_worlds": {
|
"most_visited_worlds": {
|
||||||
"header": "Most Visited Worlds"
|
"header": "Most Visited Worlds",
|
||||||
|
"loading": "Loading most visited worlds...",
|
||||||
|
"sort_by_time": "By Time",
|
||||||
|
"sort_by_count": "By Visits",
|
||||||
|
"visit_count_label": "{count} visits"
|
||||||
},
|
},
|
||||||
"preparing_data": "Preparing activity data...",
|
"preparing_data": "Preparing activity data...",
|
||||||
"preparing_data_hint": "This may take a moment on first load.",
|
"preparing_data_hint": "This may take a moment on first load.",
|
||||||
|
|||||||
@@ -28,10 +28,6 @@ function bucketCacheTable() {
|
|||||||
return `${dbVars.userPrefix}_activity_bucket_cache_v2`;
|
return `${dbVars.userPrefix}_activity_bucket_cache_v2`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function topWorldsCacheTable() {
|
|
||||||
return `${dbVars.userPrefix}_activity_top_worlds_cache_v2`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseJson(value, fallback) {
|
function parseJson(value, fallback) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return fallback;
|
return fallback;
|
||||||
@@ -52,8 +48,11 @@ const activityV2 = {
|
|||||||
ACTIVITY_RANGE_CACHE_KIND,
|
ACTIVITY_RANGE_CACHE_KIND,
|
||||||
|
|
||||||
async getActivitySourceSliceV2({ userId, isSelf, fromDays, toDays = 0 }) {
|
async getActivitySourceSliceV2({ userId, isSelf, fromDays, toDays = 0 }) {
|
||||||
const fromDateIso = new Date(Date.now() - fromDays * 86400000).toISOString();
|
const fromDateIso = new Date(
|
||||||
const toDateIso = toDays > 0
|
Date.now() - fromDays * 86400000
|
||||||
|
).toISOString();
|
||||||
|
const toDateIso =
|
||||||
|
toDays > 0
|
||||||
? new Date(Date.now() - toDays * 86400000).toISOString()
|
? new Date(Date.now() - toDays * 86400000).toISOString()
|
||||||
: '';
|
: '';
|
||||||
return isSelf
|
return isSelf
|
||||||
@@ -61,7 +60,12 @@ const activityV2 = {
|
|||||||
: this.getFriendPresenceSliceV2(userId, fromDateIso, toDateIso);
|
: this.getFriendPresenceSliceV2(userId, fromDateIso, toDateIso);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActivitySourceAfterV2({ userId, isSelf, afterCreatedAt, inclusive = false }) {
|
async getActivitySourceAfterV2({
|
||||||
|
userId,
|
||||||
|
isSelf,
|
||||||
|
afterCreatedAt,
|
||||||
|
inclusive = false
|
||||||
|
}) {
|
||||||
return isSelf
|
return isSelf
|
||||||
? this.getCurrentUserLocationAfterV2(afterCreatedAt, inclusive)
|
? this.getCurrentUserLocationAfterV2(afterCreatedAt, inclusive)
|
||||||
: this.getFriendPresenceAfterV2(userId, afterCreatedAt);
|
: this.getFriendPresenceAfterV2(userId, afterCreatedAt);
|
||||||
@@ -122,7 +126,9 @@ const activityV2 = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
return rows.sort((left, right) =>
|
||||||
|
left.created_at.localeCompare(right.created_at)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getFriendPresenceAfterV2(userId, afterCreatedAt) {
|
async getFriendPresenceAfterV2(userId, afterCreatedAt) {
|
||||||
@@ -167,7 +173,8 @@ const activityV2 = {
|
|||||||
FROM gamelog_location
|
FROM gamelog_location
|
||||||
WHERE created_at >= @fromDateIso
|
WHERE created_at >= @fromDateIso
|
||||||
${toDateIso ? 'AND created_at < @toDateIso' : ''}
|
${toDateIso ? 'AND created_at < @toDateIso' : ''}
|
||||||
${toDateIso
|
${
|
||||||
|
toDateIso
|
||||||
? `UNION ALL
|
? `UNION ALL
|
||||||
SELECT created_at, time, 2 AS sort_group
|
SELECT created_at, time, 2 AS sort_group
|
||||||
FROM (
|
FROM (
|
||||||
@@ -177,7 +184,8 @@ const activityV2 = {
|
|||||||
ORDER BY created_at
|
ORDER BY created_at
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
)`
|
)`
|
||||||
: ''}
|
: ''
|
||||||
|
}
|
||||||
)
|
)
|
||||||
ORDER BY created_at ASC, sort_group ASC
|
ORDER BY created_at ASC, sort_group ASC
|
||||||
`,
|
`,
|
||||||
@@ -214,7 +222,8 @@ const activityV2 = {
|
|||||||
updatedAt: dbRow[1] || '',
|
updatedAt: dbRow[1] || '',
|
||||||
isSelf: Boolean(dbRow[2]),
|
isSelf: Boolean(dbRow[2]),
|
||||||
sourceLastCreatedAt: dbRow[3] || '',
|
sourceLastCreatedAt: dbRow[3] || '',
|
||||||
pendingSessionStartAt: typeof dbRow[4] === 'number' ? dbRow[4] : null,
|
pendingSessionStartAt:
|
||||||
|
typeof dbRow[4] === 'number' ? dbRow[4] : null,
|
||||||
cachedRangeDays: dbRow[5] || 0
|
cachedRangeDays: dbRow[5] || 0
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -277,7 +286,11 @@ const activityV2 = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async appendActivitySessionsV2({ userId, sessions = [], replaceFromStartAt = null }) {
|
async appendActivitySessionsV2({
|
||||||
|
userId,
|
||||||
|
sessions = [],
|
||||||
|
replaceFromStartAt = null
|
||||||
|
}) {
|
||||||
await sqliteService.executeNonQuery('BEGIN');
|
await sqliteService.executeNonQuery('BEGIN');
|
||||||
try {
|
try {
|
||||||
if (replaceFromStartAt !== null) {
|
if (replaceFromStartAt !== null) {
|
||||||
@@ -391,86 +404,15 @@ const activityV2 = {
|
|||||||
'@bucketVersion': entry.bucketVersion || 1,
|
'@bucketVersion': entry.bucketVersion || 1,
|
||||||
'@builtFromCursor': entry.builtFromCursor || '',
|
'@builtFromCursor': entry.builtFromCursor || '',
|
||||||
'@rawBucketsJson': JSON.stringify(entry.rawBuckets || []),
|
'@rawBucketsJson': JSON.stringify(entry.rawBuckets || []),
|
||||||
'@normalizedBucketsJson': JSON.stringify(entry.normalizedBuckets || []),
|
'@normalizedBucketsJson': JSON.stringify(
|
||||||
|
entry.normalizedBuckets || []
|
||||||
|
),
|
||||||
'@summaryJson': JSON.stringify(entry.summary || {}),
|
'@summaryJson': JSON.stringify(entry.summary || {}),
|
||||||
'@builtAt': entry.builtAt || ''
|
'@builtAt': entry.builtAt || ''
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getActivityTopWorldsCacheV2(userId, rangeDays) {
|
|
||||||
const worlds = [];
|
|
||||||
let builtFromCursor = '';
|
|
||||||
let builtAt = '';
|
|
||||||
await sqliteService.execute(
|
|
||||||
(dbRow) => {
|
|
||||||
builtFromCursor = dbRow[0] || builtFromCursor;
|
|
||||||
builtAt = dbRow[1] || builtAt;
|
|
||||||
worlds.push({
|
|
||||||
worldId: dbRow[3],
|
|
||||||
worldName: dbRow[4],
|
|
||||||
visitCount: dbRow[5] || 0,
|
|
||||||
totalTime: dbRow[6] || 0
|
|
||||||
});
|
|
||||||
},
|
|
||||||
`SELECT built_from_cursor, built_at, rank_index, world_id, world_name, visit_count, total_time
|
|
||||||
FROM ${topWorldsCacheTable()}
|
|
||||||
WHERE user_id = @userId AND range_days = @rangeDays
|
|
||||||
ORDER BY rank_index`,
|
|
||||||
{
|
|
||||||
'@userId': userId,
|
|
||||||
'@rangeDays': rangeDays
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (worlds.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
userId,
|
|
||||||
rangeDays,
|
|
||||||
builtFromCursor,
|
|
||||||
builtAt,
|
|
||||||
worlds
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
async replaceActivityTopWorldsCacheV2(entry) {
|
|
||||||
await sqliteService.executeNonQuery('BEGIN');
|
|
||||||
try {
|
|
||||||
await sqliteService.executeNonQuery(
|
|
||||||
`DELETE FROM ${topWorldsCacheTable()} WHERE user_id = @userId AND range_days = @rangeDays`,
|
|
||||||
{
|
|
||||||
'@userId': entry.userId,
|
|
||||||
'@rangeDays': entry.rangeDays
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
for (let index = 0; index < entry.worlds.length; index++) {
|
|
||||||
const world = entry.worlds[index];
|
|
||||||
await sqliteService.executeNonQuery(
|
|
||||||
`INSERT OR REPLACE INTO ${topWorldsCacheTable()}
|
|
||||||
(user_id, range_days, rank_index, world_id, world_name, visit_count, total_time, built_from_cursor, built_at)
|
|
||||||
VALUES (@userId, @rangeDays, @rankIndex, @worldId, @worldName, @visitCount, @totalTime, @builtFromCursor, @builtAt)`,
|
|
||||||
{
|
|
||||||
'@userId': entry.userId,
|
|
||||||
'@rangeDays': entry.rangeDays,
|
|
||||||
'@rankIndex': index,
|
|
||||||
'@worldId': world.worldId,
|
|
||||||
'@worldName': world.worldName || world.worldId,
|
|
||||||
'@visitCount': world.visitCount || 0,
|
|
||||||
'@totalTime': world.totalTime || 0,
|
|
||||||
'@builtFromCursor': entry.builtFromCursor || '',
|
|
||||||
'@builtAt': entry.builtAt || ''
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await sqliteService.executeNonQuery('COMMIT');
|
|
||||||
} catch (error) {
|
|
||||||
await sqliteService.executeNonQuery('ROLLBACK');
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function insertSessions(userId, sessions = []) {
|
async function insertSessions(userId, sessions = []) {
|
||||||
@@ -479,7 +421,11 @@ async function insertSessions(userId, sessions = []) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chunkSize = 250;
|
const chunkSize = 250;
|
||||||
for (let chunkStart = 0; chunkStart < sessions.length; chunkStart += chunkSize) {
|
for (
|
||||||
|
let chunkStart = 0;
|
||||||
|
chunkStart < sessions.length;
|
||||||
|
chunkStart += chunkSize
|
||||||
|
) {
|
||||||
const chunk = sessions.slice(chunkStart, chunkStart + chunkSize);
|
const chunk = sessions.slice(chunkStart, chunkStart + chunkSize);
|
||||||
const args = {};
|
const args = {};
|
||||||
const values = chunk.map((session, index) => {
|
const values = chunk.map((session, index) => {
|
||||||
|
|||||||
@@ -1390,7 +1390,9 @@ const gameLog = {
|
|||||||
const where = [];
|
const where = [];
|
||||||
|
|
||||||
if (fromDays > 0) {
|
if (fromDays > 0) {
|
||||||
const fromDate = new Date(now.getTime() - fromDays * 86400000).toISOString();
|
const fromDate = new Date(
|
||||||
|
now.getTime() - fromDays * 86400000
|
||||||
|
).toISOString();
|
||||||
params['@fromDate'] = fromDate;
|
params['@fromDate'] = fromDate;
|
||||||
where.push('created_at >= @fromDate');
|
where.push('created_at >= @fromDate');
|
||||||
|
|
||||||
@@ -1403,12 +1405,15 @@ const gameLog = {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (toDays > 0) {
|
if (toDays > 0) {
|
||||||
const toDate = new Date(now.getTime() - toDays * 86400000).toISOString();
|
const toDate = new Date(
|
||||||
|
now.getTime() - toDays * 86400000
|
||||||
|
).toISOString();
|
||||||
params['@toDate'] = toDate;
|
params['@toDate'] = toDate;
|
||||||
where.push('created_at < @toDate');
|
where.push('created_at < @toDate');
|
||||||
}
|
}
|
||||||
|
|
||||||
const dateClause = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
const dateClause =
|
||||||
|
where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
||||||
await sqliteService.execute(
|
await sqliteService.execute(
|
||||||
(dbRow) => {
|
(dbRow) => {
|
||||||
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
data.push({ created_at: dbRow[0], time: dbRow[1] || 0 });
|
||||||
@@ -1443,12 +1448,15 @@ const gameLog = {
|
|||||||
* Groups by world_id and aggregates visit count and total time.
|
* Groups by world_id and aggregates visit count and total time.
|
||||||
* @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.
|
||||||
* @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) {
|
async getMyTopWorlds(days = 0, limit = 5, sortBy = 'time') {
|
||||||
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 orderBy =
|
||||||
|
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`;
|
||||||
@@ -1473,7 +1481,7 @@ const gameLog = {
|
|||||||
AND world_id LIKE 'wrld_%'
|
AND world_id LIKE 'wrld_%'
|
||||||
${whereClause}
|
${whereClause}
|
||||||
GROUP BY world_id
|
GROUP BY world_id
|
||||||
ORDER BY total_time DESC
|
ORDER BY ${orderBy}
|
||||||
LIMIT @limit`,
|
LIMIT @limit`,
|
||||||
params
|
params
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -128,20 +128,6 @@ const database = {
|
|||||||
PRIMARY KEY (user_id, target_user_id, range_days, view_kind, exclude_key)
|
PRIMARY KEY (user_id, target_user_id, range_days, view_kind, exclude_key)
|
||||||
)`
|
)`
|
||||||
);
|
);
|
||||||
await sqliteService.executeNonQuery(
|
|
||||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_activity_top_worlds_cache_v2 (
|
|
||||||
user_id TEXT NOT NULL,
|
|
||||||
range_days INTEGER NOT NULL,
|
|
||||||
rank_index INTEGER NOT NULL,
|
|
||||||
world_id TEXT NOT NULL,
|
|
||||||
world_name TEXT NOT NULL,
|
|
||||||
visit_count INTEGER NOT NULL DEFAULT 0,
|
|
||||||
total_time INTEGER NOT NULL DEFAULT 0,
|
|
||||||
built_from_cursor TEXT NOT NULL DEFAULT '',
|
|
||||||
built_at TEXT NOT NULL DEFAULT '',
|
|
||||||
PRIMARY KEY (user_id, range_days, rank_index)
|
|
||||||
)`
|
|
||||||
);
|
|
||||||
await sqliteService.executeNonQuery(
|
await sqliteService.executeNonQuery(
|
||||||
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_current (user_id TEXT PRIMARY KEY, display_name TEXT, trust_level TEXT, friend_number INTEGER)`
|
`CREATE TABLE IF NOT EXISTS ${dbVars.userPrefix}_friend_log_current (user_id TEXT PRIMARY KEY, display_name TEXT, trust_level TEXT, friend_number INTEGER)`
|
||||||
);
|
);
|
||||||
|
|||||||
+7
-52
@@ -41,8 +41,7 @@ function createSnapshot(userId, isSelf) {
|
|||||||
},
|
},
|
||||||
sessions: [],
|
sessions: [],
|
||||||
activityViews: new Map(),
|
activityViews: new Map(),
|
||||||
overlapViews: new Map(),
|
overlapViews: new Map()
|
||||||
topWorldsViews: new Map()
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +92,6 @@ function isUserInFlight(userId) {
|
|||||||
function clearDerivedViews(snapshot) {
|
function clearDerivedViews(snapshot) {
|
||||||
snapshot.activityViews.clear();
|
snapshot.activityViews.clear();
|
||||||
snapshot.overlapViews.clear();
|
snapshot.overlapViews.clear();
|
||||||
snapshot.topWorldsViews.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function overlapExcludeKey(excludeHours) {
|
function overlapExcludeKey(excludeHours) {
|
||||||
@@ -298,53 +296,10 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
|
|
||||||
async function loadTopWorlds(
|
async function loadTopWorlds(
|
||||||
userId,
|
userId,
|
||||||
{ rangeDays = 30, limit = 5, isSelf = true, forceRefresh = false }
|
{ rangeDays = 30, limit = 5, sortBy = 'time' }
|
||||||
) {
|
) {
|
||||||
const snapshot = await ensureSnapshot(userId, {
|
void userId;
|
||||||
isSelf,
|
return database.getMyTopWorlds(rangeDays, limit, sortBy);
|
||||||
rangeDays,
|
|
||||||
forceRefresh
|
|
||||||
});
|
|
||||||
const cacheKey = `${rangeDays}:${limit}`;
|
|
||||||
const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
|
|
||||||
|
|
||||||
let cached = snapshot.topWorldsViews.get(cacheKey);
|
|
||||||
if (!forceRefresh && cached?.builtFromCursor === currentCursor) {
|
|
||||||
return cached.worlds;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!forceRefresh) {
|
|
||||||
const persisted = await database.getActivityTopWorldsCacheV2(
|
|
||||||
userId,
|
|
||||||
rangeDays
|
|
||||||
);
|
|
||||||
if (persisted?.builtFromCursor === currentCursor) {
|
|
||||||
snapshot.topWorldsViews.set(cacheKey, persisted);
|
|
||||||
return persisted.worlds;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const worlds = await database.getMyTopWorlds(rangeDays, limit);
|
|
||||||
const entry = {
|
|
||||||
userId,
|
|
||||||
rangeDays,
|
|
||||||
worlds,
|
|
||||||
builtFromCursor: currentCursor,
|
|
||||||
builtAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
snapshot.topWorldsViews.set(cacheKey, entry);
|
|
||||||
deferWrite(() => database.replaceActivityTopWorldsCacheV2(entry));
|
|
||||||
deferWrite(() =>
|
|
||||||
database.upsertActivityRangeCacheV2({
|
|
||||||
userId,
|
|
||||||
rangeDays,
|
|
||||||
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.TOP_WORLDS,
|
|
||||||
isComplete: true,
|
|
||||||
builtFromCursor: currentCursor,
|
|
||||||
builtAt: entry.builtAt
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return worlds;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshActivity(userId, options) {
|
async function refreshActivity(userId, options) {
|
||||||
@@ -403,13 +358,13 @@ export const useActivityStore = defineStore('Activity', () => {
|
|||||||
userId,
|
userId,
|
||||||
rangeDays = 30,
|
rangeDays = 30,
|
||||||
limit = 5,
|
limit = 5,
|
||||||
forceRefresh = false
|
sortBy = 'time'
|
||||||
}) {
|
}) {
|
||||||
return loadTopWorlds(userId, {
|
return loadTopWorlds(userId, {
|
||||||
rangeDays,
|
rangeDays,
|
||||||
limit,
|
limit,
|
||||||
isSelf: true,
|
sortBy,
|
||||||
forceRefresh
|
isSelf: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user