mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge pull request #1324 from rommapp/feature/add-platform-info
feat: Add platform info drawer + per platform settings
This commit is contained in:
@@ -2,12 +2,12 @@
|
||||
# To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml
|
||||
version: 0.1
|
||||
cli:
|
||||
version: 1.22.4
|
||||
version: 1.22.8
|
||||
# Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins)
|
||||
plugins:
|
||||
sources:
|
||||
- id: trunk
|
||||
ref: v1.6.2
|
||||
ref: v1.6.5
|
||||
uri: https://github.com/trunk-io/plugins
|
||||
# Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes)
|
||||
runtimes:
|
||||
@@ -18,25 +18,25 @@ runtimes:
|
||||
# This is the section where you manage your linters. (https://docs.trunk.io/check/configuration)
|
||||
lint:
|
||||
enabled:
|
||||
- markdownlint@0.41.0
|
||||
- eslint@9.9.1
|
||||
- actionlint@1.7.1
|
||||
- bandit@1.7.9
|
||||
- black@24.8.0
|
||||
- checkov@3.2.239
|
||||
- markdownlint@0.42.0
|
||||
- eslint@9.14.0
|
||||
- actionlint@1.7.4
|
||||
- bandit@1.7.10
|
||||
- black@24.10.0
|
||||
- checkov@3.2.296
|
||||
- git-diff-check
|
||||
- isort@5.13.2
|
||||
- mypy@1.13.0
|
||||
- osv-scanner@1.8.4
|
||||
- osv-scanner@1.9.1
|
||||
- oxipng@9.1.2
|
||||
- prettier@3.3.3
|
||||
- ruff@0.6.3
|
||||
- ruff@0.7.3
|
||||
- shellcheck@0.10.0
|
||||
- shfmt@3.6.0
|
||||
- svgo@3.3.2
|
||||
- taplo@0.9.3
|
||||
- trivy@0.54.1
|
||||
- trufflehog@3.81.10
|
||||
- trivy@0.56.2
|
||||
- trufflehog@3.83.6
|
||||
- yamllint@1.35.1
|
||||
ignore:
|
||||
- linters: [ALL]
|
||||
|
||||
@@ -130,7 +130,7 @@ docker exec -i romm-mariadb-dev mariadb -uroot -p<root password> < backend/romm_
|
||||
|
||||
### - Run tests
|
||||
|
||||
*\_\_*Migrations will be run automatically when running the tests.\_\_\*
|
||||
*\_*Migrations will be run automatically when running the tests.\_\_\_
|
||||
|
||||
```sh
|
||||
cd backend
|
||||
|
||||
56
backend/alembic/versions/0027_platforms_data.py
Normal file
56
backend/alembic/versions/0027_platforms_data.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""platforms_data
|
||||
|
||||
Revision ID: 0027_platforms_data
|
||||
Revises: 0026_romuser_status_fields
|
||||
Create Date: 2024-11-17 23:05:31.038917
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from models.platform import DEFAULT_COVER_ASPECT_RATIO
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0027_platforms_data"
|
||||
down_revision = "0026_romuser_status_fields"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("category", sa.String(length=50), nullable=True))
|
||||
batch_op.add_column(sa.Column("generation", sa.Integer(), nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column("family_name", sa.String(length=1000), nullable=True)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column("family_slug", sa.String(length=1000), nullable=True)
|
||||
)
|
||||
batch_op.add_column(sa.Column("url", sa.String(length=1000), nullable=True))
|
||||
batch_op.add_column(
|
||||
sa.Column("url_logo", sa.String(length=1000), nullable=True)
|
||||
)
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"aspect_ratio",
|
||||
sa.String(length=10),
|
||||
nullable=False,
|
||||
server_default=DEFAULT_COVER_ASPECT_RATIO,
|
||||
)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.drop_column("url_logo")
|
||||
batch_op.drop_column("url")
|
||||
batch_op.drop_column("family_name")
|
||||
batch_op.drop_column("family_slug")
|
||||
batch_op.drop_column("generation")
|
||||
batch_op.drop_column("category")
|
||||
batch_op.drop_column("aspect_ratio")
|
||||
# ### end Alembic commands ###
|
||||
@@ -33,9 +33,6 @@ def get_config() -> ConfigResponse:
|
||||
EXCLUDED_MULTI_PARTS_FILES=cfg.EXCLUDED_MULTI_PARTS_FILES,
|
||||
PLATFORMS_BINDING=cfg.PLATFORMS_BINDING,
|
||||
PLATFORMS_VERSIONS=cfg.PLATFORMS_VERSIONS,
|
||||
ROMS_FOLDER_NAME=cfg.ROMS_FOLDER_NAME,
|
||||
FIRMWARE_FOLDER_NAME=cfg.FIRMWARE_FOLDER_NAME,
|
||||
HIGH_PRIO_STRUCTURE_PATH=cfg.HIGH_PRIO_STRUCTURE_PATH,
|
||||
)
|
||||
except ConfigNotReadableException as exc:
|
||||
log.critical(exc.message)
|
||||
|
||||
@@ -112,17 +112,26 @@ def get_platform(request: Request, id: int) -> PlatformSchema:
|
||||
|
||||
|
||||
@protected_route(router.put, "/platforms/{id}", [Scope.PLATFORMS_WRITE])
|
||||
async def update_platform(request: Request) -> MessageResponse:
|
||||
async def update_platform(request: Request, id: int) -> MessageResponse:
|
||||
"""Update platform endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
id (int): Platform id
|
||||
|
||||
Returns:
|
||||
MessageResponse: Standard message response
|
||||
"""
|
||||
data = await request.json()
|
||||
platform_db = db_platform_handler.get_platform(id)
|
||||
|
||||
return {"msg": "Enpoint not available yet"}
|
||||
if not platform_db:
|
||||
raise PlatformNotFoundInDatabaseException(id)
|
||||
|
||||
platform_db.aspect_ratio = data.get("aspect_ratio", platform_db.aspect_ratio)
|
||||
db_platform_handler.add_platform(platform_db)
|
||||
|
||||
return {"msg": "Platform updated successfully"}
|
||||
|
||||
|
||||
@protected_route(router.delete, "/platforms/{id}", [Scope.PLATFORMS_WRITE])
|
||||
|
||||
@@ -10,6 +10,3 @@ class ConfigResponse(TypedDict):
|
||||
EXCLUDED_MULTI_PARTS_FILES: list[str]
|
||||
PLATFORMS_BINDING: dict[str, str]
|
||||
PLATFORMS_VERSIONS: dict[str, str]
|
||||
ROMS_FOLDER_NAME: str
|
||||
FIRMWARE_FOLDER_NAME: str
|
||||
HIGH_PRIO_STRUCTURE_PATH: str
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from models.platform import DEFAULT_COVER_ASPECT_RATIO
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from .firmware import FirmwareSchema
|
||||
@@ -14,9 +15,15 @@ class PlatformSchema(BaseModel):
|
||||
igdb_id: int | None = None
|
||||
sgdb_id: int | None = None
|
||||
moby_id: int | None = None
|
||||
logo_path: str | None = ""
|
||||
category: str | None = None
|
||||
generation: int | None = None
|
||||
family_name: str | None = None
|
||||
family_slug: str | None = None
|
||||
url: str | None = None
|
||||
url_logo: str | None = None
|
||||
logo_path: str | None = None
|
||||
firmware: list[FirmwareSchema] = Field(default_factory=list)
|
||||
|
||||
aspect_ratio: str = DEFAULT_COVER_ASPECT_RATIO
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ class SearchRomSchema(BaseModel):
|
||||
summary: str
|
||||
igdb_url_cover: str = ""
|
||||
moby_url_cover: str = ""
|
||||
platform_id: int
|
||||
|
||||
|
||||
class SearchCoverSchema(BaseModel):
|
||||
|
||||
@@ -99,6 +99,7 @@ async def search_rom(
|
||||
"summary": "",
|
||||
"igdb_url_cover": "",
|
||||
"moby_url_cover": "",
|
||||
"platform_id": rom.platform_id,
|
||||
},
|
||||
**item,
|
||||
}
|
||||
|
||||
@@ -21,5 +21,3 @@ def test_config(client):
|
||||
assert config.get("EXCLUDED_MULTI_PARTS_EXT") == []
|
||||
assert config.get("EXCLUDED_MULTI_PARTS_FILES") == []
|
||||
assert config.get("PLATFORMS_BINDING") == {}
|
||||
assert config.get("ROMS_FOLDER_NAME") == "roms"
|
||||
assert config.get("FIRMWARE_FOLDER_NAME") == "bios"
|
||||
|
||||
@@ -35,6 +35,18 @@ class IGDBPlatform(TypedDict):
|
||||
slug: str
|
||||
igdb_id: int | None
|
||||
name: NotRequired[str]
|
||||
category: NotRequired[str]
|
||||
generation: NotRequired[str]
|
||||
family_name: NotRequired[str]
|
||||
family_slug: NotRequired[str]
|
||||
url: NotRequired[str]
|
||||
url_logo: NotRequired[str]
|
||||
logo_path: NotRequired[str]
|
||||
|
||||
|
||||
class IGDBMetadataPlatform(TypedDict):
|
||||
igdb_id: int
|
||||
name: str
|
||||
|
||||
|
||||
class IGDBAgeRating(TypedDict):
|
||||
@@ -43,11 +55,6 @@ class IGDBAgeRating(TypedDict):
|
||||
rating_cover_url: str
|
||||
|
||||
|
||||
class IGDBMetadataPlatform(TypedDict):
|
||||
igdb_id: int
|
||||
name: str
|
||||
|
||||
|
||||
class IGDBRelatedGame(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
@@ -88,12 +95,10 @@ class IGDBRom(TypedDict):
|
||||
igdb_metadata: NotRequired[IGDBMetadata]
|
||||
|
||||
|
||||
def extract_metadata_from_igdb_rom(
|
||||
rom: dict, video_id: str | None = None
|
||||
) -> IGDBMetadata:
|
||||
def extract_metadata_from_igdb_rom(rom: dict) -> IGDBMetadata:
|
||||
return IGDBMetadata(
|
||||
{
|
||||
"youtube_video_id": video_id,
|
||||
"youtube_video_id": str(pydash.get(rom, "videos[0].video_id", None)),
|
||||
"total_rating": str(round(rom.get("total_rating", 0.0), 2)),
|
||||
"aggregated_rating": str(round(rom.get("aggregated_rating", 0.0), 2)),
|
||||
"first_release_date": rom.get("first_release_date", None),
|
||||
@@ -120,7 +125,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=e["id"],
|
||||
slug=e["slug"],
|
||||
name=e["name"],
|
||||
cover_url=pydash.get(e, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(e, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="expansion",
|
||||
)
|
||||
for e in rom.get("expansions", [])
|
||||
@@ -130,7 +137,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=d["id"],
|
||||
slug=d["slug"],
|
||||
name=d["name"],
|
||||
cover_url=pydash.get(d, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(d, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="dlc",
|
||||
)
|
||||
for d in rom.get("dlcs", [])
|
||||
@@ -140,7 +149,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=r["id"],
|
||||
slug=r["slug"],
|
||||
name=r["name"],
|
||||
cover_url=pydash.get(r, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(r, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="remaster",
|
||||
)
|
||||
for r in rom.get("remasters", [])
|
||||
@@ -150,7 +161,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=r["id"],
|
||||
slug=r["slug"],
|
||||
name=r["name"],
|
||||
cover_url=pydash.get(r, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(r, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="remake",
|
||||
)
|
||||
for r in rom.get("remakes", [])
|
||||
@@ -160,7 +173,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=g["id"],
|
||||
slug=g["slug"],
|
||||
name=g["name"],
|
||||
cover_url=pydash.get(g, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(g, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="expanded",
|
||||
)
|
||||
for g in rom.get("expanded_games", [])
|
||||
@@ -170,7 +185,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=p["id"],
|
||||
slug=p["slug"],
|
||||
name=p["name"],
|
||||
cover_url=pydash.get(p, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(p, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="port",
|
||||
)
|
||||
for p in rom.get("ports", [])
|
||||
@@ -180,7 +197,9 @@ def extract_metadata_from_igdb_rom(
|
||||
id=s["id"],
|
||||
slug=s["slug"],
|
||||
name=s["name"],
|
||||
cover_url=pydash.get(s, "cover.url", ""),
|
||||
cover_url=MetadataHandler._normalize_cover_url(
|
||||
pydash.get(s, "cover.url", "").replace("t_thumb", "t_1080p")
|
||||
),
|
||||
type="similar",
|
||||
)
|
||||
for s in rom.get("similar_games", [])
|
||||
@@ -199,7 +218,6 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
self.games_fields = GAMES_FIELDS
|
||||
self.search_endpoint = f"{self.BASE_URL}/search"
|
||||
self.search_fields = SEARCH_FIELDS
|
||||
self.video_endpoint = f"{self.BASE_URL}/game_videos"
|
||||
self.pagination_limit = 200
|
||||
self.twitch_auth = TwitchAuth()
|
||||
self.headers = {
|
||||
@@ -318,6 +336,7 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
for rom_name in rom_names
|
||||
)
|
||||
|
||||
log.debug("Searching in games endpoint with category %s", category_filter)
|
||||
roms = await self._request(
|
||||
self.games_endpoint,
|
||||
data=f'search "{search_term}"; fields {",".join(self.games_fields)}; where platforms=[{platform_igdb_id}] {category_filter};',
|
||||
@@ -327,11 +346,16 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
if is_exact_match(rom, search_term):
|
||||
return rom
|
||||
|
||||
log.debug("Searching expanded in search endpoint")
|
||||
roms_expanded = await self._request(
|
||||
self.search_endpoint,
|
||||
data=f'fields {",".join(self.search_fields)}; where game.platforms=[{platform_igdb_id}] & (name ~ *"{search_term}"* | alternative_name ~ *"{search_term}"*);',
|
||||
)
|
||||
if roms_expanded:
|
||||
log.debug(
|
||||
"Searching expanded in games endpoint for expanded game %s",
|
||||
roms_expanded[0]["game"],
|
||||
)
|
||||
extra_roms = await self._request(
|
||||
self.games_endpoint,
|
||||
f'fields {",".join(self.games_fields)}; where id={roms_expanded[0]["game"]["id"]};',
|
||||
@@ -354,13 +378,24 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
self.platform_endpoint,
|
||||
data=f'fields {",".join(self.platforms_fields)}; where slug="{slug.lower()}";',
|
||||
)
|
||||
|
||||
platform = pydash.get(platforms, "[0]", None)
|
||||
if platform:
|
||||
return IGDBPlatform(
|
||||
igdb_id=platform["id"],
|
||||
igdb_id=platform.get("id", None),
|
||||
slug=slug,
|
||||
name=platform["name"],
|
||||
name=platform.get("name", slug),
|
||||
category=IGDB_PLATFORM_CATEGORIES.get(
|
||||
platform.get("category", 0), "Unknown"
|
||||
),
|
||||
generation=platform.get("generation", None),
|
||||
family_name=pydash.get(platform, "platform_family.name", None),
|
||||
family_slug=pydash.get(platform, "platform_family.slug", None),
|
||||
url=platform.get("url", None),
|
||||
url_logo=self._normalize_cover_url(
|
||||
pydash.get(platform, "platform_logo.url", "").replace(
|
||||
"t_thumb", "t_1080p"
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Check if platform is a version if not found
|
||||
@@ -448,13 +483,19 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
|
||||
search_term = self.normalize_search_term(search_term)
|
||||
|
||||
log.debug("Searching for %s on IGDB with category", search_term)
|
||||
rom = await self._search_rom(search_term, platform_igdb_id, with_category=True)
|
||||
if not rom:
|
||||
log.debug("Searching for %s on IGDB without category", search_term)
|
||||
rom = await self._search_rom(search_term, platform_igdb_id)
|
||||
|
||||
# Split the search term since igdb struggles with colons
|
||||
if not rom and ":" in search_term:
|
||||
for term in search_term.split(":")[::-1]:
|
||||
log.debug(
|
||||
"Searching for %s on IGDB without category after splitting semicolon",
|
||||
term,
|
||||
)
|
||||
rom = await self._search_rom(term, platform_igdb_id)
|
||||
if rom:
|
||||
break
|
||||
@@ -462,6 +503,10 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
# Some MAME games have two titles split by a slash
|
||||
if not rom and "/" in search_term:
|
||||
for term in search_term.split("/"):
|
||||
log.debug(
|
||||
"Searching for %s on IGDB without category after splitting slash",
|
||||
term,
|
||||
)
|
||||
rom = await self._search_rom(term.strip(), platform_igdb_id)
|
||||
if rom:
|
||||
break
|
||||
@@ -469,13 +514,6 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
if not rom:
|
||||
return fallback_rom
|
||||
|
||||
# Get the video ID for the game
|
||||
video_ids = await self._request(
|
||||
self.video_endpoint,
|
||||
f'fields video_id; where game={rom["id"]};',
|
||||
)
|
||||
video_id = pydash.get(video_ids, "[0].video_id", None)
|
||||
|
||||
return IGDBRom(
|
||||
igdb_id=rom["id"],
|
||||
slug=rom["slug"],
|
||||
@@ -488,7 +526,7 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
self._normalize_cover_url(s.get("url", "")).replace("t_thumb", "t_720p")
|
||||
for s in rom.get("screenshots", [])
|
||||
],
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(rom, video_id),
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(rom),
|
||||
)
|
||||
|
||||
@check_twitch_token
|
||||
@@ -505,13 +543,6 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
if not rom:
|
||||
return IGDBRom(igdb_id=None)
|
||||
|
||||
# Get the video ID for the game
|
||||
video_ids = await self._request(
|
||||
self.video_endpoint,
|
||||
f'fields video_id; where game={rom["id"]};',
|
||||
)
|
||||
video_id = pydash.get(video_ids, "[0].video_id", None)
|
||||
|
||||
return IGDBRom(
|
||||
igdb_id=rom["id"],
|
||||
slug=rom["slug"],
|
||||
@@ -524,7 +555,7 @@ class IGDBBaseHandler(MetadataHandler):
|
||||
self._normalize_cover_url(s.get("url", "")).replace("t_thumb", "t_720p")
|
||||
for s in rom.get("screenshots", [])
|
||||
],
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(rom, video_id),
|
||||
igdb_metadata=extract_metadata_from_igdb_rom(rom),
|
||||
)
|
||||
|
||||
@check_twitch_token
|
||||
@@ -694,7 +725,16 @@ class TwitchAuth(MetadataHandler):
|
||||
return token
|
||||
|
||||
|
||||
PLATFORMS_FIELDS = ["id", "name"]
|
||||
PLATFORMS_FIELDS = [
|
||||
"id",
|
||||
"name",
|
||||
"category",
|
||||
"generation",
|
||||
"url",
|
||||
"platform_family.name",
|
||||
"platform_family.slug",
|
||||
"platform_logo.url",
|
||||
]
|
||||
|
||||
GAMES_FIELDS = [
|
||||
"id",
|
||||
@@ -745,6 +785,7 @@ GAMES_FIELDS = [
|
||||
"similar_games.name",
|
||||
"similar_games.cover.url",
|
||||
"age_ratings.rating",
|
||||
"videos.video_id",
|
||||
]
|
||||
|
||||
SEARCH_FIELDS = ["game.id", "name"]
|
||||
@@ -974,6 +1015,16 @@ IGDB_PLATFORM_LIST = [
|
||||
{"slug": "airconsole", "name": "AirConsole"},
|
||||
]
|
||||
|
||||
IGDB_PLATFORM_CATEGORIES: dict[int, str] = {
|
||||
0: "Unknown",
|
||||
1: "Console",
|
||||
2: "Arcade",
|
||||
3: "Platform",
|
||||
4: "Operative System",
|
||||
5: "Portable Console",
|
||||
6: "Computer",
|
||||
}
|
||||
|
||||
IGDB_AGE_RATINGS: dict[int, IGDBAgeRating] = {
|
||||
1: {
|
||||
"rating": "Three",
|
||||
|
||||
@@ -11,6 +11,9 @@ if TYPE_CHECKING:
|
||||
from models.firmware import Firmware
|
||||
|
||||
|
||||
DEFAULT_COVER_ASPECT_RATIO = "2 / 3"
|
||||
|
||||
|
||||
class Platform(BaseModel):
|
||||
__tablename__ = "platforms"
|
||||
|
||||
@@ -21,6 +24,12 @@ class Platform(BaseModel):
|
||||
slug: Mapped[str] = mapped_column(String(length=50))
|
||||
fs_slug: Mapped[str] = mapped_column(String(length=50))
|
||||
name: Mapped[str] = mapped_column(String(length=400))
|
||||
category: Mapped[str | None] = mapped_column(String(length=50), default="")
|
||||
generation: Mapped[int | None]
|
||||
family_name: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
family_slug: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
url: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
url_logo: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
logo_path: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
|
||||
roms: Mapped[list[Rom]] = relationship(back_populates="platform")
|
||||
@@ -28,6 +37,10 @@ class Platform(BaseModel):
|
||||
lazy="selectin", back_populates="platform"
|
||||
)
|
||||
|
||||
aspect_ratio: Mapped[str] = mapped_column(
|
||||
String, server_default=DEFAULT_COVER_ASPECT_RATIO
|
||||
)
|
||||
|
||||
# This runs a subquery to get the count of roms for the platform
|
||||
rom_count = column_property(
|
||||
select(func.count(Rom.id)).where(Rom.platform_id == id).scalar_subquery()
|
||||
|
||||
223
frontend/package-lock.json
generated
223
frontend/package-lock.json
generated
@@ -29,7 +29,7 @@
|
||||
"socket.io-client": "^4.7.5",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"vuetify": "^3.6.5",
|
||||
"vuetify": "^3.7.4",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -56,7 +56,7 @@
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-plugin-static-copy": "0.17.1",
|
||||
"vite-plugin-vuetify": "^1.0.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18"
|
||||
@@ -434,17 +434,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.24.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||
"integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.24.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -562,8 +562,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.24.5",
|
||||
"license": "MIT",
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
|
||||
"integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.26.0"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@@ -1763,13 +1767,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.24.5",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "7.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
|
||||
"integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.24.1",
|
||||
"@babel/helper-validator-identifier": "^7.24.5",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
"@babel/helper-string-parser": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.25.9"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -2934,28 +2937,29 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/language-core": {
|
||||
"version": "1.11.1",
|
||||
"version": "2.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz",
|
||||
"integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/source-map": "1.11.1"
|
||||
"@volar/source-map": "2.4.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@volar/source-map": {
|
||||
"version": "1.11.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"muggle-string": "^0.3.1"
|
||||
}
|
||||
"version": "2.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz",
|
||||
"integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@volar/typescript": {
|
||||
"version": "1.11.1",
|
||||
"version": "2.4.10",
|
||||
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz",
|
||||
"integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "1.11.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
"@volar/language-core": "2.4.10",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vscode-uri": "^3.0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
@@ -3000,24 +3004,34 @@
|
||||
"@vue/shared": "3.4.27"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-vue2": {
|
||||
"version": "2.7.16",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz",
|
||||
"integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
"version": "6.6.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/language-core": {
|
||||
"version": "1.8.27",
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.10.tgz",
|
||||
"integrity": "sha512-DAI289d0K3AB5TUG3xDp9OuQ71CnrujQwJrQnfuZDwo6eGNf0UoRlPuaVNO+Zrn65PC3j0oB2i7mNmVPggeGeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/language-core": "~1.11.1",
|
||||
"@volar/source-map": "~1.11.1",
|
||||
"@vue/compiler-dom": "^3.3.0",
|
||||
"@vue/shared": "^3.3.0",
|
||||
"computeds": "^0.0.1",
|
||||
"@volar/language-core": "~2.4.8",
|
||||
"@vue/compiler-dom": "^3.5.0",
|
||||
"@vue/compiler-vue2": "^2.7.16",
|
||||
"@vue/shared": "^3.5.0",
|
||||
"alien-signals": "^0.2.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"muggle-string": "^0.3.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
"muggle-string": "^0.4.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -3028,18 +3042,49 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
|
||||
"integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.25.3",
|
||||
"@vue/shared": "3.5.13",
|
||||
"entities": "^4.5.0",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
|
||||
"integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.13",
|
||||
"@vue/shared": "3.5.13"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/@vue/shared": {
|
||||
"version": "3.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
|
||||
"integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/language-core/node_modules/minimatch": {
|
||||
"version": "9.0.4",
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -3141,6 +3186,12 @@
|
||||
"url": "https://github.com/sponsors/epoberezkin"
|
||||
}
|
||||
},
|
||||
"node_modules/alien-signals": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-0.2.2.tgz",
|
||||
"integrity": "sha512-cZIRkbERILsBOXTQmMrxc9hgpxglstn69zm+F1ARf4aPAzdAFYd6sBq87ErO0Fj3DV94tglcyHG5kQz9nDC/8A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"dev": true,
|
||||
@@ -3577,11 +3628,6 @@
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/computeds": {
|
||||
"version": "0.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"dev": true,
|
||||
@@ -3644,9 +3690,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -3733,8 +3780,9 @@
|
||||
},
|
||||
"node_modules/de-indent": {
|
||||
"version": "1.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
|
||||
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
@@ -5502,11 +5550,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
"braces": "^3.0.3",
|
||||
"picomatch": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -5579,9 +5628,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.3.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
"integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.0.7",
|
||||
@@ -5765,8 +5815,9 @@
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
@@ -6265,9 +6316,10 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.79.1",
|
||||
"version": "2.79.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
|
||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -6740,14 +6792,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -7079,7 +7123,6 @@
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
|
||||
"integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.15.9",
|
||||
"postcss": "^8.4.18",
|
||||
@@ -7147,9 +7190,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-pwa/node_modules/rollup": {
|
||||
"version": "3.29.4",
|
||||
"version": "3.29.5",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz",
|
||||
"integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@@ -7231,6 +7275,12 @@
|
||||
"esbuild-windows-arm64": "0.15.18"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-uri": {
|
||||
"version": "3.0.8",
|
||||
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz",
|
||||
"integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.4.27",
|
||||
"license": "MIT",
|
||||
@@ -7286,34 +7336,27 @@
|
||||
"vue": "^3.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-template-compiler": {
|
||||
"version": "2.7.16",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"de-indent": "^1.0.2",
|
||||
"he": "^1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-tsc": {
|
||||
"version": "1.8.27",
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz",
|
||||
"integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@volar/typescript": "~1.11.1",
|
||||
"@vue/language-core": "1.8.27",
|
||||
"@volar/typescript": "~2.4.8",
|
||||
"@vue/language-core": "2.1.10",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"bin": {
|
||||
"vue-tsc": "bin/vue-tsc.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
"typescript": ">=5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vuetify": {
|
||||
"version": "3.6.5",
|
||||
"license": "MIT",
|
||||
"version": "3.7.4",
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.4.tgz",
|
||||
"integrity": "sha512-Y8UU5wUDQXC3oz2uumPb8IOdvB4XMCxtxnmqdOc+LihNuPlkSgxIwf92ndRzbOtJFKHsggFUxpyLqpQp+A+5kg==",
|
||||
"engines": {
|
||||
"node": "^12.20 || >=14.13"
|
||||
},
|
||||
@@ -7325,7 +7368,6 @@
|
||||
"typescript": ">=4.7",
|
||||
"vite-plugin-vuetify": ">=1.0.0",
|
||||
"vue": "^3.3.0",
|
||||
"vue-i18n": "^9.0.0",
|
||||
"webpack-plugin-vuetify": ">=2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
@@ -7335,9 +7377,6 @@
|
||||
"vite-plugin-vuetify": {
|
||||
"optional": true
|
||||
},
|
||||
"vue-i18n": {
|
||||
"optional": true
|
||||
},
|
||||
"webpack-plugin-vuetify": {
|
||||
"optional": true
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
"socket.io-client": "^4.7.5",
|
||||
"vue": "^3.4.27",
|
||||
"vue-router": "^4.3.2",
|
||||
"vuetify": "^3.6.5",
|
||||
"vuetify": "^3.7.4",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -74,7 +74,7 @@
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-plugin-static-copy": "0.17.1",
|
||||
"vite-plugin-vuetify": "^1.0.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18"
|
||||
|
||||
@@ -12,8 +12,5 @@ export type ConfigResponse = {
|
||||
EXCLUDED_MULTI_PARTS_FILES: Array<string>;
|
||||
PLATFORMS_BINDING: Record<string, string>;
|
||||
PLATFORMS_VERSIONS: Record<string, string>;
|
||||
ROMS_FOLDER_NAME: string;
|
||||
FIRMWARE_FOLDER_NAME: string;
|
||||
HIGH_PRIO_STRUCTURE_PATH: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,8 +14,15 @@ export type PlatformSchema = {
|
||||
igdb_id?: (number | null);
|
||||
sgdb_id?: (number | null);
|
||||
moby_id?: (number | null);
|
||||
category?: (string | null);
|
||||
generation?: (number | null);
|
||||
family_name?: (string | null);
|
||||
family_slug?: (string | null);
|
||||
url?: (string | null);
|
||||
url_logo?: (string | null);
|
||||
logo_path?: (string | null);
|
||||
firmware?: Array<FirmwareSchema>;
|
||||
aspect_ratio?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
@@ -11,5 +11,6 @@ export type SearchRomSchema = {
|
||||
summary: string;
|
||||
igdb_url_cover?: string;
|
||||
moby_url_cover?: string;
|
||||
platform_id: number;
|
||||
};
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@ import romApi from "@/services/api/rom";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import type { Collection } from "@/stores/collections";
|
||||
import storeDownload from "@/stores/download";
|
||||
import type { Platform } from "@/stores/platforms";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { formatBytes } from "@/utils";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom; platform: Platform }>();
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const downloadStore = storeDownload();
|
||||
const auth = storeAuth();
|
||||
const romUser = ref(props.rom.rom_user);
|
||||
@@ -47,7 +46,7 @@ watch(
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-row class="align-center" no-gutters>
|
||||
<version-switcher :rom="rom" :platform="platform" />
|
||||
<version-switcher :rom="rom" />
|
||||
<v-tooltip
|
||||
location="top"
|
||||
class="tooltip"
|
||||
@@ -111,15 +110,15 @@ watch(
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Info</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip size="small" label class="mx-1 my-1">
|
||||
<v-col class="my-1">
|
||||
<v-chip size="small" label>
|
||||
Size: {{ formatBytes(rom.file_size_bytes) }}
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="!rom.multi && rom.sha1_hash"
|
||||
size="small"
|
||||
label
|
||||
class="mx-1 my-1"
|
||||
class="ml-1"
|
||||
>
|
||||
SHA-1: {{ rom.sha1_hash }}
|
||||
</v-chip>
|
||||
@@ -127,7 +126,7 @@ watch(
|
||||
v-if="!rom.multi && rom.md5_hash"
|
||||
size="small"
|
||||
label
|
||||
class="mx-1 my-1"
|
||||
class="ml-1"
|
||||
>
|
||||
MD5: {{ rom.md5_hash }}
|
||||
</v-chip>
|
||||
@@ -135,7 +134,7 @@ watch(
|
||||
v-if="!rom.multi && rom.crc_hash"
|
||||
size="small"
|
||||
label
|
||||
class="mx-1 my-1"
|
||||
class="ml-1"
|
||||
>
|
||||
CRC: {{ rom.crc_hash }}
|
||||
</v-chip>
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import storeGalleryFilter, { type FilterType } from "@/stores/galleryFilter";
|
||||
import { type FilterType } from "@/stores/galleryFilter";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const { xs } = useDisplay();
|
||||
const show = ref(false);
|
||||
const carousel = ref(0);
|
||||
const router = useRouter();
|
||||
const filters = ["genres", "franchises", "collections", "companies"] as const;
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const { defaultAspectRatioScreenshot } = storeToRefs(galleryViewStore);
|
||||
|
||||
// Functions
|
||||
function onFilterClick(filter: FilterType, value: string) {
|
||||
router.push({
|
||||
name: "platform",
|
||||
@@ -115,7 +121,7 @@ function onFilterClick(filter: FilterType, value: string) {
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
style="aspect-ratio: 16 / 9"
|
||||
:style="`aspect-ratio: ${defaultAspectRatioScreenshot}`"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</v-carousel-item>
|
||||
@@ -172,7 +178,7 @@ function onFilterClick(filter: FilterType, value: string) {
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
referrerpolicy="strict-origin-when-cross-origin"
|
||||
style="aspect-ratio: 16 / 9"
|
||||
:style="`aspect-ratio: ${defaultAspectRatioScreenshot}`"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
</v-carousel-item>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { identity } from "lodash";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom; platform: Platform }>();
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const { smAndDown } = useDisplay();
|
||||
const releaseDate = new Date(
|
||||
Number(props.rom.first_release_date) * 1000,
|
||||
@@ -39,12 +39,12 @@ const hasReleaseDate = Number(props.rom.first_release_date) > 0;
|
||||
no-gutters
|
||||
>
|
||||
<v-col>
|
||||
<v-chip :to="{ name: 'platform', params: { platform: platform.id } }">
|
||||
{{ platform.name }}
|
||||
<v-chip :to="{ name: 'platform', params: { platform: rom.platform_id } }">
|
||||
{{ rom.platform_name }}
|
||||
<platform-icon
|
||||
:key="platform.slug"
|
||||
:slug="platform.slug"
|
||||
:name="platform.name"
|
||||
:key="rom.platform_slug"
|
||||
:slug="rom.platform_slug"
|
||||
:name="rom.platform_name"
|
||||
:size="30"
|
||||
class="ml-2"
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import type { Platform } from "@/stores/platforms";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { languageToEmoji, regionToEmoji } from "@/utils";
|
||||
import type { RomSchema } from "@/__generated__";
|
||||
@@ -7,7 +6,7 @@ import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom; platform: Platform }>();
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const router = useRouter();
|
||||
const version = ref(props.rom.id);
|
||||
|
||||
|
||||
@@ -14,7 +14,15 @@ const auth = storeAuth();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar id="gallery-app-bar" elevation="0" density="compact">
|
||||
<v-app-bar
|
||||
id="gallery-app-bar"
|
||||
elevation="0"
|
||||
density="compact"
|
||||
mode="shift"
|
||||
app
|
||||
fixed
|
||||
top
|
||||
>
|
||||
<filter-btn />
|
||||
<filter-text-field v-if="!xs" />
|
||||
<div v-if="xs" class="flex-grow-1" />
|
||||
|
||||
@@ -1,57 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import AdminMenu from "@/components/Gallery/AppBar/Platform/AdminMenu.vue";
|
||||
import FirmwareBtn from "@/components/Gallery/AppBar/Platform/FirmwareBtn.vue";
|
||||
import FirmwareDrawer from "@/components/Gallery/AppBar/Platform/FirmwareDrawer.vue";
|
||||
import PlatformInfoDrawer from "@/components/Gallery/AppBar/Platform/PlatformInfoDrawer.vue";
|
||||
import FilterBtn from "@/components/Gallery/AppBar/common/FilterBtn.vue";
|
||||
import FilterDrawer from "@/components/Gallery/AppBar/common/FilterDrawer/Base.vue";
|
||||
import FilterTextField from "@/components/Gallery/AppBar/common/FilterTextField.vue";
|
||||
import GalleryViewBtn from "@/components/Gallery/AppBar/common/GalleryViewBtn.vue";
|
||||
import SelectingBtn from "@/components/Gallery/AppBar/common/SelectingBtn.vue";
|
||||
import PlatformIcon from "@/components/common/Platform/Icon.vue";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import storeNavigation from "@/stores/navigation";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const { xs } = useDisplay();
|
||||
const romsStore = storeRoms();
|
||||
const { currentPlatform, currentCollection } = storeToRefs(romsStore);
|
||||
|
||||
// Props
|
||||
const auth = storeAuth();
|
||||
const { currentPlatform } = storeToRefs(romsStore);
|
||||
const navigationStore = storeNavigation();
|
||||
const { activePlatformInfoDrawer } = storeToRefs(navigationStore);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-app-bar id="gallery-app-bar" elevation="0" density="compact">
|
||||
<v-app-bar
|
||||
id="gallery-app-bar"
|
||||
elevation="0"
|
||||
density="compact"
|
||||
mode="shift"
|
||||
app
|
||||
fixed
|
||||
top
|
||||
>
|
||||
<platform-icon
|
||||
v-if="currentPlatform"
|
||||
:slug="currentPlatform.slug"
|
||||
:name="currentPlatform.name"
|
||||
:size="36"
|
||||
class="ml-3 mr-2 platform-icon"
|
||||
class="mx-3 platform-icon"
|
||||
:class="{ active: activePlatformInfoDrawer }"
|
||||
@click="navigationStore.switchActivePlatformInfoDrawer"
|
||||
/>
|
||||
<firmware-btn />
|
||||
<filter-btn />
|
||||
<filter-text-field v-if="!xs" />
|
||||
<div v-if="xs" class="flex-grow-1" />
|
||||
<selecting-btn />
|
||||
<gallery-view-btn />
|
||||
<v-menu location="bottom">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-if="auth.scopes.includes('roms.write')"
|
||||
v-bind="props"
|
||||
rounded="0"
|
||||
variant="text"
|
||||
class="mr-0"
|
||||
icon="mdi-dots-vertical"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<admin-menu />
|
||||
</v-menu>
|
||||
<template #append>
|
||||
<selecting-btn />
|
||||
<gallery-view-btn />
|
||||
</template>
|
||||
</v-app-bar>
|
||||
|
||||
<platform-info-drawer />
|
||||
<filter-drawer />
|
||||
<firmware-drawer />
|
||||
</template>
|
||||
@@ -61,6 +59,16 @@ const auth = storeAuth();
|
||||
z-index: 999 !important;
|
||||
}
|
||||
.platform-icon {
|
||||
filter: drop-shadow(0px 0px 2px #a452fe);
|
||||
cursor: pointer;
|
||||
transition: filter 0.15s ease-in-out;
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
.platform-icon {
|
||||
filter: drop-shadow(0px 0px 1px rgba(var(--v-theme-romm-accent-1)));
|
||||
}
|
||||
.platform-icon:hover,
|
||||
.platform-icon.active {
|
||||
filter: drop-shadow(0px 0px 3px rgba(var(--v-theme-romm-accent-1)));
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -52,9 +52,11 @@ function deleteSelectedFirmware() {
|
||||
}
|
||||
|
||||
function updateDataTablePages() {
|
||||
pageCount.value = Math.ceil(
|
||||
Number(currentPlatform.value?.firmware?.length) / itemsPerPage.value,
|
||||
);
|
||||
if (currentPlatform.value?.firmware) {
|
||||
pageCount.value = Math.ceil(
|
||||
Number(currentPlatform.value.firmware.length) / itemsPerPage.value,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
watch(itemsPerPage, async () => {
|
||||
@@ -67,7 +69,12 @@ onMounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer v-model="activeFirmwareDrawer" mobile location="bottom">
|
||||
<v-navigation-drawer
|
||||
v-model="activeFirmwareDrawer"
|
||||
mobile
|
||||
floating
|
||||
location="bottom"
|
||||
>
|
||||
<v-data-table
|
||||
:items="currentPlatform?.firmware ?? []"
|
||||
:width="mdAndUp ? '60vw' : '95vw'"
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
<script setup lang="ts">
|
||||
import PlatformIcon from "@/components/common/Platform/Icon.vue";
|
||||
import RSection from "@/components/common/RSection.vue";
|
||||
import platformApi from "@/services/api/platform";
|
||||
import socket from "@/services/socket";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeNavigation from "@/stores/navigation";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import storeScanning from "@/stores/scanning";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, inject, ref, watch } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const { xs } = useDisplay();
|
||||
const viewportWidth = ref(window.innerWidth);
|
||||
const heartbeat = storeHeartbeat();
|
||||
const romsStore = storeRoms();
|
||||
const scanningStore = storeScanning();
|
||||
const { scanning } = storeToRefs(scanningStore);
|
||||
const { currentPlatform } = storeToRefs(romsStore);
|
||||
const auth = storeAuth();
|
||||
const navigationStore = storeNavigation();
|
||||
const { activePlatformInfoDrawer } = storeToRefs(navigationStore);
|
||||
const selectedAspectRatio = ref(0);
|
||||
const aspectRatioOptions = computed(() => [
|
||||
{
|
||||
name: "2 / 3",
|
||||
size: 2 / 3,
|
||||
source: "SteamGridDB",
|
||||
},
|
||||
{
|
||||
name: "3 / 4",
|
||||
size: 3 / 4,
|
||||
source: "IGDB / MobyGames",
|
||||
},
|
||||
{
|
||||
name: "1 / 1",
|
||||
size: 1 / 1,
|
||||
source: "Old squared cases",
|
||||
},
|
||||
]);
|
||||
|
||||
const platformInfoFields = [
|
||||
{ key: "name", label: "Name" },
|
||||
{ key: "slug", label: "Slug" },
|
||||
{ key: "fs_slug", label: "Filesystem folder name" },
|
||||
{ key: "category", label: "Category" },
|
||||
{ key: "generation", label: "Generation" },
|
||||
{ key: "family_name", label: "Family" },
|
||||
];
|
||||
|
||||
watch(
|
||||
() => currentPlatform.value?.aspect_ratio,
|
||||
(aspectRatio) => {
|
||||
if (aspectRatio) {
|
||||
// Find the index of the aspect ratio option that matches the current aspect ratio
|
||||
const defaultAspectRatio = aspectRatioOptions.value.findIndex(
|
||||
(option) => option.name == aspectRatio,
|
||||
);
|
||||
// If a matching aspect ratio option is found, update the selectedAspectRatio
|
||||
if (defaultAspectRatio !== -1) {
|
||||
selectedAspectRatio.value = defaultAspectRatio;
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }, // Execute the callback immediately with the current value
|
||||
);
|
||||
|
||||
// Functions
|
||||
async function scan() {
|
||||
scanningStore.set(true);
|
||||
|
||||
if (!socket.connected) socket.connect();
|
||||
|
||||
socket.emit("scan", {
|
||||
platforms: [romsStore.currentPlatform?.id],
|
||||
type: "quick",
|
||||
apis: heartbeat.getMetadataOptions().map((s) => s.value),
|
||||
});
|
||||
}
|
||||
|
||||
async function setAspectRatio() {
|
||||
if (currentPlatform.value) {
|
||||
const selectedOption = aspectRatioOptions.value[selectedAspectRatio.value];
|
||||
platformApi
|
||||
.updatePlatform({
|
||||
platform: {
|
||||
...currentPlatform.value,
|
||||
aspect_ratio: selectedOption.name,
|
||||
},
|
||||
})
|
||||
.then(({ data }) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: data.msg,
|
||||
icon: "mdi-check-bold",
|
||||
color: "green",
|
||||
});
|
||||
if (currentPlatform.value) {
|
||||
currentPlatform.value.aspect_ratio = selectedOption.name;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Failed to update aspect ratio: ${
|
||||
error.response?.data?.msg || error.message
|
||||
}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
v-model="activePlatformInfoDrawer"
|
||||
floating
|
||||
mobile
|
||||
:width="xs ? viewportWidth : '500'"
|
||||
v-if="currentPlatform"
|
||||
>
|
||||
<v-row no-gutters class="justify-center align-center">
|
||||
<v-col cols="12">
|
||||
<div class="text-center">
|
||||
<platform-icon
|
||||
:slug="currentPlatform.slug"
|
||||
:name="currentPlatform.name"
|
||||
class="platform-icon"
|
||||
:size="160"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<v-btn
|
||||
class="bg-terciary"
|
||||
@click="emitter?.emit('showUploadRomDialog', currentPlatform)"
|
||||
>
|
||||
<v-icon class="text-romm-green mr-2">mdi-upload</v-icon>
|
||||
Upload roms
|
||||
</v-btn>
|
||||
<v-btn
|
||||
:disabled="scanning"
|
||||
rounded="4"
|
||||
:loading="scanning"
|
||||
@click="scan"
|
||||
class="ml-2 bg-terciary"
|
||||
>
|
||||
<template #prepend>
|
||||
<v-icon :color="scanning ? '' : 'romm-accent-1'"
|
||||
>mdi-magnify-scan</v-icon
|
||||
>
|
||||
</template>
|
||||
Scan platform
|
||||
<template #loader>
|
||||
<v-progress-circular
|
||||
color="romm-accent-1"
|
||||
:width="2"
|
||||
:size="20"
|
||||
indeterminate
|
||||
/>
|
||||
</template>
|
||||
</v-btn>
|
||||
</div>
|
||||
<div class="mt-4 text-center">
|
||||
<a
|
||||
v-if="currentPlatform.igdb_id"
|
||||
style="text-decoration: none; color: inherit"
|
||||
:href="currentPlatform.url ? currentPlatform.url : ''"
|
||||
target="_blank"
|
||||
>
|
||||
<v-chip size="x-small" @click.stop>
|
||||
<span>IGDB</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>ID: {{ currentPlatform.igdb_id }}</span>
|
||||
</v-chip>
|
||||
</a>
|
||||
<v-chip
|
||||
size="x-small"
|
||||
class="ml-1"
|
||||
@click.stop
|
||||
:class="{ 'ml-1': currentPlatform.igdb_id }"
|
||||
v-if="currentPlatform.moby_id"
|
||||
>
|
||||
<span>Mobygames</span>
|
||||
<v-divider class="mx-2 border-opacity-25" vertical />
|
||||
<span>ID: {{ currentPlatform.moby_id }}</span>
|
||||
</v-chip>
|
||||
</div>
|
||||
<v-card class="mt-4 mx-4 bg-terciary fill-width" elevation="0">
|
||||
<v-card-text class="pa-4">
|
||||
<template
|
||||
v-for="(field, index) in platformInfoFields"
|
||||
:key="field.key"
|
||||
>
|
||||
<div
|
||||
v-if="
|
||||
currentPlatform[field.key as keyof typeof currentPlatform]
|
||||
"
|
||||
:class="{ 'mt-4': index !== 0 }"
|
||||
>
|
||||
<p class="text-subtitle-1 text-decoration-underline">
|
||||
{{ field.label }}
|
||||
</p>
|
||||
<p class="text-subtitle-2">
|
||||
{{
|
||||
currentPlatform[field.key as keyof typeof currentPlatform]
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="mt-4" no-gutters>
|
||||
<v-col cols="12">
|
||||
<r-section
|
||||
v-if="auth.scopes.includes('platforms.write')"
|
||||
icon="mdi-aspect-ratio"
|
||||
title="UI Settings"
|
||||
elevation="0"
|
||||
>
|
||||
<template #content>
|
||||
<v-item-group
|
||||
v-model="selectedAspectRatio"
|
||||
mandatory
|
||||
@update:model-value="setAspectRatio"
|
||||
>
|
||||
<v-row no-gutters class="text-center justify-center align-center">
|
||||
<v-col class="ma-2" v-for="aspectRatio in aspectRatioOptions">
|
||||
<v-item v-slot="{ isSelected, toggle }">
|
||||
<v-card
|
||||
:color="isSelected ? 'romm-accent-1' : 'romm-gray'"
|
||||
variant="outlined"
|
||||
@click="toggle"
|
||||
>
|
||||
<v-card-text
|
||||
class="pa-0 text-center align-center justify-center"
|
||||
>
|
||||
<v-img
|
||||
:aspect-ratio="aspectRatio.size"
|
||||
cover
|
||||
src="/assets/login_bg.png"
|
||||
:class="{ greyscale: !isSelected }"
|
||||
class="d-flex align-center justify-center"
|
||||
>
|
||||
<p class="text-h5 text-romm-white">
|
||||
{{ aspectRatio.name }}
|
||||
</p>
|
||||
</v-img>
|
||||
<p class="text-center mx-2 text-caption">
|
||||
{{ aspectRatio.source }}
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-item>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-item-group>
|
||||
</template>
|
||||
</r-section>
|
||||
<r-section
|
||||
v-if="auth.scopes.includes('platforms.write')"
|
||||
icon="mdi-alert"
|
||||
icon-color="red"
|
||||
title="Danger zone"
|
||||
elevation="0"
|
||||
>
|
||||
<template #content>
|
||||
<div class="text-center my-2">
|
||||
<v-btn
|
||||
class="text-romm-red bg-terciary"
|
||||
variant="flat"
|
||||
@click="
|
||||
emitter?.emit('showDeletePlatformDialog', currentPlatform)
|
||||
"
|
||||
>
|
||||
<v-icon class="text-romm-red mr-2">mdi-delete</v-icon>
|
||||
Delete platform
|
||||
</v-btn>
|
||||
</div>
|
||||
</template>
|
||||
</r-section>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-navigation-drawer>
|
||||
</template>
|
||||
<style scoped>
|
||||
.platform-icon {
|
||||
filter: drop-shadow(0px 0px 1px rgba(var(--v-theme-romm-accent-1)));
|
||||
}
|
||||
.greyscale {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
</style>
|
||||
@@ -78,11 +78,11 @@ function resetFilters() {
|
||||
|
||||
<template>
|
||||
<v-navigation-drawer
|
||||
@update:model-value="galleryFilterStore.switchActiveFilterDrawer()"
|
||||
floating
|
||||
width="300"
|
||||
v-model="activeFilterDrawer"
|
||||
mobile
|
||||
@update:model-value="galleryFilterStore.switchActiveFilterDrawer()"
|
||||
v-model="activeFilterDrawer"
|
||||
>
|
||||
<v-list>
|
||||
<v-list-item v-if="xs">
|
||||
|
||||
@@ -38,3 +38,14 @@ const emit = defineEmits(["update:modelValue"]);
|
||||
></v-list-item>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.text-romm-accent-1 {
|
||||
transition: color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.v-list-item-title,
|
||||
.v-icon {
|
||||
transition: color 0.3s ease-in-out;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import type { Collection } from "@/stores/collections";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
// Props
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
collection: Collection;
|
||||
@@ -20,6 +22,7 @@ withDefaults(
|
||||
},
|
||||
);
|
||||
const theme = useTheme();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -65,7 +68,7 @@ const theme = useTheme();
|
||||
? `/assets/default/cover/small_${theme.global.name.value}_fav.png`
|
||||
: `/assets/default/cover/small_${theme.global.name.value}_collection.png`
|
||||
"
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
>
|
||||
<div class="position-absolute append-inner">
|
||||
<slot name="append-inner"></slot>
|
||||
@@ -75,7 +78,7 @@ const theme = useTheme();
|
||||
<v-img
|
||||
:src="`/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`"
|
||||
cover
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
></v-img>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
|
||||
@@ -3,19 +3,21 @@ import type { SearchRomSchema } from "@/__generated__";
|
||||
import ActionBar from "@/components/common/Game/Card/ActionBar.vue";
|
||||
import GameCardFlags from "@/components/common/Game/Card/Flags.vue";
|
||||
import Sources from "@/components/common/Game/Card/Sources.vue";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import PlatformIcon from "@/components/common/Platform/Icon.vue";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import storeDownload from "@/stores/download";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import { type SimpleRom } from "@/stores/roms.js";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
// Props
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
rom: SimpleRom | SearchRomSchema;
|
||||
aspectRatio?: string | number;
|
||||
transformScale?: boolean;
|
||||
titleOnHover?: boolean;
|
||||
showFlags?: boolean;
|
||||
@@ -29,6 +31,7 @@ const props = withDefaults(
|
||||
src?: string;
|
||||
}>(),
|
||||
{
|
||||
aspectRatio: undefined,
|
||||
transformScale: false,
|
||||
titleOnHover: false,
|
||||
showFlags: false,
|
||||
@@ -42,6 +45,7 @@ const props = withDefaults(
|
||||
src: "",
|
||||
},
|
||||
);
|
||||
const platfotmsStore = storePlatforms();
|
||||
const romsStore = storeRoms();
|
||||
const emit = defineEmits(["click", "touchstart", "touchend"]);
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
@@ -58,7 +62,15 @@ const card = ref();
|
||||
const theme = useTheme();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const collectionsStore = storeCollections();
|
||||
const computedAspectRatio = computed(() => {
|
||||
const ratio =
|
||||
props.aspectRatio ||
|
||||
platfotmsStore.getAspectRatio(props.rom.platform_id) ||
|
||||
galleryViewStore.defaultAspectRatioCover;
|
||||
return parseFloat(ratio.toString());
|
||||
});
|
||||
|
||||
// Functions
|
||||
onMounted(() => {
|
||||
card.value.$el.addEventListener("contextmenu", (event: Event) => {
|
||||
event.preventDefault();
|
||||
@@ -123,7 +135,7 @@ onMounted(() => {
|
||||
? rom.igdb_url_cover
|
||||
: rom.moby_url_cover
|
||||
"
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
>
|
||||
<div v-bind="props" style="position: absolute; top: 0; width: 100%">
|
||||
<template v-if="titleOnHover">
|
||||
@@ -198,7 +210,7 @@ onMounted(() => {
|
||||
<v-img
|
||||
:src="`/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`"
|
||||
cover
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
></v-img>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
|
||||
@@ -19,6 +19,9 @@ const showLanguages = isNull(localStorage.getItem("settings.showLanguages"))
|
||||
const showSiblings = isNull(localStorage.getItem("settings.showSiblings"))
|
||||
? true
|
||||
: localStorage.getItem("settings.showSiblings") === "true";
|
||||
const showStatus = isNull(localStorage.getItem("settings.showStatus"))
|
||||
? true
|
||||
: localStorage.getItem("settings.showStatus") === "true";
|
||||
|
||||
const playingStatus = computed(() => {
|
||||
if (props.rom.rom_user.now_playing) return "now_playing";
|
||||
@@ -51,7 +54,7 @@ const playingStatus = computed(() => {
|
||||
</span>
|
||||
</v-chip>
|
||||
<v-chip
|
||||
v-if="playingStatus"
|
||||
v-if="playingStatus && showStatus"
|
||||
class="translucent-dark mr-1 mt-1"
|
||||
density="compact"
|
||||
:title="getTextForStatus(playingStatus)"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { IGDBRelatedGame } from "@/__generated__";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
// Props
|
||||
@@ -7,6 +8,7 @@ const props = defineProps<{
|
||||
game: IGDBRelatedGame;
|
||||
}>();
|
||||
const theme = useTheme();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const handleClick = () => {
|
||||
if (props.game.slug) {
|
||||
window.open(
|
||||
@@ -28,6 +30,7 @@ const handleClick = () => {
|
||||
open-delay="1000"
|
||||
>{{ game.name }}</v-tooltip
|
||||
>
|
||||
<!-- TODO: fix aspect ratio -->
|
||||
<v-img
|
||||
v-bind="props"
|
||||
:src="
|
||||
@@ -35,7 +38,7 @@ const handleClick = () => {
|
||||
? `https:${game.cover_url.replace('t_thumb', 't_cover_big')}`
|
||||
: `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
|
||||
"
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
|
||||
cover
|
||||
lazy
|
||||
><v-chip
|
||||
|
||||
@@ -6,13 +6,13 @@ import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, ref, computed } from "vue";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
|
||||
// Props
|
||||
const theme = useTheme();
|
||||
const { lgAndUp, mdAndUp, smAndUp, smAndDown } = useDisplay();
|
||||
const { lgAndUp, mdAndUp, smAndDown } = useDisplay();
|
||||
const heartbeat = storeHeartbeat();
|
||||
const route = useRoute();
|
||||
const show = ref(false);
|
||||
@@ -147,7 +147,7 @@ function closeDialog() {
|
||||
>
|
||||
<template #content>
|
||||
<v-row class="align-center pa-2" no-gutters>
|
||||
<v-col cols="12" md="8" lg="8" xl="9">
|
||||
<v-col cols="12" md="8" xl="9">
|
||||
<v-row class="px-2" no-gutters>
|
||||
<v-col>
|
||||
<v-text-field
|
||||
@@ -170,21 +170,18 @@ function closeDialog() {
|
||||
label="Filename"
|
||||
variant="outlined"
|
||||
required
|
||||
hide-details
|
||||
@keyup.enter="updateRom()"
|
||||
>
|
||||
<v-label
|
||||
v-if="smAndUp"
|
||||
id="file-name-label"
|
||||
class="text-caption"
|
||||
>
|
||||
<v-icon size="small" class="mr-1">
|
||||
mdi-folder-file-outline
|
||||
</v-icon>
|
||||
<span>
|
||||
/romm/library/{{ rom.file_path }}/{{ rom.file_name }}
|
||||
</span>
|
||||
</v-label>
|
||||
<template #details>
|
||||
<v-label class="text-caption text-wrap">
|
||||
<v-icon size="small" class="mr-2 text-romm-accent-1">
|
||||
mdi-folder-file-outline
|
||||
</v-icon>
|
||||
<span>
|
||||
/romm/library/{{ rom.file_path }}/{{ rom.file_name }}
|
||||
</span>
|
||||
</v-label>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -226,9 +223,9 @@ function closeDialog() {
|
||||
</v-btn-group>
|
||||
</v-row>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-row class="justify-center">
|
||||
<v-col :class="{ 'mobile-cover': smAndDown, 'pa-8': !smAndDown }">
|
||||
<v-col cols="12" md="4" xl="3">
|
||||
<v-row class="justify-center" no-gutters>
|
||||
<v-col style="max-width: 240px" :class="{ 'my-4': smAndDown }">
|
||||
<game-card :rom="rom" :src="imagePreviewUrl">
|
||||
<template #append-inner-right>
|
||||
<v-btn-group rounded="0" divided density="compact">
|
||||
@@ -241,7 +238,7 @@ function closeDialog() {
|
||||
@click="
|
||||
emitter?.emit(
|
||||
'showSearchCoverDialog',
|
||||
rom?.name as string,
|
||||
rom.name as string,
|
||||
)
|
||||
"
|
||||
>
|
||||
@@ -276,7 +273,7 @@ function closeDialog() {
|
||||
</game-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="smAndDown" class="justify-space-between pa-4">
|
||||
<v-row v-if="smAndDown" class="justify-space-between pa-2" no-gutters>
|
||||
<v-btn-group divided density="compact" class="my-1">
|
||||
<v-btn
|
||||
:disabled="noMetadataMatch"
|
||||
@@ -301,18 +298,3 @@ function closeDialog() {
|
||||
</template>
|
||||
</r-dialog>
|
||||
</template>
|
||||
<style scoped>
|
||||
.mobile-cover {
|
||||
min-width: 240px;
|
||||
min-height: 330px;
|
||||
max-width: 240px;
|
||||
max-height: 330px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
#file-name-label {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { SearchRomSchema } from "@/__generated__";
|
||||
import GameCard from "@/components/common/Game/Card/Base.vue";
|
||||
import RDialog from "@/components/common/RDialog.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
@@ -22,6 +23,7 @@ const { xs, lgAndUp } = useDisplay();
|
||||
const show = ref(false);
|
||||
const rom = ref<SimpleRom | null>(null);
|
||||
const romsStore = storeRoms();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const searching = ref(false);
|
||||
const route = useRoute();
|
||||
const searchTerm = ref("");
|
||||
@@ -330,6 +332,7 @@ onBeforeUnmount(() => {
|
||||
v-for="matchedRom in filteredMatchedRoms"
|
||||
>
|
||||
<game-card
|
||||
v-if="rom"
|
||||
@click="showSources(matchedRom)"
|
||||
:rom="matchedRom"
|
||||
title-on-footer
|
||||
@@ -394,7 +397,7 @@ onBeforeUnmount(() => {
|
||||
? `/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`
|
||||
: source.url_cover
|
||||
"
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
|
||||
cover
|
||||
lazy
|
||||
>
|
||||
|
||||
@@ -184,7 +184,7 @@ onBeforeUnmount(() => {
|
||||
</v-row>
|
||||
</template>
|
||||
<template #content>
|
||||
<v-row no-gutters>
|
||||
<v-row no-gutters class="align-content-start align-center">
|
||||
<v-col
|
||||
class="pa-1"
|
||||
cols="4"
|
||||
@@ -194,13 +194,15 @@ onBeforeUnmount(() => {
|
||||
v-for="rom in filteredRoms"
|
||||
>
|
||||
<game-card
|
||||
:key="rom.updated_at"
|
||||
:rom="rom"
|
||||
@click="onGameClick({ rom, event: $event })"
|
||||
title-on-hover
|
||||
show-flags
|
||||
transform-scale
|
||||
show-platform-icon
|
||||
show-fav
|
||||
transform-scale
|
||||
show-action-bar
|
||||
show-platform-icon
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -28,7 +28,7 @@ function clear() {
|
||||
<v-navigation-drawer
|
||||
:location="smAndDown ? 'top' : 'left'"
|
||||
mobile
|
||||
width="400"
|
||||
width="500"
|
||||
v-model="activeCollectionsDrawer"
|
||||
class="bg-terciary"
|
||||
>
|
||||
|
||||
@@ -9,10 +9,13 @@ import UserBtn from "@/components/common/Navigation/UserBtn.vue";
|
||||
</script>
|
||||
<template>
|
||||
<v-app-bar
|
||||
:elevation="0"
|
||||
elevation="0"
|
||||
class="bg-primary justify-center px-1"
|
||||
mode="shift"
|
||||
height="45"
|
||||
app
|
||||
fixed
|
||||
left
|
||||
>
|
||||
<template #prepend>
|
||||
<home-btn />
|
||||
|
||||
@@ -21,7 +21,7 @@ function clear() {
|
||||
:location="smAndDown ? 'top' : 'left'"
|
||||
mobile
|
||||
@update:model-value="clear"
|
||||
width="400"
|
||||
width="500"
|
||||
v-model="activePlatformsDrawer"
|
||||
class="bg-terciary"
|
||||
>
|
||||
|
||||
@@ -43,7 +43,7 @@ async function logout() {
|
||||
<v-navigation-drawer
|
||||
:location="smAndDown ? 'top' : 'left'"
|
||||
mobile
|
||||
width="400"
|
||||
width="500"
|
||||
v-model="activeSettingsDrawer"
|
||||
class="bg-terciary"
|
||||
>
|
||||
|
||||
@@ -10,21 +10,27 @@ const auth = storeAuth();
|
||||
const { user } = storeToRefs(auth);
|
||||
</script>
|
||||
<template>
|
||||
<v-hover v-slot="{ isHovering, props: hoverProps }">
|
||||
<v-avatar
|
||||
@click="navigationStore.switchActiveSettingsDrawer"
|
||||
class="pointer"
|
||||
size="35"
|
||||
v-bind="hoverProps"
|
||||
:class="{ 'border-romm-accent-1': isHovering }"
|
||||
>
|
||||
<v-img
|
||||
:src="
|
||||
user?.avatar_path
|
||||
? `/assets/romm/assets/${user?.avatar_path}?ts=${user?.updated_at}`
|
||||
: defaultAvatarPath
|
||||
"
|
||||
/>
|
||||
</v-avatar>
|
||||
</v-hover>
|
||||
<v-avatar
|
||||
@click="navigationStore.switchActiveSettingsDrawer"
|
||||
class="pointer"
|
||||
size="35"
|
||||
:class="{ active: navigationStore.activeSettingsDrawer }"
|
||||
>
|
||||
<v-img
|
||||
:src="
|
||||
user?.avatar_path
|
||||
? `/assets/romm/assets/${user?.avatar_path}?ts=${user?.updated_at}`
|
||||
: defaultAvatarPath
|
||||
"
|
||||
/>
|
||||
</v-avatar>
|
||||
</template>
|
||||
<style scoped>
|
||||
.v-avatar {
|
||||
transition: filter 0.15s ease-in-out;
|
||||
}
|
||||
.v-avatar:hover,
|
||||
.v-avatar.active {
|
||||
filter: drop-shadow(0px 0px 2px rgba(var(--v-theme-romm-accent-1)));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,8 +16,8 @@ withDefaults(
|
||||
scrollContent?: boolean;
|
||||
showRommIcon?: boolean;
|
||||
icon?: string | null;
|
||||
width?: string;
|
||||
height?: string;
|
||||
width?: number | string;
|
||||
height?: number | string;
|
||||
}>(),
|
||||
{
|
||||
loadingCondition: false,
|
||||
@@ -27,8 +27,8 @@ withDefaults(
|
||||
scrollContent: false,
|
||||
showRommIcon: false,
|
||||
icon: null,
|
||||
width: "",
|
||||
height: "",
|
||||
width: "auto",
|
||||
height: "auto",
|
||||
},
|
||||
);
|
||||
const emit = defineEmits(["update:modelValue", "close"]);
|
||||
@@ -62,7 +62,7 @@ onMounted(() => {
|
||||
no-click-animation
|
||||
persistent
|
||||
>
|
||||
<v-card rounded="0" :height="height">
|
||||
<v-card rounded="0" :min-height="height" :max-height="height">
|
||||
<v-toolbar density="compact" class="bg-terciary">
|
||||
<v-icon v-if="icon" :icon="icon" class="ml-5" />
|
||||
<romm-iso :size="30" class="mx-4" v-if="showRommIcon" />
|
||||
@@ -90,12 +90,12 @@ onMounted(() => {
|
||||
|
||||
<v-card-text
|
||||
id="r-dialog-content"
|
||||
class="pa-1"
|
||||
class="pa-1 d-flex flex-column"
|
||||
:class="{ scroll: scrollContent }"
|
||||
>
|
||||
<v-row
|
||||
v-if="loadingCondition"
|
||||
class="justify-center align-center h-100"
|
||||
class="justify-center align-center flex-grow-1"
|
||||
no-gutters
|
||||
>
|
||||
<v-progress-circular
|
||||
@@ -108,7 +108,7 @@ onMounted(() => {
|
||||
|
||||
<v-row
|
||||
v-if="!loadingCondition && emptyStateCondition"
|
||||
class="justify-center align-center h-100"
|
||||
class="justify-center align-center flex-grow-1"
|
||||
no-gutters
|
||||
>
|
||||
<empty-game v-if="emptyStateType == 'game'" />
|
||||
@@ -117,7 +117,10 @@ onMounted(() => {
|
||||
<slot v-else name="emptyState"></slot>
|
||||
</v-row>
|
||||
|
||||
<slot name="content"></slot>
|
||||
<slot
|
||||
v-if="!loadingCondition && !emptyStateCondition"
|
||||
name="content"
|
||||
></slot>
|
||||
</v-card-text>
|
||||
<v-card-text v-if="hasAppendSlot" class="pa-1">
|
||||
<slot name="append"></slot>
|
||||
|
||||
@@ -3,15 +3,16 @@ withDefaults(
|
||||
defineProps<{
|
||||
title: string;
|
||||
icon: string;
|
||||
iconColor?: string;
|
||||
}>(),
|
||||
{ title: "", icon: "" },
|
||||
{ title: "", icon: "", iconColor: "" },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<v-card rounded="0">
|
||||
<v-toolbar class="bg-terciary" density="compact">
|
||||
<v-toolbar-title class="text-button">
|
||||
<v-icon class="mr-3">{{ icon }}</v-icon>
|
||||
<v-icon :color="iconColor" class="mr-3">{{ icon }}</v-icon>
|
||||
{{ title }}
|
||||
</v-toolbar-title>
|
||||
<slot name="toolbar-append"></slot>
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, onBeforeUnmount, ref } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
|
||||
// Props
|
||||
const { lgAndUp } = useDisplay();
|
||||
@@ -15,6 +16,7 @@ const searchTerm = ref("");
|
||||
const coverType = ref("all");
|
||||
const covers = ref<SearchCoverSchema[]>([]);
|
||||
const filteredCovers = ref<SearchCoverSchema[]>();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const panels = ref([0]);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("showSearchCoverDialog", (term) => {
|
||||
@@ -23,6 +25,7 @@ emitter?.on("showSearchCoverDialog", (term) => {
|
||||
if (searchTerm.value) searchCovers();
|
||||
});
|
||||
|
||||
// Functions
|
||||
async function searchCovers() {
|
||||
covers.value = [];
|
||||
|
||||
@@ -175,20 +178,22 @@ onBeforeUnmount(() => {
|
||||
v-for="resource in game.resources"
|
||||
>
|
||||
<v-hover v-slot="{ isHovering, props: hoverProps }">
|
||||
<!-- TODO: fix aspect ratio -->
|
||||
<v-img
|
||||
v-bind="hoverProps"
|
||||
:class="{ 'on-hover': isHovering }"
|
||||
class="transform-scale pointer"
|
||||
@click="selectCover(resource.url)"
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
|
||||
:src="resource.thumb"
|
||||
cover
|
||||
>
|
||||
<template #error>
|
||||
<!-- TODO: fix aspect ratio -->
|
||||
<v-img
|
||||
:src="resource.url"
|
||||
cover
|
||||
:aspect-ratio="2 / 3"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCover"
|
||||
></v-img>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
|
||||
@@ -27,7 +27,7 @@ function onGameClick(emitData: { rom: SimpleRom; event: MouseEvent }) {
|
||||
<template>
|
||||
<r-section icon="mdi-shimmer" title="Recently added">
|
||||
<template #content>
|
||||
<v-row class="flex-nowrap overflow-x-auto" no-gutters>
|
||||
<v-row class="flex-nowrap overflow-x-auto align-center" no-gutters>
|
||||
<v-col
|
||||
v-for="rom in recentRoms"
|
||||
:key="rom.id"
|
||||
@@ -40,13 +40,14 @@ function onGameClick(emitData: { rom: SimpleRom; event: MouseEvent }) {
|
||||
>
|
||||
<game-card
|
||||
:key="rom.updated_at"
|
||||
@click="onGameClick"
|
||||
:rom="rom"
|
||||
@click="onGameClick"
|
||||
title-on-hover
|
||||
show-flags
|
||||
show-fav
|
||||
transform-scale
|
||||
show-action-bar
|
||||
show-platform-icon
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
@@ -23,6 +23,9 @@ const languagesRef = ref(
|
||||
isNull(storedLanguages) ? true : storedLanguages === "true",
|
||||
);
|
||||
|
||||
const storedStatus = localStorage.getItem("settings.showStatus");
|
||||
const statusRef = ref(isNull(storedStatus) ? true : storedStatus === "true");
|
||||
|
||||
// Functions to update localStorage
|
||||
const toggleGroupRoms = (value: boolean) => {
|
||||
groupRomsRef.value = value;
|
||||
@@ -44,6 +47,11 @@ const toggleLanguages = (value: boolean) => {
|
||||
localStorage.setItem("settings.showLanguages", value.toString());
|
||||
};
|
||||
|
||||
const toggleStatus = (value: boolean) => {
|
||||
statusRef.value = value;
|
||||
localStorage.setItem("settings.showStatus", value.toString());
|
||||
};
|
||||
|
||||
const options = computed(() => [
|
||||
{
|
||||
title: "Group roms",
|
||||
@@ -79,6 +87,15 @@ const options = computed(() => [
|
||||
model: languagesRef,
|
||||
modelTrigger: toggleLanguages,
|
||||
},
|
||||
{
|
||||
title: "Show status",
|
||||
description:
|
||||
"Show status icons in the gallery (backlogged, playing, completed, etc)",
|
||||
iconEnabled: "mdi-check-circle-outline",
|
||||
iconDisabled: "mdi-close-circle-outline",
|
||||
model: statusRef,
|
||||
modelTrigger: toggleStatus,
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ async function updatePlatform({
|
||||
}: {
|
||||
platform: Platform;
|
||||
}): Promise<{ data: MessageResponse }> {
|
||||
return api.delete(`/platforms/${platform.id}`);
|
||||
return api.put(`/platforms/${platform.id}`, platform);
|
||||
}
|
||||
|
||||
async function deletePlatform({
|
||||
|
||||
@@ -22,9 +22,6 @@ export default defineStore("config", {
|
||||
EXCLUDED_MULTI_PARTS_FILES: [] as string[],
|
||||
PLATFORMS_BINDING: {} as Record<string, string>,
|
||||
PLATFORMS_VERSIONS: {} as Record<string, string>,
|
||||
ROMS_FOLDER_NAME: "",
|
||||
FIRMWARE_FOLDER_NAME: "",
|
||||
HIGH_PRIO_STRUCTURE_PATH: "",
|
||||
} as ConfigResponse,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -5,6 +5,9 @@ export default defineStore("galleryView", {
|
||||
currentView: JSON.parse(
|
||||
localStorage.getItem("currentView") ?? "0",
|
||||
) as number,
|
||||
defaultAspectRatioCover: 2 / 3,
|
||||
defaultAspectRatioCollection: 2 / 3,
|
||||
defaultAspectRatioScreenshot: 16 / 9,
|
||||
activeFirmwareDrawer: false,
|
||||
scrolledToTop: false,
|
||||
scroll: 0,
|
||||
|
||||
@@ -5,24 +5,26 @@ export default defineStore("navigation", {
|
||||
activePlatformsDrawer: false,
|
||||
activeCollectionsDrawer: false,
|
||||
activeSettingsDrawer: false,
|
||||
activePlatformInfoDrawer: false,
|
||||
}),
|
||||
|
||||
actions: {
|
||||
switchActivePlatformsDrawer() {
|
||||
this.activeCollectionsDrawer = false;
|
||||
this.activeSettingsDrawer = false;
|
||||
this.resetDrawersExcept("activePlatformsDrawer");
|
||||
this.activePlatformsDrawer = !this.activePlatformsDrawer;
|
||||
},
|
||||
switchActiveCollectionsDrawer() {
|
||||
this.activePlatformsDrawer = false;
|
||||
this.activeSettingsDrawer = false;
|
||||
this.resetDrawersExcept("activeCollectionsDrawer");
|
||||
this.activeCollectionsDrawer = !this.activeCollectionsDrawer;
|
||||
},
|
||||
switchActiveSettingsDrawer() {
|
||||
this.activePlatformsDrawer = false;
|
||||
this.activeCollectionsDrawer = false;
|
||||
this.resetDrawersExcept("activeSettingsDrawer");
|
||||
this.activeSettingsDrawer = !this.activeSettingsDrawer;
|
||||
},
|
||||
switchActivePlatformInfoDrawer() {
|
||||
this.resetDrawersExcept("activePlatformInfoDrawer");
|
||||
this.activePlatformInfoDrawer = !this.activePlatformInfoDrawer;
|
||||
},
|
||||
goHome() {
|
||||
this.resetDrawers();
|
||||
this.$router.push({ name: "dashboard" });
|
||||
@@ -35,6 +37,21 @@ export default defineStore("navigation", {
|
||||
this.activePlatformsDrawer = false;
|
||||
this.activeCollectionsDrawer = false;
|
||||
this.activeSettingsDrawer = false;
|
||||
this.activePlatformInfoDrawer = false;
|
||||
},
|
||||
resetDrawersExcept(drawer: string) {
|
||||
this.activePlatformsDrawer =
|
||||
drawer === "activePlatformsDrawer" ? this.activePlatformsDrawer : false;
|
||||
this.activeCollectionsDrawer =
|
||||
drawer === "activeCollectionsDrawer"
|
||||
? this.activeCollectionsDrawer
|
||||
: false;
|
||||
this.activeSettingsDrawer =
|
||||
drawer === "activeSettingsDrawer" ? this.activeSettingsDrawer : false;
|
||||
this.activePlatformInfoDrawer =
|
||||
drawer === "activePlatformInfoDrawer"
|
||||
? this.activePlatformInfoDrawer
|
||||
: false;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,21 +1,22 @@
|
||||
import { defineStore } from "pinia";
|
||||
import type { PlatformSchema } from "@/__generated__";
|
||||
import { uniqBy } from "lodash";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export type Platform = PlatformSchema;
|
||||
|
||||
export default defineStore("platforms", {
|
||||
state: () => {
|
||||
return {
|
||||
all: [] as Platform[],
|
||||
allPlatforms: [] as Platform[],
|
||||
searchText: "" as string,
|
||||
};
|
||||
},
|
||||
getters: {
|
||||
totalGames: ({ all: value }) =>
|
||||
totalGames: ({ allPlatforms: value }) =>
|
||||
value.reduce((count, p) => count + p.rom_count, 0),
|
||||
filledPlatforms: ({ all }) => all.filter((p) => p.rom_count > 0),
|
||||
filteredPlatforms: ({ all, searchText }) =>
|
||||
filledPlatforms: ({ allPlatforms: all }) =>
|
||||
all.filter((p) => p.rom_count > 0),
|
||||
filteredPlatforms: ({ allPlatforms: all, searchText }) =>
|
||||
all.filter(
|
||||
(p) =>
|
||||
p.rom_count > 0 &&
|
||||
@@ -24,28 +25,37 @@ export default defineStore("platforms", {
|
||||
},
|
||||
actions: {
|
||||
_reorder() {
|
||||
this.all = this.all.sort((a, b) => {
|
||||
this.allPlatforms = this.allPlatforms.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
this.all = uniqBy(this.all, "id");
|
||||
this.allPlatforms = uniqBy(this.allPlatforms, "id");
|
||||
},
|
||||
set(platforms: Platform[]) {
|
||||
this.all = platforms;
|
||||
this.allPlatforms = platforms;
|
||||
},
|
||||
add(platform: Platform) {
|
||||
this.all.push(platform);
|
||||
this.allPlatforms.push(platform);
|
||||
this._reorder();
|
||||
},
|
||||
exists(platform: Platform) {
|
||||
return this.all.filter((p) => p.fs_slug == platform.fs_slug).length > 0;
|
||||
return (
|
||||
this.allPlatforms.filter((p) => p.fs_slug == platform.fs_slug).length >
|
||||
0
|
||||
);
|
||||
},
|
||||
remove(platform: Platform) {
|
||||
this.all = this.all.filter((p) => {
|
||||
this.allPlatforms = this.allPlatforms.filter((p) => {
|
||||
return p.slug !== platform.slug;
|
||||
});
|
||||
},
|
||||
get(platformId: number) {
|
||||
return this.all.find((p) => p.id === platformId);
|
||||
return this.allPlatforms.find((p) => p.id === platformId);
|
||||
},
|
||||
getAspectRatio(platformId: number): number {
|
||||
const platform = this.allPlatforms.find((p) => p.id === platformId);
|
||||
return platform && platform.aspect_ratio
|
||||
? parseFloat(eval(platform.aspect_ratio as string))
|
||||
: 2 / 3;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { isNull } from "lodash";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import type { FirmwareSchema, SaveSchema, StateSchema } from "@/__generated__";
|
||||
import RAvatar from "@/components/common/Game/RAvatar.vue";
|
||||
import firmwareApi from "@/services/api/firmware";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { formatBytes, formatTimestamp, getSupportedEJSCores } from "@/utils";
|
||||
import Player from "@/views/EmulatorJS/Player.vue";
|
||||
import { isNull } from "lodash";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
// Props
|
||||
const route = useRoute();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const { defaultAspectRatioScreenshot } = storeToRefs(galleryViewStore);
|
||||
const rom = ref<DetailedRom | null>(null);
|
||||
const firmwareOptions = ref<FirmwareSchema[]>([]);
|
||||
const biosRef = ref<FirmwareSchema | null>(null);
|
||||
@@ -27,6 +30,7 @@ const script = document.createElement("script");
|
||||
script.src = "/assets/emulatorjs/loader.js";
|
||||
script.async = true;
|
||||
|
||||
// Functions
|
||||
function onPlay() {
|
||||
window.EJS_fullscreenOnLoaded = fullScreenOnPlay.value;
|
||||
document.body.appendChild(script);
|
||||
@@ -100,6 +104,7 @@ onMounted(async () => {
|
||||
md="8"
|
||||
xl="10"
|
||||
id="game-wrapper"
|
||||
:style="`aspect-ratio: ${defaultAspectRatioScreenshot}`"
|
||||
class="bg-primary"
|
||||
rounded
|
||||
>
|
||||
@@ -376,9 +381,3 @@ onMounted(async () => {
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#game-wrapper {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -270,7 +270,7 @@ onBeforeUnmount(() => {
|
||||
<template v-if="filteredRoms.length > 0">
|
||||
<v-row
|
||||
no-gutters
|
||||
class="overflow-hidden"
|
||||
class="overflow-hidden align-center"
|
||||
:class="{ 'pa-1': currentView != 2 }"
|
||||
>
|
||||
<!-- Gallery cards view -->
|
||||
|
||||
@@ -5,7 +5,6 @@ import EmptyGame from "@/components/common/EmptyGame.vue";
|
||||
import EmptyPlatform from "@/components/common/EmptyPlatform.vue";
|
||||
import GameCard from "@/components/common/Game/Card/Base.vue";
|
||||
import GameDataTable from "@/components/common/Game/Table.vue";
|
||||
import platformApi from "@/services/api/platform";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeGalleryFilter, { type FilterType } from "@/stores/galleryFilter";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
@@ -15,7 +14,7 @@ import type { Events } from "@/types/emitter";
|
||||
import { normalizeString, views } from "@/utils";
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
@@ -25,7 +24,8 @@ const route = useRoute();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { scrolledToTop, currentView } = storeToRefs(galleryViewStore);
|
||||
const platforms = storePlatforms();
|
||||
const platformsStore = storePlatforms();
|
||||
const { allPlatforms } = storeToRefs(platformsStore);
|
||||
const romsStore = storeRoms();
|
||||
const {
|
||||
allRoms,
|
||||
@@ -218,66 +218,80 @@ const filterToSetFilter: Record<FilterType, Function> = {
|
||||
|
||||
onMounted(async () => {
|
||||
const routePlatformId = Number(route.params.platform);
|
||||
const routePlatform = platforms.get(routePlatformId);
|
||||
|
||||
if (!routePlatform) {
|
||||
await platformApi
|
||||
.getPlatform(routePlatformId)
|
||||
.then((data) => {
|
||||
platforms.add(data.data);
|
||||
romsStore.setCurrentPlatform(data.data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
noPlatformError.value = true;
|
||||
});
|
||||
} else {
|
||||
romsStore.setCurrentPlatform(routePlatform);
|
||||
}
|
||||
watch(
|
||||
() => allPlatforms.value,
|
||||
(platforms) => {
|
||||
if (
|
||||
platforms.length > 0 &&
|
||||
platforms.some((platform) => platform.id === routePlatformId)
|
||||
) {
|
||||
const platform = platforms.find(
|
||||
(platform) => platform.id === routePlatformId,
|
||||
);
|
||||
|
||||
if (!noPlatformError.value) {
|
||||
resetGallery();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
// Check if the current platform is different or no ROMs have been loaded
|
||||
if (
|
||||
(currentPlatform.value?.id !== routePlatformId ||
|
||||
allRoms.value.length === 0) &&
|
||||
platform
|
||||
) {
|
||||
romsStore.setCurrentPlatform(platform);
|
||||
resetGallery();
|
||||
fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
|
||||
// Check if there are query params to set filters
|
||||
if (route.query.filter && route.query.value) {
|
||||
const filter = route.query.filter as FilterType;
|
||||
const value = route.query.value as string;
|
||||
filterToSetFilter[filter](value);
|
||||
onFilterChange(); // Update the UI
|
||||
router.replace({ query: {} }); // Clear query params
|
||||
}
|
||||
// Check for query params to set filters
|
||||
if (route.query.filter && route.query.value) {
|
||||
const filter = route.query.filter as FilterType;
|
||||
const value = route.query.value as string;
|
||||
filterToSetFilter[filter](value);
|
||||
onFilterChange(); // Update the UI
|
||||
router.replace({ query: {} }); // Clear query params
|
||||
}
|
||||
|
||||
window.addEventListener("wheel", onScroll);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
}
|
||||
window.addEventListener("wheel", onScroll);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
}
|
||||
},
|
||||
{ immediate: true }, // Ensure watcher is triggered immediately
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
// Triggers when change param of the same route
|
||||
// Reset store if switching to another platform
|
||||
if (to.path === from.path) return true;
|
||||
// Avoid unnecessary actions if navigating within the same path
|
||||
if (to.path === from.path) return;
|
||||
|
||||
resetGallery();
|
||||
|
||||
const routePlatformId = Number(to.params.platform);
|
||||
const routePlatform = platforms.get(routePlatformId);
|
||||
if (!routePlatform) {
|
||||
const { data } = await platformApi.getPlatform(routePlatformId);
|
||||
platforms.add(data);
|
||||
} else {
|
||||
romsStore.setCurrentPlatform(routePlatform);
|
||||
}
|
||||
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
watch(
|
||||
() => allPlatforms.value,
|
||||
(platforms) => {
|
||||
if (platforms.length > 0) {
|
||||
const platform = platforms.find(
|
||||
(platform) => platform.id === routePlatformId,
|
||||
);
|
||||
|
||||
return true;
|
||||
// Only trigger fetchRoms if switching platforms or ROMs are not loaded
|
||||
if (
|
||||
(currentPlatform.value?.id !== routePlatformId ||
|
||||
allRoms.value.length === 0) &&
|
||||
platform
|
||||
) {
|
||||
romsStore.setCurrentPlatform(platform);
|
||||
fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }, // Ensure watcher is triggered immediately
|
||||
);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
romsStore.setCurrentPlatform(null);
|
||||
window.removeEventListener("wheel", onScroll);
|
||||
window.removeEventListener("scroll", onScroll);
|
||||
});
|
||||
@@ -306,6 +320,7 @@ onBeforeUnmount(() => {
|
||||
:xl="views[currentView]['size-xl']"
|
||||
>
|
||||
<game-card
|
||||
v-if="currentPlatform"
|
||||
:key="rom.updated_at"
|
||||
:rom="rom"
|
||||
title-on-hover
|
||||
|
||||
@@ -7,25 +7,23 @@ import GameInfo from "@/components/Details/Info/GameInfo.vue";
|
||||
import Personal from "@/components/Details/Personal.vue";
|
||||
import RelatedGames from "@/components/Details/RelatedGames.vue";
|
||||
import Saves from "@/components/Details/Saves.vue";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import States from "@/components/Details/States.vue";
|
||||
import TitleInfo from "@/components/Details/Title.vue";
|
||||
import EmptyGame from "@/components/common/EmptyGame.vue";
|
||||
import Cover from "@/components/common/Game/Card/Base.vue";
|
||||
import platformApi from "@/services/api/platform";
|
||||
import GameCard from "@/components/common/Game/Card/Base.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeDownload from "@/stores/download";
|
||||
import type { Platform } from "@/stores/platforms";
|
||||
import storeRoms from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject, onBeforeMount, ref, watch } from "vue";
|
||||
import { onBeforeRouteLeave, useRoute } from "vue-router";
|
||||
import { useRoute } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const route = useRoute();
|
||||
const platform = ref<Platform>();
|
||||
const tab = ref<
|
||||
| "details"
|
||||
| "saves"
|
||||
@@ -39,9 +37,11 @@ const { smAndDown, mdAndDown, mdAndUp, lgAndUp } = useDisplay();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const noRomError = ref(false);
|
||||
const romsStore = storeRoms();
|
||||
const { currentRom } = storeToRefs(romsStore);
|
||||
const platfotmsStore = storePlatforms();
|
||||
const { currentRom, gettingRoms } = storeToRefs(romsStore);
|
||||
|
||||
async function fetchDetails() {
|
||||
gettingRoms.value = true;
|
||||
await romApi
|
||||
.getRom({ romId: parseInt(route.params.rom as string) })
|
||||
.then(({ data }) => {
|
||||
@@ -53,50 +53,40 @@ async function fetchDetails() {
|
||||
})
|
||||
.finally(() => {
|
||||
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
|
||||
gettingRoms.value = false;
|
||||
});
|
||||
|
||||
if (!noRomError.value) {
|
||||
await platformApi
|
||||
.getPlatform(currentRom.value?.platform_id)
|
||||
.then((response) => {
|
||||
platform.value = response.data;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
})
|
||||
.finally(() => {
|
||||
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onBeforeMount(async () => {
|
||||
emitter?.emit("showLoadingDialog", { loading: true, scrim: false });
|
||||
if (currentRom.value?.id == parseInt(route.params.rom as string)) {
|
||||
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
|
||||
} else {
|
||||
const romId = parseInt(route.params.rom as string);
|
||||
|
||||
// Only fetch details if the currentRom ID differs
|
||||
if (currentRom.value?.id !== romId) {
|
||||
emitter?.emit("showLoadingDialog", { loading: true, scrim: false });
|
||||
await fetchDetails();
|
||||
} else {
|
||||
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
|
||||
}
|
||||
|
||||
const downloadStore = storeDownload();
|
||||
downloadStore.clear();
|
||||
});
|
||||
|
||||
onBeforeRouteLeave(() => {
|
||||
currentRom.value = null;
|
||||
return true;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => route.fullPath,
|
||||
async () => {
|
||||
await fetchDetails();
|
||||
const romId = parseInt(route.params.rom as string);
|
||||
|
||||
// Only fetch details if the currentRom ID differs
|
||||
if (currentRom.value?.id !== romId) {
|
||||
await fetchDetails();
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- TODO: review layout on certain roms - ej: mortal kombat 2 for gb -->
|
||||
<template v-if="currentRom && platform">
|
||||
<template v-if="currentRom && !gettingRoms">
|
||||
<background-header />
|
||||
|
||||
<v-row
|
||||
@@ -111,10 +101,16 @@ watch(
|
||||
class="cover"
|
||||
:class="{
|
||||
'cover-desktop': mdAndUp,
|
||||
'cover-mobile': smAndDown,
|
||||
}"
|
||||
:style="
|
||||
smAndDown
|
||||
? platfotmsStore.getAspectRatio(currentRom.platform_id) == 1 / 1
|
||||
? 'margin-top: -220px;'
|
||||
: 'margin-top: -280px;'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<cover
|
||||
<game-card
|
||||
:key="currentRom.updated_at"
|
||||
:pointerOnHover="false"
|
||||
:rom="currentRom"
|
||||
@@ -129,8 +125,14 @@ watch(
|
||||
class="px-5"
|
||||
:class="{
|
||||
'info-lg': mdAndUp,
|
||||
'info-mobile': smAndDown,
|
||||
}"
|
||||
:style="
|
||||
smAndDown
|
||||
? platfotmsStore.getAspectRatio(currentRom.platform_id) == 1 / 1
|
||||
? 'margin-top: -40px;'
|
||||
: 'margin-top: 100px;'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="px-3 pb-3"
|
||||
@@ -139,7 +141,7 @@ watch(
|
||||
'justify-center': smAndDown,
|
||||
}"
|
||||
>
|
||||
<title-info :rom="currentRom" :platform="platform" />
|
||||
<title-info :rom="currentRom" />
|
||||
</div>
|
||||
<v-row
|
||||
:class="{
|
||||
@@ -185,7 +187,7 @@ watch(
|
||||
<v-window-item value="details">
|
||||
<v-row no-gutters :class="{ 'mx-2': mdAndUp }">
|
||||
<v-col>
|
||||
<file-info :rom="currentRom" :platform="platform" />
|
||||
<file-info :rom="currentRom" />
|
||||
<game-info :rom="currentRom" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -254,9 +256,6 @@ watch(
|
||||
margin-top: -190px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
.cover-mobile {
|
||||
margin-top: -280px;
|
||||
}
|
||||
.info-mobile {
|
||||
margin-top: 100px;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ import DeletePlatformDialog from "@/components/common/Platform/Dialog/DeletePlat
|
||||
import SearchCoverDialog from "@/components/common/SearchCover.vue";
|
||||
import collectionApi from "@/services/api/collection";
|
||||
import platformApi from "@/services/api/platform";
|
||||
import userApi from "@/services/api/user";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import storeNavigation from "@/stores/navigation";
|
||||
@@ -74,14 +73,6 @@ onBeforeMount(async () => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
await userApi
|
||||
.fetchCurrentUser()
|
||||
.then(({ data: user }) => {
|
||||
auth.setUser(user);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
navigationStore.resetDrawers();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { isNull } from "lodash";
|
||||
import { onMounted, ref, nextTick } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
import RAvatar from "@/components/common/Game/RAvatar.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { isNull } from "lodash";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { nextTick, onMounted, ref } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
// Props
|
||||
const route = useRoute();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const { defaultAspectRatioScreenshot } = storeToRefs(galleryViewStore);
|
||||
const rom = ref<DetailedRom | null>(null);
|
||||
const gameRunning = ref(false);
|
||||
const storedFSOP = localStorage.getItem("fullScreenOnPlay");
|
||||
@@ -25,6 +28,7 @@ const script = document.createElement("script");
|
||||
script.src = "/assets/ruffle/ruffle.js";
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Functions
|
||||
function onPlay() {
|
||||
gameRunning.value = true;
|
||||
|
||||
@@ -71,6 +75,7 @@ onMounted(async () => {
|
||||
md="8"
|
||||
xl="10"
|
||||
id="game-wrapper"
|
||||
:style="`aspect-ratio: ${defaultAspectRatioScreenshot}`"
|
||||
class="bg-secondary"
|
||||
rounded
|
||||
>
|
||||
@@ -191,12 +196,6 @@ onMounted(async () => {
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
#game-wrapper {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
#game {
|
||||
max-height: 100dvh;
|
||||
|
||||
@@ -105,7 +105,7 @@ async function stopScan() {
|
||||
v-model="platformsToScan"
|
||||
label="Platforms"
|
||||
item-title="name"
|
||||
:items="platforms.all"
|
||||
:items="platforms.allPlatforms"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
multiple
|
||||
@@ -231,12 +231,7 @@ async function stopScan() {
|
||||
>mdi-magnify-scan</v-icon
|
||||
>
|
||||
</template>
|
||||
<span
|
||||
:class="{
|
||||
'text-romm-accent-1': !scanning,
|
||||
}"
|
||||
>Scan</span
|
||||
>
|
||||
Scan
|
||||
<template #loader>
|
||||
<v-progress-circular
|
||||
color="romm-accent-1"
|
||||
@@ -256,7 +251,7 @@ async function stopScan() {
|
||||
<template #prepend>
|
||||
<v-icon :color="scanning ? 'red' : ''">mdi-alert-octagon</v-icon>
|
||||
</template>
|
||||
<span :class="{ 'text-romm-red': scanning }">Abort</span>
|
||||
Abort
|
||||
</v-btn>
|
||||
<v-btn
|
||||
prepend-icon="mdi-table-cog"
|
||||
|
||||
Reference in New Issue
Block a user