Merge remote-tracking branch 'upstream/master' into console-mode

This commit is contained in:
Spencer Kuzara
2025-08-24 11:23:59 +09:00
14 changed files with 220 additions and 61 deletions

View File

@@ -8,6 +8,29 @@ Thank you for considering contributing to RomM! This document outlines some guid
Please note that this project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md). By participating in this project, you are expected to uphold this code.
## AI Assistance Notice
> [!IMPORTANT]
>
> If you are using **any kind of AI assistance** to contribute to RomM, it must be disclosed in the pull request.
If you are using any kind of AI assistance while contributing to RomM **this must be disclosed in the pull request**, along with the extent to which AI assistance was used (e.g. docs only vs. code generation). If PR responses are being generated by an AI, disclose that as well. As a small exception, trivial tab-completion doesn't need to be disclosed.
An example disclosure:
> This PR was written primarily by Claude Code.
Or a more detailed disclosure:
> I consulted ChatGPT to understand the codebase but the solution
> was fully authored manually by myself.
Failure to disclose this is rude to the human operators on the other end of the pull request, but it also makes it difficult to determine how much scrutiny to apply to the contribution.
In a perfect world, AI assistance would produce equal or higher quality work than any human. That isn't the world we live in today, and in most cases it's generating slop.
Please be respectful to maintainers and disclose AI assistance.
## Contributing to the Docs
If you would like to contribute to the project's [documentation](https://docs.romm.app), open a pull request against [the docs repo](https://github.com/rommapp/docs). We welcome any contributions that help improve the documentation (new pages, updates, or corrections).

View File

@@ -4,7 +4,7 @@ from functools import cached_property
from typing import TYPE_CHECKING
from models.base import BaseModel
from models.rom import Rom, RomFile
from models.rom import Rom
from sqlalchemy import String, func, select
from sqlalchemy.orm import Mapped, column_property, mapped_column, relationship
@@ -59,10 +59,9 @@ class Platform(BaseModel):
)
fs_size_bytes: Mapped[int] = column_property(
select(func.coalesce(func.sum(RomFile.file_size_bytes), 0))
.select_from(RomFile.__table__.join(Rom.__table__, RomFile.rom_id == Rom.id))
select(func.coalesce(func.sum(Rom.fs_size_bytes), 0))
.where(Rom.platform_id == id)
.correlate_except(RomFile, Rom)
.correlate_except(Rom)
.scalar_subquery()
)

View File

@@ -15,7 +15,54 @@
</head>
<body>
<div id="app"></div>
<div id="app">
<div id="app-loading-logo">
<img
src="/assets/logos/romm_logo_xbox_one_circle_grayscale.svg"
alt="Romm Logo"
/>
</div>
</div>
<style>
#app-loading-logo {
position: fixed;
top: 0;
left: 0;
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
z-index: 1020 !important;
background-color: rgba(242, 244, 248) !important;
}
#app-loading-logo img {
width: 30%;
height: 30%;
opacity: 0.1;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: rgba(13, 17, 23) !important;
}
#app-loading-logo {
background-color: rgba(13, 17, 23) !important;
}
}
@media (max-width: 960px) {
#app-loading-logo img {
width: 50%;
height: 50%;
}
}
</style>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@@ -19,7 +19,18 @@ storeLanguage.setLanguage(selectedLanguage.value);
<template>
<v-app>
<v-main id="main" class="no-transition">
<router-view />
<router-view v-slot="{ Component }">
<component :is="Component" />
<!-- Fade out the app loading logo -->
<Transition name="fade" mode="out-in">
<div v-if="!Component" id="app-loading-logo">
<img
src="/assets/logos/romm_logo_xbox_one_circle_grayscale.svg"
alt="Romm Logo"
/>
</div>
</Transition>
</router-view>
</v-main>
</v-app>
</template>
@@ -28,4 +39,14 @@ storeLanguage.setLanguage(selectedLanguage.value);
#main.no-transition {
transition: none;
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.35s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@@ -90,7 +90,7 @@ watch(
height="100%"
class="position-fixed bg-surface mt-4 char-index-toolbar"
:style="{
'max-height': calculatedHeight,
height: calculatedHeight,
}"
>
<v-tabs

View File

@@ -2,6 +2,8 @@
import storePlatforms from "@/stores/platforms";
import storeGalleryView from "@/stores/galleryView";
import { computed } from "vue";
import { useDisplay } from "vuetify";
import { isNull } from "lodash";
const props = withDefaults(
defineProps<{
@@ -12,13 +14,22 @@ const props = withDefaults(
{
platformId: undefined,
aspectRatio: undefined,
type: "image, avatar, chip, chip",
type: "image, avatar, chip, chip, actions",
},
);
const { smAndDown } = useDisplay();
const platformsStore = storePlatforms();
const galleryViewStore = storeGalleryView();
const showActionBarAlways = isNull(
localStorage.getItem("settings.showActionBar"),
)
? false
: localStorage.getItem("settings.showActionBar") === "true";
const showActionBar = computed(() => smAndDown.value || showActionBarAlways);
const computedAspectRatio = computed(() => {
const ratio =
props.aspectRatio ||
@@ -31,12 +42,17 @@ const computedAspectRatio = computed(() => {
<template>
<v-skeleton-loader
class="card-skeleton"
:class="{ 'show-action-bar': showActionBar }"
:type="type"
:style="{ aspectRatio: computedAspectRatio }"
/>
</template>
<style>
.card-skeleton {
border-radius: 4px;
}
.card-skeleton .v-skeleton-loader__card-avatar {
height: 100%;
}
@@ -68,6 +84,7 @@ const computedAspectRatio = computed(() => {
padding: 0 12px;
height: 24px;
margin: 4px;
margin-inline-start: 4px !important;
&:after {
animation: none;
@@ -83,4 +100,33 @@ const computedAspectRatio = computed(() => {
top: 0;
left: 0;
}
.card-skeleton .v-skeleton-loader__actions {
display: none;
}
.card-skeleton.show-action-bar .v-skeleton-loader__button {
height: 24px;
margin: 4px;
flex: 1;
}
.card-skeleton.show-action-bar {
.v-skeleton-loader__image {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.v-skeleton-loader__avatar,
.v-skeleton-loader__chip:nth-of-type(3) {
bottom: 32px;
}
.v-skeleton-loader__actions {
display: flex;
flex-wrap: nowrap;
}
}
</style>

View File

@@ -2,7 +2,6 @@
import storeHeartbeat from "@/stores/heartbeat";
import semver from "semver";
import { onMounted, ref } from "vue";
import { useDisplay } from "vuetify";
const heartbeat = storeHeartbeat();
const { VERSION } = heartbeat.value.SYSTEM;
@@ -13,13 +12,15 @@ function dismissVersionBanner() {
localStorage.setItem("dismissedVersion", GITHUB_VERSION.value);
latestVersionDismissed.value = true;
}
onMounted(async () => {
async function fetchLatestVersion() {
try {
const response = await fetch(
"https://api.github.com/repos/rommapp/romm/releases/latest",
);
const json = await response.json();
GITHUB_VERSION.value = json.tag_name;
const publishedAt = new Date(json.published_at);
latestVersionDismissed.value =
// Hide if the version is not valid
@@ -31,6 +32,12 @@ onMounted(async () => {
} catch (error) {
console.error("Failed to fetch latest version from Github", error);
}
document.removeEventListener("network-quiesced", fetchLatestVersion);
}
onMounted(async () => {
document.addEventListener("network-quiesced", fetchLatestVersion);
});
</script>

View File

@@ -29,6 +29,7 @@ import { isNull } from "lodash";
const navigationStore = storeNavigation();
const platformsStore = storePlatforms();
const collectionsStore = storeCollections();
const emitter = inject<Emitter<Events>>("emitter");
emitter?.on("refreshDrawer", async () => {
platformsStore.fetchPlatforms();
@@ -49,21 +50,25 @@ const virtualCollectionTypeRef = ref(
: storedVirtualCollectionType,
);
onBeforeMount(async () => {
await Promise.all([
platformsStore.fetchPlatforms(),
collectionsStore.fetchCollections(),
collectionsStore.fetchSmartCollections(),
showVirtualCollections
? collectionsStore.fetchVirtualCollections(virtualCollectionTypeRef.value)
: Promise.resolve(),
]);
navigationStore.reset();
function unhackNavbar() {
document.removeEventListener("network-quiesced", unhackNavbar);
// Hack to prevent main page transition on first load
const main = document.getElementById("main");
if (main) main.classList.remove("no-transition");
}
onBeforeMount(async () => {
document.addEventListener("network-quiesced", unhackNavbar);
platformsStore.fetchPlatforms();
collectionsStore.fetchCollections();
collectionsStore.fetchSmartCollections();
if (showVirtualCollections) {
collectionsStore.fetchVirtualCollections(virtualCollectionTypeRef.value);
}
navigationStore.reset();
});
</script>

View File

@@ -21,18 +21,22 @@ async function initializeData() {
try {
const { data: heartbeatData } = await api.get("/heartbeat");
heartbeat.set(heartbeatData);
} catch (heartbeatError) {
console.error("Error fetching heartbeat: ", heartbeatError);
}
try {
const { data: userData } = await userApi.fetchCurrentUser();
auth.setUser(userData);
} catch (userError) {
console.error("Error loading user: ", userError);
}
try {
const { data: userData } = await userApi.fetchCurrentUser();
auth.setUser(userData);
} catch (userError) {
console.error("Error loading user: ", userError);
}
try {
const { data: configData } = await api.get("/config");
configStore.set(configData);
} catch (error) {
console.error("Error during initialization: ", error);
} catch (configError) {
console.error("Error fetching config: ", configError);
}
}

View File

@@ -201,6 +201,32 @@ export default defineStore("roms", {
});
});
},
fetchRecentRoms(): Promise<SimpleRom[]> {
return new Promise((resolve, reject) => {
romApi
.getRecentRoms()
.then(({ data: { items } }) => {
this.setRecentRoms(items);
resolve(items);
})
.catch((error) => {
reject(error);
});
});
},
fetchContinuePlayingRoms(): Promise<SimpleRom[]> {
return new Promise((resolve, reject) => {
romApi
.getRecentPlayedRoms()
.then(({ data: { items } }) => {
this.setContinuePlayingRoms(items);
resolve(items);
})
.catch((error) => {
reject(error);
});
});
},
add(roms: SimpleRom[]) {
this.allRoms = this.allRoms.concat(roms);
},

View File

@@ -2,6 +2,7 @@ html,
body {
background-color: rgba(var(--v-theme-background)) !important;
overflow-y: auto;
margin: 0 !important;
}
.main-layout {
z-index: 1010 !important;

View File

@@ -299,12 +299,12 @@ onBeforeUnmount(() => {
</v-row>
<load-more-btn :fetchRoms="fetchRoms" />
<fab-overlay />
</template>
<template v-else>
<empty-game v-if="filteredPlatforms.length > 0 && !fetchingRoms" />
</template>
</template>
<fab-overlay />
</template>
<empty-platform v-else />

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, computed } from "vue";
import { onBeforeMount, ref, computed } from "vue";
import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
import Stats from "@/components/Home/Stats.vue";
@@ -10,7 +10,6 @@ import RecentAddedSkeleton from "@/components/Home/RecentAddedSkeleton.vue";
import RecentAdded from "@/components/Home/RecentAdded.vue";
import ContinuePlaying from "@/components/Home/ContinuePlaying.vue";
import EmptyHome from "@/components/Home/EmptyHome.vue";
import romApi from "@/services/api/rom";
import storeCollections from "@/stores/collections";
import storePlatforms from "@/stores/platforms";
import storeRoms from "@/stores/roms";
@@ -62,37 +61,17 @@ const isEmpty = computed(
filteredSmartCollections.value.length === 0,
);
const fetchRecentRoms = async (): Promise<void> => {
try {
fetchingRecentAdded.value = true;
const {
data: { items },
} = await romApi.getRecentRoms();
romsStore.setRecentRoms(items);
} catch (error) {
console.error("Failed to fetch recent ROMs:", error);
} finally {
fetchingRecentAdded.value = false;
}
};
onBeforeMount(async () => {
fetchingRecentAdded.value = true;
fetchingContinuePlaying.value = true;
const fetchContinuePlayingRoms = async (): Promise<void> => {
try {
fetchingContinuePlaying.value = true;
const {
data: { items },
} = await romApi.getRecentPlayedRoms();
const filteredItems = items.filter((rom) => rom.rom_user.last_played);
romsStore.setContinuePlayingRoms(filteredItems);
} catch (error) {
console.error("Failed to fetch continue playing ROMs:", error);
} finally {
fetchingContinuePlaying.value = false;
}
};
await Promise.all([
romsStore.fetchRecentRoms(),
romsStore.fetchContinuePlayingRoms(),
]);
onMounted(async () => {
await Promise.all([fetchRecentRoms(), fetchContinuePlayingRoms()]);
fetchingRecentAdded.value = false;
fetchingContinuePlaying.value = false;
});
</script>

View File

@@ -16,6 +16,7 @@ export default defineConfig(({ mode }) => {
...loadEnv(mode, "../", envPrefixes),
...loadEnv(mode, "./", envPrefixes),
};
const backendPort = env.DEV_PORT ?? "5000";
const allowedHosts = env.VITE_ALLOWED_HOSTS == "true" ? true : false;