From c3ea419c68ee5c337cfdbcbb1af067f18f60896e Mon Sep 17 00:00:00 2001 From: Georges-Antoine Assi Date: Mon, 26 Aug 2024 15:55:45 -0400 Subject: [PATCH 1/5] Add age ratings to UI + filters --- backend/endpoints/responses/rom.py | 1 + backend/endpoints/tests/test_rom.py | 1 + backend/handler/metadata/igdb_handler.py | 206 +++++++++++++++++- backend/models/rom.py | 4 + .../models/tests/rom_response_example.json | 1 + frontend/src/__generated__/index.ts | 1 + .../__generated__/models/DetailedRomSchema.ts | 1 + .../src/__generated__/models/IGDBAgeRating.ts | 11 + .../__generated__/models/RomIGDBMetadata.ts | 2 + .../src/__generated__/models/RomSchema.ts | 1 + .../__generated__/models/SimpleRomSchema.ts | 1 + .../Users/Dialog/CreateUser.vue | 1 - .../Administration/Users/Dialog/EditUser.vue | 5 +- .../components/Administration/Users/Table.vue | 1 - frontend/src/components/Details/ActionBar.vue | 12 +- .../src/components/Details/Info/FileInfo.vue | 31 ++- .../src/components/Details/Info/GameInfo.vue | 31 ++- frontend/src/components/Details/Notes.vue | 11 +- frontend/src/components/Details/Saves.vue | 2 +- frontend/src/components/Details/States.vue | 2 +- .../AppBar/Platform/FirmwareDrawer.vue | 2 +- .../AppBar/common/FilterDrawer/Base.vue | 11 +- .../Gallery/AppBar/common/FilterTextField.vue | 1 - .../src/components/Gallery/FabOverlay.vue | 8 +- .../Dialog/CreatePlatformBinding.vue | 7 +- .../Dialog/CreatePlatformVersion.vue | 6 +- .../Dialog/DeletePlatformBinding.vue | 6 +- .../common/Collection/Dialog/AddRoms.vue | 1 - .../Collection/Dialog/CreateCollection.vue | 7 +- .../Collection/Dialog/EditCollection.vue | 13 +- .../common/Collection/Dialog/RemoveRoms.vue | 3 +- .../src/components/common/Game/AdminMenu.vue | 3 +- .../common/Game/Dialog/Asset/UploadSaves.vue | 1 - .../common/Game/Dialog/Asset/UploadStates.vue | 1 - .../common/Game/Dialog/CopyDownloadLink.vue | 1 - .../common/Game/Dialog/DeleteRom.vue | 1 - .../common/Game/Dialog/MatchRom.vue | 2 +- .../common/Game/Dialog/SearchRom.vue | 5 +- .../common/Game/Dialog/UploadRom.vue | 5 +- .../src/components/common/Game/FavBtn.vue | 2 +- frontend/src/components/common/Game/Table.vue | 9 +- .../common/Navigation/CollectionsDrawer.vue | 1 - .../common/Navigation/PlatformsDrawer.vue | 1 - frontend/src/components/common/NewVersion.vue | 5 +- .../common/Platform/Dialog/UploadFirmware.vue | 1 - frontend/src/components/common/RDialog.vue | 3 +- .../src/components/common/SearchCover.vue | 5 +- frontend/src/layouts/Dashboard/Recent.vue | 2 +- frontend/src/stores/galleryFilter.ts | 20 +- frontend/src/stores/roms.ts | 10 + frontend/src/views/EmulatorJS/Base.vue | 7 +- frontend/src/views/Gallery/Collection.vue | 21 +- frontend/src/views/Gallery/Platform.vue | 23 +- frontend/src/views/GameDetails.vue | 3 +- frontend/src/views/Home.vue | 5 +- frontend/src/views/Setup.vue | 25 ++- 56 files changed, 427 insertions(+), 126 deletions(-) create mode 100644 frontend/src/__generated__/models/IGDBAgeRating.ts diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py index 200a9fef8..e3ebce0db 100644 --- a/backend/endpoints/responses/rom.py +++ b/backend/endpoints/responses/rom.py @@ -91,6 +91,7 @@ class RomSchema(BaseModel): collections: list[str] companies: list[str] game_modes: list[str] + age_ratings: list[str] igdb_metadata: RomIGDBMetadata | None moby_metadata: RomMobyMetadata | None diff --git a/backend/endpoints/tests/test_rom.py b/backend/endpoints/tests/test_rom.py index 9d396b93e..5792a24a2 100644 --- a/backend/endpoints/tests/test_rom.py +++ b/backend/endpoints/tests/test_rom.py @@ -63,6 +63,7 @@ def test_update_rom(rename_file_mock, get_rom_by_id_mock, client, access_token, "expanded_games": "[]", "ports": "[]", "similar_games": "[]", + "age_ratings": "[1, 2]", }, ) assert response.status_code == 200 diff --git a/backend/handler/metadata/igdb_handler.py b/backend/handler/metadata/igdb_handler.py index 87181485f..85f963e95 100644 --- a/backend/handler/metadata/igdb_handler.py +++ b/backend/handler/metadata/igdb_handler.py @@ -39,6 +39,12 @@ class IGDBPlatform(TypedDict): name: NotRequired[str] +class IGDBAgeRating(TypedDict): + rating: str + category: str + rating_cover_url: str + + class IGDBMetadataPlatform(TypedDict): igdb_id: int name: str @@ -62,6 +68,7 @@ class IGDBMetadata(TypedDict): collections: list[str] companies: list[str] game_modes: list[str] + age_ratings: list[IGDBAgeRating] platforms: list[IGDBMetadataPlatform] expansions: list[IGDBRelatedGame] dlcs: list[IGDBRelatedGame] @@ -101,6 +108,9 @@ def extract_metadata_from_igdb_rom(rom: dict) -> IGDBMetadata: IGDBMetadataPlatform(igdb_id=p.get("id", ""), name=p.get("name", "")) for p in rom.get("platforms", []) ], + "age_ratings": [ + IGDB_AGE_RATINGS[r["rating"]] for r in rom.get("age_ratings", []) + ], "expansions": [ IGDBRelatedGame( id=e["id"], @@ -537,8 +547,8 @@ async def get_matched_roms_by_name( ] return [ - IGDBRom( # type: ignore[misc] - { + IGDBRom( + { # type: ignore[misc] k: v for k, v in { "igdb_id": rom["id"], @@ -546,12 +556,12 @@ async def get_matched_roms_by_name( "name": rom["name"], "summary": rom.get("summary", ""), "url_cover": self._normalize_cover_url( - rom.get("cover", {}) - .get("url", "") - .replace("t_thumb", "t_cover_big") + pydash.get(rom, "cover.url", "").replace( + "t_thumb", "t_cover_big" + ) ), "url_screenshots": [ - self._normalize_cover_url(s.get("url", "")) + self._normalize_cover_url(s.get("url", "")) # type: ignore[arg-type] for s in rom.get("screenshots", []) ], "igdb_metadata": extract_metadata_from_igdb_rom(rom), @@ -674,6 +684,7 @@ async def get_oauth_token(self) -> str: "similar_games.slug", "similar_games.name", "similar_games.cover.url", + "age_ratings.rating", ] SEARCH_FIELDS = ["game.id", "name"] @@ -902,3 +913,186 @@ async def get_oauth_token(self) -> str: {"slug": "vc", "name": "Virtual Console"}, {"slug": "airconsole", "name": "AirConsole"}, ] + +IGDB_AGE_RATINGS: dict[int, IGDBAgeRating] = { + 1: { + "rating": "Three", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_3.png", + }, + 2: { + "rating": "Seven", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_7.png", + }, + 3: { + "rating": "Twelve", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_12.png", + }, + 4: { + "rating": "Sixteen", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_16.png", + }, + 5: { + "rating": "Eighteen", + "category": "PEGI", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/pegi/pegi_18.png", + }, + 6: { + "rating": "RP", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_rp.png", + }, + 7: { + "rating": "EC", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_ec.png", + }, + 8: { + "rating": "E", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_e.png", + }, + 9: { + "rating": "E10", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_e10.png", + }, + 10: { + "rating": "T", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_t.png", + }, + 11: { + "rating": "M", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_m.png", + }, + 12: { + "rating": "AO", + "category": "ESRB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/esrb/esrb_ao.png", + }, + 13: { + "rating": "CERO_A", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_a.png", + }, + 14: { + "rating": "CERO_B", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_b.png", + }, + 15: { + "rating": "CERO_C", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_c.png", + }, + 16: { + "rating": "CERO_D", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_d.png", + }, + 17: { + "rating": "CERO_Z", + "category": "CERO", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/cero/cero_z.png", + }, + 18: { + "rating": "USK_0", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_0.png", + }, + 19: { + "rating": "USK_6", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_6.png", + }, + 20: { + "rating": "USK_12", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_12.png", + }, + 21: { + "rating": "USK_16", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_16.png", + }, + 22: { + "rating": "USK_18", + "category": "USK", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/usk/usk_18.png", + }, + 23: { + "rating": "GRAC_ALL", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_all.png", + }, + 24: { + "rating": "GRAC_Twelve", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_twelve.png", + }, + 25: { + "rating": "GRAC_Fifteen", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_fifteen.png", + }, + 26: { + "rating": "GRAC_Eighteen", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_eighteen.png", + }, + 27: { + "rating": "GRAC_TESTING", + "category": "GRAC", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/grac/grac_testing.png", + }, + 28: { + "rating": "CLASS_IND_L", + "category": "CLASS_IND", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_l.png", + }, + 29: { + "rating": "CLASS_IND_Ten", + "category": "CLASS_IND", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_ten.png", + }, + 30: { + "rating": "CLASS_IND_Twelve", + "category": "CLASS_IND", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/classind/classind_twelve.png", + }, + 31: { + "rating": "ACB_G", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_g.png", + }, + 32: { + "rating": "ACB_PG", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_pg.png", + }, + 33: { + "rating": "ACB_M", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_m.png", + }, + 34: { + "rating": "ACB_MA15", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_ma15.png", + }, + 35: { + "rating": "ACB_R18", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_r18.png", + }, + 36: { + "rating": "ACB_RC", + "category": "ACB", + "rating_cover_url": "https://www.igdb.com/icons/rating_icons/acb/acb_rc.png", + }, +} diff --git a/backend/models/rom.py b/backend/models/rom.py index 0f196a251..d176d21b2 100644 --- a/backend/models/rom.py +++ b/backend/models/rom.py @@ -162,6 +162,10 @@ def companies(self) -> list[str]: def game_modes(self) -> list[str]: return self.igdb_metadata.get("game_modes", []) + @property + def age_ratings(self) -> list[str]: + return [r["rating"] for r in self.igdb_metadata.get("age_ratings", [])] + @property def fs_resources_path(self) -> str: return f"roms/{str(self.platform_id)}/{str(self.id)}" diff --git a/backend/models/tests/rom_response_example.json b/backend/models/tests/rom_response_example.json index 21e7f7c55..a4de8dec6 100644 --- a/backend/models/tests/rom_response_example.json +++ b/backend/models/tests/rom_response_example.json @@ -123,6 +123,7 @@ } } ], + "age_ratings": [{ "rating": 1 }, { "rating": 2 }], "expansions": [ { "id": 239930, diff --git a/frontend/src/__generated__/index.ts b/frontend/src/__generated__/index.ts index 4ba37aea8..d89f440e0 100644 --- a/frontend/src/__generated__/index.ts +++ b/frontend/src/__generated__/index.ts @@ -19,6 +19,7 @@ export type { DetailedRomSchema } from './models/DetailedRomSchema'; export type { FirmwareSchema } from './models/FirmwareSchema'; export type { HeartbeatResponse } from './models/HeartbeatResponse'; export type { HTTPValidationError } from './models/HTTPValidationError'; +export type { IGDBAgeRating } from './models/IGDBAgeRating'; export type { IGDBMetadataPlatform } from './models/IGDBMetadataPlatform'; export type { IGDBRelatedGame } from './models/IGDBRelatedGame'; export type { MessageResponse } from './models/MessageResponse'; diff --git a/frontend/src/__generated__/models/DetailedRomSchema.ts b/frontend/src/__generated__/models/DetailedRomSchema.ts index 1888f2db9..b23f6d6b8 100644 --- a/frontend/src/__generated__/models/DetailedRomSchema.ts +++ b/frontend/src/__generated__/models/DetailedRomSchema.ts @@ -38,6 +38,7 @@ export type DetailedRomSchema = { collections: Array; companies: Array; game_modes: Array; + age_ratings: Array; igdb_metadata: (RomIGDBMetadata | null); moby_metadata: (RomMobyMetadata | null); path_cover_s: (string | null); diff --git a/frontend/src/__generated__/models/IGDBAgeRating.ts b/frontend/src/__generated__/models/IGDBAgeRating.ts new file mode 100644 index 000000000..3e5b6f228 --- /dev/null +++ b/frontend/src/__generated__/models/IGDBAgeRating.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type IGDBAgeRating = { + rating: string; + category: string; + rating_cover_url: string; +}; + diff --git a/frontend/src/__generated__/models/RomIGDBMetadata.ts b/frontend/src/__generated__/models/RomIGDBMetadata.ts index b00c9997b..0eee20bfa 100644 --- a/frontend/src/__generated__/models/RomIGDBMetadata.ts +++ b/frontend/src/__generated__/models/RomIGDBMetadata.ts @@ -3,6 +3,7 @@ /* tslint:disable */ /* eslint-disable */ +import type { IGDBAgeRating } from './IGDBAgeRating'; import type { IGDBMetadataPlatform } from './IGDBMetadataPlatform'; import type { IGDBRelatedGame } from './IGDBRelatedGame'; @@ -16,6 +17,7 @@ export type RomIGDBMetadata = { collections?: Array; companies?: Array; game_modes?: Array; + age_ratings?: Array; platforms?: Array; expansions?: Array; dlcs?: Array; diff --git a/frontend/src/__generated__/models/RomSchema.ts b/frontend/src/__generated__/models/RomSchema.ts index 4d2f71149..17636ecb4 100644 --- a/frontend/src/__generated__/models/RomSchema.ts +++ b/frontend/src/__generated__/models/RomSchema.ts @@ -31,6 +31,7 @@ export type RomSchema = { collections: Array; companies: Array; game_modes: Array; + age_ratings: Array; igdb_metadata: (RomIGDBMetadata | null); moby_metadata: (RomMobyMetadata | null); path_cover_s: (string | null); diff --git a/frontend/src/__generated__/models/SimpleRomSchema.ts b/frontend/src/__generated__/models/SimpleRomSchema.ts index a06ea7335..b776f6caa 100644 --- a/frontend/src/__generated__/models/SimpleRomSchema.ts +++ b/frontend/src/__generated__/models/SimpleRomSchema.ts @@ -33,6 +33,7 @@ export type SimpleRomSchema = { collections: Array; companies: Array; game_modes: Array; + age_ratings: Array; igdb_metadata: (RomIGDBMetadata | null); moby_metadata: (RomMobyMetadata | null); path_cover_s: (string | null); diff --git a/frontend/src/components/Administration/Users/Dialog/CreateUser.vue b/frontend/src/components/Administration/Users/Dialog/CreateUser.vue index 3379e6d08..d882f8c97 100644 --- a/frontend/src/components/Administration/Users/Dialog/CreateUser.vue +++ b/frontend/src/components/Administration/Users/Dialog/CreateUser.vue @@ -22,7 +22,6 @@ emitter?.on("showCreateUserDialog", () => { show.value = true; }); -// Functions async function createUser() { await userApi .createUser(user.value) diff --git a/frontend/src/components/Administration/Users/Dialog/EditUser.vue b/frontend/src/components/Administration/Users/Dialog/EditUser.vue index a61ee6b59..d1198dbc1 100644 --- a/frontend/src/components/Administration/Users/Dialog/EditUser.vue +++ b/frontend/src/components/Administration/Users/Dialog/EditUser.vue @@ -22,7 +22,6 @@ emitter?.on("showEditUserDialog", (userToEdit) => { show.value = true; }); -// Functions function triggerFileInput() { const fileInput = document.getElementById("file-input"); fileInput?.click(); @@ -138,8 +137,8 @@ function closeDialog() { imagePreviewUrl ? imagePreviewUrl : user.avatar_path - ? `/assets/romm/assets/${user.avatar_path}?ts=${user.updated_at}` - : defaultAvatarPath + ? `/assets/romm/assets/${user.avatar_path}?ts=${user.updated_at}` + : defaultAvatarPath " > diff --git a/frontend/src/components/Administration/Users/Table.vue b/frontend/src/components/Administration/Users/Table.vue index 6318c420a..25697193c 100644 --- a/frontend/src/components/Administration/Users/Table.vue +++ b/frontend/src/components/Administration/Users/Table.vue @@ -58,7 +58,6 @@ const usersPerPage = ref(isNaN(storedUsersPerPage) ? 25 : storedUsersPerPage); const pageCount = ref(0); emitter?.on("updateDataTablePages", updateDataTablePages); -// Functions function updateDataTablePages() { pageCount.value = Math.ceil(usersStore.allUsers.length / usersPerPage.value); } diff --git a/frontend/src/components/Details/ActionBar.vue b/frontend/src/components/Details/ActionBar.vue index 5047e7ac7..83bb5bed7 100644 --- a/frontend/src/components/Details/ActionBar.vue +++ b/frontend/src/components/Details/ActionBar.vue @@ -4,7 +4,11 @@ import romApi from "@/services/api/rom"; import storeDownload from "@/stores/download"; import type { DetailedRom } from "@/stores/roms"; import type { Events } from "@/types/emitter"; -import { getDownloadLink, isEJSEmulationSupported, isRuffleEmulationSupported } from "@/utils"; +import { + getDownloadLink, + isEJSEmulationSupported, + isRuffleEmulationSupported, +} from "@/utils"; import type { Emitter } from "mitt"; import { inject, ref } from "vue"; @@ -14,7 +18,9 @@ const downloadStore = storeDownload(); const emitter = inject>("emitter"); const playInfoIcon = ref("mdi-play"); const ejsEmulationSupported = isEJSEmulationSupported(props.rom.platform_slug); -const ruffleEmulationSupported = isRuffleEmulationSupported(props.rom.platform_slug); +const ruffleEmulationSupported = isRuffleEmulationSupported( + props.rom.platform_slug, +); // Functions async function copyDownloadLink(rom: DetailedRom) { @@ -26,7 +32,7 @@ async function copyDownloadLink(rom: DetailedRom) { getDownloadLink({ rom, files: downloadStore.filesToDownload, - }) + }), ); if (navigator.clipboard && window.isSecureContext) { await navigator.clipboard.writeText(downloadLink); diff --git a/frontend/src/components/Details/Info/FileInfo.vue b/frontend/src/components/Details/Info/FileInfo.vue index a121249ac..aa6153b1a 100644 --- a/frontend/src/components/Details/Info/FileInfo.vue +++ b/frontend/src/components/Details/Info/FileInfo.vue @@ -23,7 +23,7 @@ const romUser = ref( note_raw_markdown: "", note_is_public: false, is_main_sibling: false, - } + }, ); // Functions @@ -53,7 +53,7 @@ watch( note_is_public: false, is_main_sibling: false, }; - } + }, );