mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge remote-tracking branch 'upstream/master' into console-mode
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user