mirror of
https://github.com/rommapp/romm.git
synced 2026-06-30 07:45:52 +00:00
add playtime tracking via emujs
This commit is contained in:
5
frontend/src/__generated__/index.ts
generated
5
frontend/src/__generated__/index.ts
generated
@@ -80,6 +80,11 @@ export type { OIDCDict } from './models/OIDCDict';
|
||||
export type { OIDCLogoutResponse } from './models/OIDCLogoutResponse';
|
||||
export type { PlatformBindingPayload } from './models/PlatformBindingPayload';
|
||||
export type { PlatformSchema } from './models/PlatformSchema';
|
||||
export type { PlaySessionEntry } from './models/PlaySessionEntry';
|
||||
export type { PlaySessionIngestPayload } from './models/PlaySessionIngestPayload';
|
||||
export type { PlaySessionIngestResponse } from './models/PlaySessionIngestResponse';
|
||||
export type { PlaySessionIngestResult } from './models/PlaySessionIngestResult';
|
||||
export type { PlaySessionSchema } from './models/PlaySessionSchema';
|
||||
export type { RAGameRomAchievement } from './models/RAGameRomAchievement';
|
||||
export type { RAProgression } from './models/RAProgression';
|
||||
export type { RAUserGameProgression } from './models/RAUserGameProgression';
|
||||
|
||||
12
frontend/src/__generated__/models/PlaySessionEntry.ts
generated
Normal file
12
frontend/src/__generated__/models/PlaySessionEntry.ts
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PlaySessionEntry = {
|
||||
rom_id?: (number | null);
|
||||
save_slot?: (string | null);
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
duration_ms: number;
|
||||
};
|
||||
|
||||
10
frontend/src/__generated__/models/PlaySessionIngestPayload.ts
generated
Normal file
10
frontend/src/__generated__/models/PlaySessionIngestPayload.ts
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlaySessionEntry } from './PlaySessionEntry';
|
||||
export type PlaySessionIngestPayload = {
|
||||
device_id?: (string | null);
|
||||
sessions: Array<PlaySessionEntry>;
|
||||
};
|
||||
|
||||
11
frontend/src/__generated__/models/PlaySessionIngestResponse.ts
generated
Normal file
11
frontend/src/__generated__/models/PlaySessionIngestResponse.ts
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlaySessionIngestResult } from './PlaySessionIngestResult';
|
||||
export type PlaySessionIngestResponse = {
|
||||
results: Array<PlaySessionIngestResult>;
|
||||
created_count: number;
|
||||
skipped_count: number;
|
||||
};
|
||||
|
||||
11
frontend/src/__generated__/models/PlaySessionIngestResult.ts
generated
Normal file
11
frontend/src/__generated__/models/PlaySessionIngestResult.ts
generated
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PlaySessionIngestResult = {
|
||||
index: number;
|
||||
status: 'created' | 'duplicate' | 'error';
|
||||
id?: (number | null);
|
||||
detail?: (string | null);
|
||||
};
|
||||
|
||||
17
frontend/src/__generated__/models/PlaySessionSchema.ts
generated
Normal file
17
frontend/src/__generated__/models/PlaySessionSchema.ts
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type PlaySessionSchema = {
|
||||
id: number;
|
||||
user_id: number;
|
||||
device_id: (string | null);
|
||||
rom_id: (number | null);
|
||||
save_slot: (string | null);
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
duration_ms: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import { useThemeAssets } from "@/console/composables/useThemeAssets";
|
||||
import { ROUTES } from "@/plugins/router";
|
||||
import api from "@/services/api";
|
||||
import firmwareApi from "@/services/api/firmware";
|
||||
import playSessionApi from "@/services/api/play-session";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeConfig from "@/stores/config";
|
||||
import storeLanguage from "@/stores/language";
|
||||
@@ -72,8 +73,6 @@ const loaderStatus = ref<
|
||||
"idle" | "loading-local" | "loading-cdn" | "loaded" | "failed"
|
||||
>("idle");
|
||||
|
||||
let pausedByPrompt = false;
|
||||
|
||||
const exitOptions = computed(() => [
|
||||
{
|
||||
id: "save",
|
||||
@@ -94,16 +93,50 @@ const exitOptions = computed(() => [
|
||||
|
||||
const { subscribe } = useInputScope();
|
||||
let exitScopeOff: (() => void) | null = null;
|
||||
let pausedByPrompt = false;
|
||||
let sessionStartTime: Date | null = null;
|
||||
let requestedAnimationFrame: number | null = null;
|
||||
let lastPressedKeys: Record<number, number> = { 8: 0, 9: 0 };
|
||||
|
||||
const INVALID_CHARS_REGEX = /[#<$+%>!`&*'|{}/\\?"=@:^\r\n]/gi;
|
||||
|
||||
function immediateExit() {
|
||||
router
|
||||
.push({ name: ROUTES.CONSOLE_ROM, params: { rom: romId } })
|
||||
.catch((error) => {
|
||||
console.error("Error navigating to console rom", error);
|
||||
if (!sessionStartTime || !romRef.value) {
|
||||
return router
|
||||
.push({ name: ROUTES.CONSOLE_ROM, params: { rom: romId } })
|
||||
.catch((error) => {
|
||||
console.error("Error navigating to console rom", error);
|
||||
});
|
||||
}
|
||||
|
||||
const endTime = new Date();
|
||||
const durationMs = endTime.getTime() - sessionStartTime.getTime();
|
||||
if (durationMs < 1000) {
|
||||
// Don't log sessions under 1s, likely accidental opens
|
||||
console.info("Play session too short, not logging");
|
||||
return;
|
||||
}
|
||||
|
||||
playSessionApi
|
||||
.ingestPlaySessions({
|
||||
sessions: [
|
||||
{
|
||||
rom_id: romRef.value.id,
|
||||
start_time: sessionStartTime.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
duration_ms: durationMs,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((err) => console.error("Failed to submit play session:", err))
|
||||
.finally(() => {
|
||||
sessionStartTime = null;
|
||||
|
||||
router
|
||||
.push({ name: ROUTES.CONSOLE_ROM, params: { rom: romId } })
|
||||
.catch((error) => {
|
||||
console.error("Error navigating to console rom", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -527,6 +560,7 @@ async function boot() {
|
||||
|
||||
// Ensure a controller is auto-assigned to Player 1 when available
|
||||
window.EJS_onGameStart = () => {
|
||||
sessionStartTime = new Date();
|
||||
if (!window.EJS_emulator) return;
|
||||
const waitForGameManager = async () => {
|
||||
const deadline = Date.now() + 5000; // 5s timeout
|
||||
@@ -700,9 +734,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="play-root fixed inset-0 bg-black text-white z-[70] overflow-hidden"
|
||||
>
|
||||
<div class="play-root fixed inset-0 bg-black text-white z-70 overflow-hidden">
|
||||
<div id="game" class="w-full h-full" />
|
||||
<div
|
||||
v-if="bezelSrc"
|
||||
@@ -741,7 +773,7 @@ onBeforeUnmount(() => {
|
||||
<div class="text-red-300 font-medium">
|
||||
{{ t("console.emulator-failed") }}
|
||||
</div>
|
||||
<div class="mt-1 text-[11px] max-w-xs leading-snug break-words">
|
||||
<div class="mt-1 text-[11px] max-w-xs leading-snug wrap-break-word">
|
||||
{{ loaderError }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -771,7 +803,7 @@ onBeforeUnmount(() => {
|
||||
borderColor: 'var(--console-modal-border)',
|
||||
boxShadow: 'var(--console-modal-shadow)',
|
||||
}"
|
||||
class="relative w-full max-w-[560px] mx-auto rounded-2xl pa-10 md:p-9 flex flex-col gap-6 focus:outline-none border"
|
||||
class="relative w-full max-w-140 mx-auto rounded-2xl pa-10 md:p-9 flex flex-col gap-6 focus:outline-none border"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2
|
||||
@@ -799,7 +831,7 @@ onBeforeUnmount(() => {
|
||||
? 'opacity-40 cursor-not-allowed'
|
||||
: '',
|
||||
focusedExitIndex === i
|
||||
? 'shadow-[0_0_0_2px_var(--console-modal-tile-selected-border),_0_0_18px_-4px_var(--console-modal-tile-selected-border)]'
|
||||
? 'shadow-[0_0_0_2px_var(--console-modal-tile-selected-border),0_0_18px_-4px_var(--console-modal-tile-selected-border)]'
|
||||
: '',
|
||||
]"
|
||||
:style="
|
||||
|
||||
21
frontend/src/services/api/play-session.ts
Normal file
21
frontend/src/services/api/play-session.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import api from "@/services/api";
|
||||
|
||||
async function ingestPlaySessions({
|
||||
deviceId = null,
|
||||
sessions,
|
||||
}: {
|
||||
deviceId?: string | null;
|
||||
sessions: {
|
||||
rom_id: number;
|
||||
start_time: string;
|
||||
end_time: string;
|
||||
duration_ms: number;
|
||||
}[];
|
||||
}) {
|
||||
return api.post("/play-sessions", {
|
||||
device_id: deviceId,
|
||||
sessions,
|
||||
});
|
||||
}
|
||||
|
||||
export default { ingestPlaySessions };
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
NetplayICEServer,
|
||||
} from "@/__generated__";
|
||||
import { ROUTES } from "@/plugins/router";
|
||||
import playSessionApi from "@/services/api/play-session";
|
||||
import { saveApi as api } from "@/services/api/save";
|
||||
import storeConfig from "@/stores/config";
|
||||
import storeLanguage from "@/stores/language";
|
||||
@@ -51,6 +52,7 @@ const props = defineProps<{
|
||||
}>();
|
||||
const romRef = ref<DetailedRom>(props.rom);
|
||||
const saveRef = ref<SaveSchema | null>(props.save);
|
||||
const sessionStartTime = ref<Date | null>(null);
|
||||
const theme = useTheme();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const { playing, fullScreen } = storeToRefs(playingStore);
|
||||
@@ -348,6 +350,7 @@ window.EJS_onSaveState = async function ({
|
||||
};
|
||||
|
||||
window.EJS_onGameStart = async () => {
|
||||
sessionStartTime.value = new Date();
|
||||
setTimeout(async () => {
|
||||
if (props.save) await loadSave(props.save);
|
||||
if (props.state) await loadState(props.state);
|
||||
@@ -434,10 +437,36 @@ window.EJS_onGameStart = async () => {
|
||||
};
|
||||
|
||||
function immediateExit() {
|
||||
router
|
||||
.push({ name: ROUTES.ROM, params: { rom: romRef.value.id } })
|
||||
.catch((error) => {
|
||||
console.error("Error navigating to console rom", error);
|
||||
if (!sessionStartTime.value) {
|
||||
return router
|
||||
.push({ name: ROUTES.ROM, params: { rom: romRef.value.id } })
|
||||
.catch((error) => {
|
||||
console.error("Error navigating to console rom", error);
|
||||
});
|
||||
}
|
||||
|
||||
const endTime = new Date();
|
||||
const durationMs = endTime.getTime() - sessionStartTime.value.getTime();
|
||||
|
||||
playSessionApi
|
||||
.ingestPlaySessions({
|
||||
sessions: [
|
||||
{
|
||||
rom_id: romRef.value.id,
|
||||
start_time: sessionStartTime.value.toISOString(),
|
||||
end_time: endTime.toISOString(),
|
||||
duration_ms: durationMs,
|
||||
},
|
||||
],
|
||||
})
|
||||
.catch((err) => console.error("Failed to submit play session:", err))
|
||||
.finally(() => {
|
||||
sessionStartTime.value = null;
|
||||
router
|
||||
.push({ name: ROUTES.ROM, params: { rom: romRef.value.id } })
|
||||
.catch((error) => {
|
||||
console.error("Error navigating to console rom", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user