mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 23:42:07 +01:00
Merge branch 'master' into feature/screenscraper-integration
This commit is contained in:
@@ -6,6 +6,7 @@ from alembic import context
|
||||
from config.config_manager import ConfigManager
|
||||
from models.assets import Save, Screenshot, State # noqa
|
||||
from models.base import BaseModel
|
||||
from models.collection import VirtualCollection
|
||||
from models.firmware import Firmware # noqa
|
||||
from models.platform import Platform # noqa
|
||||
from models.rom import Rom, SiblingRom # noqa
|
||||
@@ -35,7 +36,10 @@ target_metadata = BaseModel.metadata
|
||||
|
||||
# Ignore specific models when running migrations
|
||||
def include_object(object, name, type_, reflected, compare_to):
|
||||
if type_ == "table" and name in [SiblingRom.__tablename__]: # Virtual table
|
||||
if type_ == "table" and name in [
|
||||
SiblingRom.__tablename__,
|
||||
VirtualCollection.__tablename__,
|
||||
]: # Virtual table
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
306
backend/alembic/versions/0034_virtual_collections_db_view.py
Normal file
306
backend/alembic/versions/0034_virtual_collections_db_view.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0034_virtual_collections_db_view
|
||||
Revises: 0033_rom_file_and_hashes
|
||||
Create Date: 2024-08-08 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from utils.database import CustomJSON, is_postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0034_virtual_collections_db_view"
|
||||
down_revision = "0033_rom_file_and_hashes"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"collections_roms",
|
||||
sa.Column("collection_id", sa.Integer(), nullable=False),
|
||||
sa.Column("rom_id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.TIMESTAMP(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["collection_id"], ["collections.id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.ForeignKeyConstraint(["rom_id"], ["roms.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("collection_id", "rom_id"),
|
||||
sa.UniqueConstraint("collection_id", "rom_id", name="unique_collection_rom"),
|
||||
)
|
||||
|
||||
connection = op.get_bind()
|
||||
|
||||
if is_postgresql(connection):
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO collections_roms (collection_id, rom_id, created_at, updated_at)
|
||||
SELECT c.id, rom_id::INT, NOW(), NOW()
|
||||
FROM collections c,
|
||||
LATERAL jsonb_array_elements_text(c.roms) AS rom_id
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE OR REPLACE VIEW virtual_collections AS
|
||||
WITH genres_collection AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
jsonb_array_elements_text(igdb_metadata -> 'genres') as collection_name,
|
||||
'genre' as collection_type
|
||||
FROM roms r
|
||||
WHERE igdb_metadata->'genres' IS NOT NULL
|
||||
),
|
||||
franchises_collection AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
jsonb_array_elements_text(igdb_metadata -> 'franchises') as collection_name,
|
||||
'franchise' as collection_type
|
||||
FROM roms r
|
||||
WHERE igdb_metadata->'franchises' IS NOT NULL
|
||||
),
|
||||
collection_collection AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
jsonb_array_elements_text(igdb_metadata -> 'collections') as collection_name,
|
||||
'collection' as collection_type
|
||||
FROM roms r
|
||||
WHERE igdb_metadata->'collections' IS NOT NULL
|
||||
),
|
||||
modes_collection AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
jsonb_array_elements_text(igdb_metadata -> 'game_modes') as collection_name,
|
||||
'mode' as collection_type
|
||||
FROM roms r
|
||||
WHERE igdb_metadata->'game_modes' IS NOT NULL
|
||||
),
|
||||
companies_collection AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
jsonb_array_elements_text(igdb_metadata -> 'companies') as collection_name,
|
||||
'company' as collection_type
|
||||
FROM roms r
|
||||
WHERE igdb_metadata->'companies' IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
collection_name as name,
|
||||
collection_type as type,
|
||||
'Autogenerated ' || collection_type || ' collection' AS description,
|
||||
NOW() AS created_at,
|
||||
NOW() AS updated_at,
|
||||
array_to_json(array_agg(rom_id)) as rom_ids,
|
||||
array_to_json(array_agg(path_cover_s)) as path_covers_s,
|
||||
array_to_json(array_agg(path_cover_l)) as path_covers_l
|
||||
FROM (
|
||||
SELECT * FROM genres_collection
|
||||
UNION ALL
|
||||
SELECT * FROM franchises_collection
|
||||
UNION ALL
|
||||
SELECT * FROM collection_collection
|
||||
UNION ALL
|
||||
SELECT * FROM modes_collection
|
||||
UNION ALL
|
||||
SELECT * FROM companies_collection
|
||||
) combined
|
||||
GROUP BY collection_type, collection_name
|
||||
HAVING COUNT(DISTINCT rom_id) > 2
|
||||
ORDER BY collection_type, collection_name;
|
||||
""" # nosec B608
|
||||
),
|
||||
)
|
||||
else:
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
INSERT INTO collections_roms (collection_id, rom_id, created_at, updated_at)
|
||||
SELECT c.id, jt.rom_id, NOW(), NOW()
|
||||
FROM collections c
|
||||
JOIN JSON_TABLE(c.roms, '$[*]' COLUMNS (rom_id INT PATH '$')) AS jt
|
||||
"""
|
||||
)
|
||||
)
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
CREATE OR REPLACE VIEW virtual_collections AS
|
||||
WITH genres AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
CONCAT(j.genre) as collection_name,
|
||||
'genre' as collection_type
|
||||
FROM
|
||||
roms r
|
||||
CROSS JOIN JSON_TABLE(
|
||||
JSON_EXTRACT(igdb_metadata, '$.genres'),
|
||||
'$[*]' COLUMNS (genre VARCHAR(255) PATH '$')
|
||||
) j
|
||||
WHERE
|
||||
JSON_EXTRACT(igdb_metadata, '$.genres') IS NOT NULL
|
||||
),
|
||||
franchises AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
CONCAT(j.franchise) as collection_name,
|
||||
'franchise' as collection_type
|
||||
FROM
|
||||
roms r
|
||||
CROSS JOIN JSON_TABLE(
|
||||
JSON_EXTRACT(igdb_metadata, '$.franchises'),
|
||||
'$[*]' COLUMNS (franchise VARCHAR(255) PATH '$')
|
||||
) j
|
||||
WHERE
|
||||
JSON_EXTRACT(igdb_metadata, '$.franchises') IS NOT NULL
|
||||
),
|
||||
collections AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
CONCAT(j.collection) as collection_name,
|
||||
'collection' as collection_type
|
||||
FROM
|
||||
roms r
|
||||
CROSS JOIN JSON_TABLE(
|
||||
JSON_EXTRACT(igdb_metadata, '$.collections'),
|
||||
'$[*]' COLUMNS (collection VARCHAR(255) PATH '$')
|
||||
) j
|
||||
WHERE
|
||||
JSON_EXTRACT(igdb_metadata, '$.collections') IS NOT NULL
|
||||
),
|
||||
modes AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
CONCAT(j.mode) as collection_name,
|
||||
'mode' as collection_type
|
||||
FROM
|
||||
roms r
|
||||
CROSS JOIN JSON_TABLE(
|
||||
JSON_EXTRACT(igdb_metadata, '$.game_modes'),
|
||||
'$[*]' COLUMNS (mode VARCHAR(255) PATH '$')
|
||||
) j
|
||||
WHERE
|
||||
JSON_EXTRACT(igdb_metadata, '$.game_modes') IS NOT NULL
|
||||
),
|
||||
companies AS (
|
||||
SELECT
|
||||
r.id as rom_id,
|
||||
r.path_cover_s as path_cover_s,
|
||||
r.path_cover_l as path_cover_l,
|
||||
CONCAT(j.company) as collection_name,
|
||||
'company' as collection_type
|
||||
FROM
|
||||
roms r
|
||||
CROSS JOIN JSON_TABLE(
|
||||
JSON_EXTRACT(igdb_metadata, '$.companies'),
|
||||
'$[*]' COLUMNS (company VARCHAR(255) PATH '$')
|
||||
) j
|
||||
WHERE
|
||||
JSON_EXTRACT(igdb_metadata, '$.companies') IS NOT NULL
|
||||
)
|
||||
SELECT
|
||||
collection_name as name,
|
||||
collection_type as type,
|
||||
CONCAT('Autogenerated ', collection_name, ' collection') AS description,
|
||||
NOW() AS created_at,
|
||||
NOW() AS updated_at,
|
||||
JSON_ARRAYAGG(DISTINCT rom_id) as rom_ids,
|
||||
JSON_ARRAYAGG(DISTINCT path_cover_s) as path_covers_s,
|
||||
JSON_ARRAYAGG(DISTINCT path_cover_l) as path_covers_l
|
||||
FROM
|
||||
(
|
||||
SELECT * FROM genres
|
||||
UNION ALL
|
||||
SELECT * FROM franchises
|
||||
UNION ALL
|
||||
SELECT * FROM collections
|
||||
UNION ALL
|
||||
SELECT * FROM modes
|
||||
UNION ALL
|
||||
SELECT * FROM companies
|
||||
) combined
|
||||
GROUP BY collection_type, collection_name
|
||||
HAVING COUNT(DISTINCT rom_id) > 2
|
||||
ORDER BY collection_type, collection_name;
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
op.drop_column("collections", "roms")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("collections", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("roms", CustomJSON(), nullable=False))
|
||||
|
||||
connection = op.get_bind()
|
||||
if is_postgresql(connection):
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE collections c
|
||||
SET roms = (
|
||||
SELECT jsonb_agg(rom_id)
|
||||
FROM collections_roms cr
|
||||
WHERE cr.collection_id = c.id
|
||||
);
|
||||
"""
|
||||
)
|
||||
)
|
||||
else:
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
UPDATE collections c
|
||||
JOIN (
|
||||
SELECT collection_id, JSON_ARRAYAGG(rom_id) as roms
|
||||
FROM collections_roms
|
||||
GROUP BY collection_id
|
||||
) cr
|
||||
ON c.id = cr.collection_id
|
||||
SET c.roms = cr.roms;
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
op.drop_table("collections_roms")
|
||||
|
||||
connection.execute(
|
||||
sa.text(
|
||||
"""
|
||||
DROP VIEW virtual_collections;
|
||||
"""
|
||||
),
|
||||
)
|
||||
@@ -6,7 +6,7 @@ from anyio import Path
|
||||
from config import RESOURCES_BASE_PATH
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses import MessageResponse
|
||||
from endpoints.responses.collection import CollectionSchema
|
||||
from endpoints.responses.collection import CollectionSchema, VirtualCollectionSchema
|
||||
from exceptions.endpoint_exceptions import (
|
||||
CollectionAlreadyExistsException,
|
||||
CollectionNotFoundInDatabaseException,
|
||||
@@ -29,7 +29,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.COLLECTIONS_WRITE])
|
||||
@protected_route(router.post, "", [Scope.COLLECTIONS_WRITE])
|
||||
async def add_collection(
|
||||
request: Request,
|
||||
artwork: UploadFile | None = None,
|
||||
@@ -98,7 +98,7 @@ async def add_collection(
|
||||
return CollectionSchema.model_validate(created_collection)
|
||||
|
||||
|
||||
@protected_route(router.get, "/", [Scope.COLLECTIONS_READ])
|
||||
@protected_route(router.get, "", [Scope.COLLECTIONS_READ])
|
||||
def get_collections(request: Request) -> list[CollectionSchema]:
|
||||
"""Get collections endpoint
|
||||
|
||||
@@ -111,9 +111,30 @@ def get_collections(request: Request) -> list[CollectionSchema]:
|
||||
"""
|
||||
|
||||
collections = db_collection_handler.get_collections()
|
||||
|
||||
return CollectionSchema.for_user(request.user.id, [c for c in collections])
|
||||
|
||||
|
||||
@protected_route(router.get, "/virtual", [Scope.COLLECTIONS_READ])
|
||||
def get_virtual_collections(
|
||||
request: Request,
|
||||
type: str,
|
||||
limit: int | None = None,
|
||||
) -> list[VirtualCollectionSchema]:
|
||||
"""Get virtual collections endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
|
||||
Returns:
|
||||
list[VirtualCollectionSchema]: List of virtual collections
|
||||
"""
|
||||
|
||||
virtual_collections = db_collection_handler.get_virtual_collections(type, limit)
|
||||
|
||||
return [VirtualCollectionSchema.model_validate(vc) for vc in virtual_collections]
|
||||
|
||||
|
||||
@protected_route(router.get, "/{id}", [Scope.COLLECTIONS_READ])
|
||||
def get_collection(request: Request, id: int) -> CollectionSchema:
|
||||
"""Get collections endpoint
|
||||
@@ -127,13 +148,31 @@ def get_collection(request: Request, id: int) -> CollectionSchema:
|
||||
"""
|
||||
|
||||
collection = db_collection_handler.get_collection(id)
|
||||
|
||||
if not collection:
|
||||
raise CollectionNotFoundInDatabaseException(id)
|
||||
|
||||
return CollectionSchema.model_validate(collection)
|
||||
|
||||
|
||||
@protected_route(router.get, "/virtual/{id}", [Scope.COLLECTIONS_READ])
|
||||
def get_virtual_collection(request: Request, id: str) -> VirtualCollectionSchema:
|
||||
"""Get virtual collections endpoint
|
||||
|
||||
Args:
|
||||
request (Request): Fastapi Request object
|
||||
id (str): Virtual collection id
|
||||
|
||||
Returns:
|
||||
VirtualCollectionSchema: Virtual collection
|
||||
"""
|
||||
|
||||
virtual_collection = db_collection_handler.get_virtual_collection(id)
|
||||
if not virtual_collection:
|
||||
raise CollectionNotFoundInDatabaseException(id)
|
||||
|
||||
return VirtualCollectionSchema.model_validate(virtual_collection)
|
||||
|
||||
|
||||
@protected_route(router.put, "/{id}", [Scope.COLLECTIONS_WRITE])
|
||||
async def update_collection(
|
||||
request: Request,
|
||||
@@ -164,17 +203,14 @@ async def update_collection(
|
||||
raise CollectionNotFoundInDatabaseException(id)
|
||||
|
||||
try:
|
||||
roms = json.loads(data["roms"]) # type: ignore
|
||||
rom_ids = json.loads(data["rom_ids"]) # type: ignore
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError("Invalid list for roms field in update collection") from e
|
||||
except KeyError:
|
||||
roms = collection.roms
|
||||
raise ValueError("Invalid list for rom_ids field in update collection") from e
|
||||
|
||||
cleaned_data = {
|
||||
"name": data.get("name", collection.name),
|
||||
"description": data.get("description", collection.description),
|
||||
"is_public": is_public if is_public is not None else collection.is_public,
|
||||
"roms": list(set(roms)),
|
||||
"user_id": request.user.id,
|
||||
}
|
||||
|
||||
@@ -219,7 +255,9 @@ async def update_collection(
|
||||
{"path_cover_s": path_cover_s, "path_cover_l": path_cover_l}
|
||||
)
|
||||
|
||||
updated_collection = db_collection_handler.update_collection(id, cleaned_data)
|
||||
updated_collection = db_collection_handler.update_collection(
|
||||
id, cleaned_data, rom_ids
|
||||
)
|
||||
|
||||
return CollectionSchema.model_validate(updated_collection)
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("")
|
||||
def get_config() -> ConfigResponse:
|
||||
"""Get config endpoint
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.FIRMWARE_WRITE])
|
||||
@protected_route(router.post, "", [Scope.FIRMWARE_WRITE])
|
||||
def add_firmware(
|
||||
request: Request,
|
||||
platform_id: int,
|
||||
@@ -84,7 +84,7 @@ def add_firmware(
|
||||
}
|
||||
|
||||
|
||||
@protected_route(router.get, "/", [Scope.FIRMWARE_READ])
|
||||
@protected_route(router.get, "", [Scope.FIRMWARE_READ])
|
||||
def get_platform_firmware(
|
||||
request: Request,
|
||||
platform_id: int | None = None,
|
||||
|
||||
@@ -20,7 +20,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.PLATFORMS_WRITE])
|
||||
@protected_route(router.post, "", [Scope.PLATFORMS_WRITE])
|
||||
async def add_platforms(request: Request) -> PlatformSchema:
|
||||
"""Create platform endpoint
|
||||
|
||||
@@ -43,7 +43,7 @@ async def add_platforms(request: Request) -> PlatformSchema:
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.get, "/", [Scope.PLATFORMS_READ])
|
||||
@protected_route(router.get, "", [Scope.PLATFORMS_READ])
|
||||
def get_platforms(request: Request) -> list[PlatformSchema]:
|
||||
"""Get platforms endpoint
|
||||
|
||||
|
||||
@@ -9,15 +9,17 @@ class CollectionSchema(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
path_cover_l: str | None
|
||||
path_cover_s: str | None
|
||||
has_cover: bool
|
||||
path_cover_small: str | None
|
||||
path_cover_large: str | None
|
||||
path_covers_small: list[str]
|
||||
path_covers_large: list[str]
|
||||
url_cover: str
|
||||
roms: set[int]
|
||||
rom_ids: set[int]
|
||||
rom_count: int
|
||||
user_id: int
|
||||
user__username: str
|
||||
is_public: bool
|
||||
is_favorite: bool
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
@@ -34,3 +36,17 @@ class CollectionSchema(BaseModel):
|
||||
for c in collections
|
||||
if c.user_id == user_id or c.is_public
|
||||
]
|
||||
|
||||
|
||||
class VirtualCollectionSchema(BaseModel):
|
||||
id: str
|
||||
name: str
|
||||
type: str
|
||||
description: str
|
||||
rom_ids: set[int]
|
||||
rom_count: int
|
||||
path_covers_small: list[str]
|
||||
path_covers_large: list[str]
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@@ -147,7 +147,7 @@ class RomSchema(BaseModel):
|
||||
alternative_names: list[str]
|
||||
genres: list[str]
|
||||
franchises: list[str]
|
||||
collections: list[str]
|
||||
meta_collections: list[str]
|
||||
companies: list[str]
|
||||
game_modes: list[str]
|
||||
age_ratings: list[str]
|
||||
@@ -155,14 +155,16 @@ class RomSchema(BaseModel):
|
||||
moby_metadata: RomMobyMetadata | None
|
||||
ss_metadata: RomMobyMetadata | None
|
||||
|
||||
path_cover_s: str | None
|
||||
path_cover_l: str | None
|
||||
has_cover: bool
|
||||
path_cover_small: str | None
|
||||
path_cover_large: str | None
|
||||
url_cover: str | None
|
||||
|
||||
has_manual: bool
|
||||
path_manual: str | None
|
||||
url_manual: str | None
|
||||
|
||||
is_unidentified: bool
|
||||
|
||||
revision: str | None
|
||||
regions: list[str]
|
||||
languages: list[str]
|
||||
@@ -242,7 +244,7 @@ class DetailedRomSchema(RomSchema):
|
||||
if s.user_id == user_id
|
||||
]
|
||||
db_rom.user_collections = CollectionSchema.for_user( # type: ignore
|
||||
user_id, [c for c in db_rom.get_collections()]
|
||||
user_id, db_rom.collections
|
||||
)
|
||||
|
||||
return cls.model_validate(db_rom)
|
||||
|
||||
@@ -27,7 +27,7 @@ from exceptions.fs_exceptions import RomAlreadyExistsException
|
||||
from fastapi import HTTPException, Request, UploadFile, status
|
||||
from fastapi.responses import Response
|
||||
from handler.auth.constants import Scope
|
||||
from handler.database import db_collection_handler, db_platform_handler, db_rom_handler
|
||||
from handler.database import db_platform_handler, db_rom_handler
|
||||
from handler.filesystem import fs_resource_handler, fs_rom_handler
|
||||
from handler.filesystem.base_handler import CoverSize
|
||||
from handler.metadata import meta_igdb_handler, meta_moby_handler, meta_ss_handler
|
||||
@@ -50,7 +50,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.ROMS_WRITE])
|
||||
@protected_route(router.post, "", [Scope.ROMS_WRITE])
|
||||
async def add_rom(request: Request):
|
||||
"""Upload single rom endpoint
|
||||
|
||||
@@ -112,11 +112,12 @@ async def add_rom(request: Request):
|
||||
return Response(status_code=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@protected_route(router.get, "/", [Scope.ROMS_READ])
|
||||
@protected_route(router.get, "", [Scope.ROMS_READ])
|
||||
def get_roms(
|
||||
request: Request,
|
||||
platform_id: int | None = None,
|
||||
collection_id: int | None = None,
|
||||
virtual_collection_id: str | None = None,
|
||||
search_term: str = "",
|
||||
limit: int | None = None,
|
||||
offset: int | None = None,
|
||||
@@ -130,6 +131,7 @@ def get_roms(
|
||||
request (Request): Fastapi Request object
|
||||
platform_id (int, optional): Platform ID to filter ROMs
|
||||
collection_id (int, optional): Collection ID to filter ROMs
|
||||
virtual_collection_id (str, optional): Virtual Collection ID to filter ROMs
|
||||
search_term (str, optional): Search term to filter ROMs
|
||||
limit (int, optional): Limit the number of ROMs returned
|
||||
offset (int, optional): Offset for pagination
|
||||
@@ -145,6 +147,7 @@ def get_roms(
|
||||
roms = db_rom_handler.get_roms(
|
||||
platform_id=platform_id,
|
||||
collection_id=collection_id,
|
||||
virtual_collection_id=virtual_collection_id,
|
||||
search_term=search_term.lower(),
|
||||
order_by=order_by.lower(),
|
||||
order_dir=order_dir.lower(),
|
||||
@@ -156,6 +159,7 @@ def get_roms(
|
||||
user_id=request.user.id,
|
||||
platform_id=platform_id,
|
||||
collection_id=collection_id,
|
||||
virtual_collection_id=virtual_collection_id,
|
||||
search_term=search_term,
|
||||
order_by=order_by,
|
||||
order_dir=order_dir,
|
||||
@@ -694,14 +698,6 @@ async def delete_roms(
|
||||
log.info(f"Deleting {rom.fs_name} from database")
|
||||
db_rom_handler.delete_rom(id)
|
||||
|
||||
# Update collections to remove the deleted rom
|
||||
collections = db_collection_handler.get_collections_by_rom_id(id)
|
||||
for collection in collections:
|
||||
collection.roms = {rom_id for rom_id in collection.roms if rom_id != id}
|
||||
db_collection_handler.update_collection(
|
||||
collection.id, {"roms": collection.roms}
|
||||
)
|
||||
|
||||
try:
|
||||
rmtree(f"{RESOURCES_BASE_PATH}/{rom.fs_resources_path}")
|
||||
except FileNotFoundError:
|
||||
|
||||
@@ -18,7 +18,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.ASSETS_WRITE])
|
||||
@protected_route(router.post, "", [Scope.ASSETS_WRITE])
|
||||
def add_saves(
|
||||
request: Request,
|
||||
rom_id: int,
|
||||
@@ -87,7 +87,7 @@ def add_saves(
|
||||
}
|
||||
|
||||
|
||||
# @protected_route(router.get, "/", [Scope.ASSETS_READ])
|
||||
# @protected_route(router.get, "", [Scope.ASSETS_READ])
|
||||
# def get_saves(request: Request) -> MessageResponse:
|
||||
# pass
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.ASSETS_WRITE])
|
||||
@protected_route(router.post, "", [Scope.ASSETS_WRITE])
|
||||
def add_screenshots(
|
||||
request: Request,
|
||||
rom_id: int,
|
||||
|
||||
@@ -18,7 +18,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@protected_route(router.post, "/", [Scope.ASSETS_WRITE])
|
||||
@protected_route(router.post, "", [Scope.ASSETS_WRITE])
|
||||
def add_states(
|
||||
request: Request,
|
||||
rom_id: int,
|
||||
@@ -86,7 +86,7 @@ def add_states(
|
||||
}
|
||||
|
||||
|
||||
# @protected_route(router.get, "/", [Scope.ASSETS_READ])
|
||||
# @protected_route(router.get, "", [Scope.ASSETS_READ])
|
||||
# def get_states(request: Request) -> MessageResponse:
|
||||
# pass
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ router = APIRouter(
|
||||
)
|
||||
|
||||
|
||||
@router.get("/")
|
||||
@router.get("")
|
||||
def stats() -> StatsReturn:
|
||||
"""Endpoint to return the current RomM stats
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ router = APIRouter(
|
||||
|
||||
@protected_route(
|
||||
router.post,
|
||||
"/",
|
||||
"",
|
||||
[],
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
@@ -80,7 +80,7 @@ def add_user(
|
||||
return UserSchema.model_validate(db_user_handler.add_user(user))
|
||||
|
||||
|
||||
@protected_route(router.get, "/", [Scope.USERS_READ])
|
||||
@protected_route(router.get, "", [Scope.USERS_READ])
|
||||
def get_users(request: Request) -> list[UserSchema]:
|
||||
"""Get all users endpoint
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from typing import Any, Sequence
|
||||
from typing import Sequence
|
||||
|
||||
from decorators.database import begin_session
|
||||
from models.collection import Collection
|
||||
from sqlalchemy import delete, select, update
|
||||
from models.collection import Collection, CollectionRom, VirtualCollection
|
||||
from sqlalchemy import delete, insert, literal, or_, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql import ColumnExpressionArgument
|
||||
from utils.database import json_array_contains_value
|
||||
|
||||
from .base_handler import DBBaseHandler
|
||||
|
||||
@@ -24,6 +22,15 @@ class DBCollectionsHandler(DBBaseHandler):
|
||||
def get_collection(self, id: int, session: Session = None) -> Collection | None:
|
||||
return session.scalar(select(Collection).filter_by(id=id).limit(1))
|
||||
|
||||
@begin_session
|
||||
def get_virtual_collection(
|
||||
self, id: str, session: Session = None
|
||||
) -> VirtualCollection | None:
|
||||
name, type = VirtualCollection.from_id(id)
|
||||
return session.scalar(
|
||||
select(VirtualCollection).filter_by(name=name, type=type).limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_collection_by_name(
|
||||
self, name: str, user_id: int, session: Session = None
|
||||
@@ -41,24 +48,27 @@ class DBCollectionsHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_collections_by_rom_id(
|
||||
self,
|
||||
rom_id: int,
|
||||
*,
|
||||
order_by: Sequence[str | ColumnExpressionArgument[Any]] | None = None,
|
||||
session: Session = None,
|
||||
) -> Sequence[Collection]:
|
||||
query = select(Collection).filter(
|
||||
json_array_contains_value(Collection.roms, rom_id, session=session)
|
||||
def get_virtual_collections(
|
||||
self, type: str, limit: int | None = None, session: Session = None
|
||||
) -> Sequence[VirtualCollection]:
|
||||
return (
|
||||
session.scalars(
|
||||
select(VirtualCollection)
|
||||
.filter(or_(VirtualCollection.type == type, literal(type == "all")))
|
||||
.limit(limit)
|
||||
.order_by(VirtualCollection.name.asc())
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
if order_by is not None:
|
||||
query = query.order_by(*order_by)
|
||||
|
||||
return session.scalars(query).all()
|
||||
|
||||
@begin_session
|
||||
def update_collection(
|
||||
self, id: int, data: dict, session: Session = None
|
||||
self,
|
||||
id: int,
|
||||
data: dict,
|
||||
rom_ids: list[int] | None = None,
|
||||
session: Session = None,
|
||||
) -> Collection:
|
||||
session.execute(
|
||||
update(Collection)
|
||||
@@ -66,6 +76,18 @@ class DBCollectionsHandler(DBBaseHandler):
|
||||
.values(**data)
|
||||
.execution_options(synchronize_session="evaluate")
|
||||
)
|
||||
|
||||
if rom_ids:
|
||||
# Delete all existing CollectionRom entries for this collection
|
||||
session.execute(
|
||||
delete(CollectionRom).where(CollectionRom.collection_id == id)
|
||||
)
|
||||
# Insert new CollectionRom entries for this collection
|
||||
session.execute(
|
||||
insert(CollectionRom),
|
||||
[{"collection_id": id, "rom_id": rom_id} for rom_id in set(rom_ids)],
|
||||
)
|
||||
|
||||
return session.query(Collection).filter_by(id=id).one()
|
||||
|
||||
@begin_session
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections.abc import Iterable
|
||||
from typing import Sequence
|
||||
|
||||
from decorators.database import begin_session
|
||||
from models.collection import Collection
|
||||
from models.collection import Collection, VirtualCollection
|
||||
from models.rom import Rom, RomFile, RomUser
|
||||
from sqlalchemy import and_, delete, func, or_, select, update
|
||||
from sqlalchemy.orm import Query, Session, selectinload
|
||||
@@ -26,6 +26,7 @@ def with_details(func):
|
||||
selectinload(Rom.screenshots),
|
||||
selectinload(Rom.rom_users),
|
||||
selectinload(Rom.sibling_roms),
|
||||
selectinload(Rom.collections),
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@@ -55,6 +56,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
data,
|
||||
platform_id: int | None,
|
||||
collection_id: int | None,
|
||||
virtual_collection_id: str | None,
|
||||
search_term: str,
|
||||
session: Session,
|
||||
):
|
||||
@@ -68,7 +70,17 @@ class DBRomsHandler(DBBaseHandler):
|
||||
.one_or_none()
|
||||
)
|
||||
if collection:
|
||||
data = data.filter(Rom.id.in_(collection.roms))
|
||||
data = data.filter(Rom.id.in_(collection.rom_ids))
|
||||
|
||||
if virtual_collection_id:
|
||||
name, type = VirtualCollection.from_id(virtual_collection_id)
|
||||
v_collection = (
|
||||
session.query(VirtualCollection)
|
||||
.filter(VirtualCollection.name == name, VirtualCollection.type == type)
|
||||
.one_or_none()
|
||||
)
|
||||
if v_collection:
|
||||
data = data.filter(Rom.id.in_(v_collection.rom_ids))
|
||||
|
||||
if search_term:
|
||||
data = data.filter(
|
||||
@@ -113,6 +125,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
*,
|
||||
platform_id: int | None = None,
|
||||
collection_id: int | None = None,
|
||||
virtual_collection_id: str | None = None,
|
||||
search_term: str = "",
|
||||
order_by: str = "name",
|
||||
order_dir: str = "asc",
|
||||
@@ -122,7 +135,12 @@ class DBRomsHandler(DBBaseHandler):
|
||||
session: Session = None,
|
||||
) -> Sequence[Rom]:
|
||||
filtered_query = self._filter(
|
||||
query, platform_id, collection_id, search_term, session
|
||||
query,
|
||||
platform_id,
|
||||
collection_id,
|
||||
virtual_collection_id,
|
||||
search_term,
|
||||
session,
|
||||
)
|
||||
ordered_query = self._order(filtered_query, order_by, order_dir)
|
||||
offset_query = ordered_query.offset(offset)
|
||||
@@ -246,6 +264,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
user_id: int,
|
||||
platform_id: int | None = None,
|
||||
collection_id: int | None = None,
|
||||
virtual_collection_id: str | None = None,
|
||||
search_term: str = "",
|
||||
order_by: str = "name",
|
||||
order_dir: str = "asc",
|
||||
@@ -260,7 +279,12 @@ class DBRomsHandler(DBBaseHandler):
|
||||
.order_by(RomUser.last_played.desc())
|
||||
)
|
||||
filtered_query = self._filter(
|
||||
filtered_query, platform_id, collection_id, search_term, session
|
||||
filtered_query,
|
||||
platform_id,
|
||||
collection_id,
|
||||
virtual_collection_id,
|
||||
search_term,
|
||||
session,
|
||||
)
|
||||
offset_query = filtered_query.offset(offset)
|
||||
limited_query = offset_query.limit(limit)
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
import base64
|
||||
import json
|
||||
|
||||
from config import FRONTEND_RESOURCES_PATH
|
||||
from models.base import BaseModel
|
||||
from models.rom import Rom
|
||||
from models.user import User
|
||||
from sqlalchemy import ForeignKey, String, Text
|
||||
from sqlalchemy import ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from utils.database import CustomJSON
|
||||
|
||||
@@ -16,37 +19,141 @@ class Collection(BaseModel):
|
||||
|
||||
name: Mapped[str] = mapped_column(String(length=400))
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
|
||||
is_public: Mapped[bool] = mapped_column(default=False)
|
||||
path_cover_l: Mapped[str | None] = mapped_column(Text, default="")
|
||||
path_cover_s: Mapped[str | None] = mapped_column(Text, default="")
|
||||
|
||||
url_cover: Mapped[str | None] = mapped_column(
|
||||
Text, default="", doc="URL to cover image stored in IGDB"
|
||||
)
|
||||
|
||||
roms: Mapped[set[int]] = mapped_column(
|
||||
CustomJSON(), default=[], doc="Rom id's that belong to this collection"
|
||||
roms: Mapped[list[Rom]] = relationship(
|
||||
"Rom",
|
||||
secondary="collections_roms",
|
||||
collection_class=set,
|
||||
back_populates="collections",
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
is_public: Mapped[bool] = mapped_column(default=False)
|
||||
user: Mapped[User] = relationship(lazy="joined", back_populates="collections")
|
||||
|
||||
@property
|
||||
def user__username(self) -> str:
|
||||
return self.user.username
|
||||
|
||||
@property
|
||||
def rom_ids(self) -> list[int]:
|
||||
return [r.id for r in self.roms]
|
||||
|
||||
@property
|
||||
def rom_count(self) -> int:
|
||||
return len(self.roms)
|
||||
|
||||
@cached_property
|
||||
def has_cover(self) -> bool:
|
||||
return bool(self.path_cover_s or self.path_cover_l)
|
||||
|
||||
@property
|
||||
def fs_resources_path(self) -> str:
|
||||
return f"collections/{str(self.id)}"
|
||||
|
||||
@property
|
||||
def path_cover_small(self) -> str:
|
||||
return (
|
||||
f"{FRONTEND_RESOURCES_PATH}/{self.path_cover_s}?ts={self.updated_at}"
|
||||
if self.path_cover_s
|
||||
else ""
|
||||
)
|
||||
|
||||
@property
|
||||
def path_cover_large(self) -> str:
|
||||
return (
|
||||
f"{FRONTEND_RESOURCES_PATH}/{self.path_cover_l}?ts={self.updated_at}"
|
||||
if self.path_cover_l
|
||||
else ""
|
||||
)
|
||||
|
||||
@property
|
||||
def path_covers_small(self) -> list[str]:
|
||||
return [
|
||||
f"{FRONTEND_RESOURCES_PATH}/{r.path_cover_s}?ts={self.updated_at}"
|
||||
for r in self.roms
|
||||
if r.path_cover_s
|
||||
]
|
||||
|
||||
@property
|
||||
def path_covers_large(self) -> list[str]:
|
||||
return [
|
||||
f"{FRONTEND_RESOURCES_PATH}/{r.path_cover_l}?ts={self.updated_at}"
|
||||
for r in self.roms
|
||||
if r.path_cover_l
|
||||
]
|
||||
|
||||
@property
|
||||
def is_favorite(self) -> bool:
|
||||
return self.name.lower() == "favourites"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.name
|
||||
|
||||
|
||||
class CollectionRom(BaseModel):
|
||||
__tablename__ = "collections_roms"
|
||||
|
||||
collection_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("collections.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
rom_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("roms.id", ondelete="CASCADE"), primary_key=True
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("collection_id", "rom_id", name="unique_collection_rom"),
|
||||
)
|
||||
|
||||
|
||||
class VirtualCollection(BaseModel):
|
||||
__tablename__ = "virtual_collections"
|
||||
|
||||
name: Mapped[str] = mapped_column(String(length=400), primary_key=True)
|
||||
type: Mapped[str] = mapped_column(String(length=50), primary_key=True)
|
||||
description: Mapped[str | None] = mapped_column(Text)
|
||||
path_covers_s: Mapped[list[str]] = mapped_column(CustomJSON(), default=[])
|
||||
path_covers_l: Mapped[list[str]] = mapped_column(CustomJSON(), default=[])
|
||||
|
||||
rom_ids: Mapped[set[int]] = mapped_column(
|
||||
CustomJSON(), default=[], doc="Rom IDs that belong to this collection"
|
||||
)
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
# Create a reversible encoded ID
|
||||
data = json.dumps({"name": self.name, "type": self.type})
|
||||
return base64.urlsafe_b64encode(data.encode()).decode()
|
||||
|
||||
@classmethod
|
||||
def from_id(cls, id_: str):
|
||||
data = json.loads(base64.urlsafe_b64decode(id_).decode())
|
||||
return data["name"], data["type"]
|
||||
|
||||
@property
|
||||
def rom_count(self) -> int:
|
||||
return len(self.rom_ids)
|
||||
|
||||
@property
|
||||
def path_covers_small(self) -> list[str]:
|
||||
return [
|
||||
f"{FRONTEND_RESOURCES_PATH}/{cover}?ts={self.updated_at}"
|
||||
for cover in self.path_covers_s
|
||||
]
|
||||
|
||||
@property
|
||||
def path_covers_large(self) -> list[str]:
|
||||
return [
|
||||
f"{FRONTEND_RESOURCES_PATH}/{cover}?ts={self.updated_at}"
|
||||
for cover in self.path_covers_l
|
||||
]
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"name",
|
||||
"type",
|
||||
name="unique_virtual_collection_name_type",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -39,7 +39,9 @@ class Firmware(BaseModel):
|
||||
md5_hash: Mapped[str] = mapped_column(String(length=100))
|
||||
sha1_hash: Mapped[str] = mapped_column(String(length=100))
|
||||
|
||||
platform: Mapped[Platform] = relationship(lazy="joined", back_populates="firmware")
|
||||
platform: Mapped[Platform] = relationship(
|
||||
lazy="immediate", back_populates="firmware"
|
||||
)
|
||||
|
||||
@property
|
||||
def platform_slug(self) -> str:
|
||||
|
||||
@@ -173,6 +173,12 @@
|
||||
"md5": "79ae0d2bb1901b7e606b6dc339b79a97",
|
||||
"sha1": "06fc753d015b43ca1787f4cfd9331b1674202e64"
|
||||
},
|
||||
"arcade:naomi2.zip": {
|
||||
"size": "2189528",
|
||||
"crc": "f25cf3a8",
|
||||
"md5": "0ea5bf0345e27b1cf51bbde1bd398eca",
|
||||
"sha1": "533ef12a6d22726da81a50f08871c6e9a377a328"
|
||||
},
|
||||
"atari8bit:ATARIBAS.ROM": {
|
||||
"size": "8192",
|
||||
"crc": "7d684184",
|
||||
@@ -335,6 +341,12 @@
|
||||
"md5": "80dcd1ad1a4cf65d64b7ba10504e8190",
|
||||
"sha1": "032cb1c1c75b9a191fa1230978971698d9d2a17f"
|
||||
},
|
||||
"msx2:DISK.ROM": {
|
||||
"size": "16384",
|
||||
"crc": "721f61df",
|
||||
"md5": "80dcd1ad1a4cf65d64b7ba10504e8190",
|
||||
"sha1": "032cb1c1c75b9a191fa1230978971698d9d2a17f"
|
||||
},
|
||||
"msx:FMPAC.ROM": {
|
||||
"size": "65536",
|
||||
"crc": "0e84505d",
|
||||
@@ -1439,6 +1451,12 @@
|
||||
"md5": "08ca8b2dba6662e8024f9e789711c6fc",
|
||||
"sha1": "5142f205912869b673a71480c5828b1eaed782a8"
|
||||
},
|
||||
"neo-geo-cd:neocdz.zip": {
|
||||
"size": "214876",
|
||||
"crc": "19681e91",
|
||||
"md5": "c38cb8e50321783e413dc5ff292a3ff8",
|
||||
"sha1": "bf6b379c204da77dece1aedf83ff35227a623e5d"
|
||||
},
|
||||
"ps:PSXONPSP660.BIN": {
|
||||
"size": "524288",
|
||||
"crc": "5660f34f",
|
||||
@@ -2122,5 +2140,107 @@
|
||||
"crc": "1466aed4",
|
||||
"md5": "88dc7876d584f90e4106f91444ab23b7",
|
||||
"sha1": "072c6160d4e7d406f5d8f5b1b66066c797d35561"
|
||||
},
|
||||
"doom:DOOM1.WAD": {
|
||||
"size": "4196020",
|
||||
"crc": "162b696a",
|
||||
"md5": "f0cefca49926d00903cf57551d901abe",
|
||||
"sha1": "5b2e249b9c5133ec987b3ea77596381dc0d6bc1d"
|
||||
},
|
||||
"doom:DOOM.WAD": {
|
||||
"size": "12733492",
|
||||
"crc": "cff03d9f",
|
||||
"md5": "4461d4511386518e784c647e3128e7bc",
|
||||
"sha1": "997bae5e5a190c5bb3b1fb9e7e3e75b2da88cb27"
|
||||
},
|
||||
"doom:DOOM2.WAD": {
|
||||
"size": "14802506",
|
||||
"crc": "09b8a6ae",
|
||||
"md5": "9aa3cbf65b961d0bdac98ec403b832e1",
|
||||
"sha1": "c745f04a6abc2e6d2a2d52382f45500dd2a260be"
|
||||
},
|
||||
"doom:DOOM2F.WAD": {
|
||||
"size": "14607420",
|
||||
"crc": "27eaae69",
|
||||
"md5": "3cb02349b3df649c86290907eed64e7b",
|
||||
"sha1": "d510c877031bbd5f3d198581a2c8651e09b9861f"
|
||||
},
|
||||
"doom:DOOM64.WAD": {
|
||||
"size": "15103212",
|
||||
"crc": "65816192",
|
||||
"md5": "0aaba212339c72250f8a53a0a2b6189e",
|
||||
"sha1": "d041456bea851c173f65ac6ab3f2ee61bb0b8b53"
|
||||
},
|
||||
"doom:TNT.WAD": {
|
||||
"size": "18304630",
|
||||
"crc": "15f18ddb",
|
||||
"md5": "8974e3117ed4a1839c752d5e11ab1b7b",
|
||||
"sha1": "9820e2a3035f0cdd87f69a7d57c59a7a267c9409"
|
||||
},
|
||||
"doom:PLUTONIA.WAD": {
|
||||
"size": "17531493",
|
||||
"crc": "650b998d",
|
||||
"md5": "24037397056e919961005e08611623f4",
|
||||
"sha1": " 816c7c6b0098f66c299c9253f62bd908456efb63"
|
||||
},
|
||||
"doom:HERETIC1.WAD": {
|
||||
"size": "5120920",
|
||||
"crc": "22d3f0ca",
|
||||
"md5": "ae779722390ec32fa37b0d361f7d82f8",
|
||||
"sha1": "b4c50ca9bea07f7c35250a1a11906091971c05ae"
|
||||
},
|
||||
"doom:HERETIC.WAD": {
|
||||
"size": "14189976",
|
||||
"crc": "5b16049e",
|
||||
"md5": "66d686b1ed6d35ff103f15dbd30e0341",
|
||||
"sha1": "f489d479371df32f6d280a0cb23b59a35ba2b833"
|
||||
},
|
||||
"doom:HEXEN.WAD": {
|
||||
"size": "20083672",
|
||||
"crc": "dca9114c",
|
||||
"md5": "abb033caf81e26f12a2103e1fa25453f",
|
||||
"sha1": "4b53832f0733c1e29e5f1de2428e5475e891af29"
|
||||
},
|
||||
"doom:HEXDD.WAD": {
|
||||
"size": "4440584",
|
||||
"crc": "fd5eb11d",
|
||||
"md5": "78d5898e99e220e4de64edaa0e479593",
|
||||
"sha1": "081f6a2024643b54ef4a436a85508539b6d20a1e"
|
||||
},
|
||||
"doom:STRIFE0.WAD": {
|
||||
"size": "9934413",
|
||||
"crc": "93c144dd",
|
||||
"md5": "bb545b9c4eca0ff92c14d466b3294023",
|
||||
"sha1": "bc0a110bf27aee89a0b2fc8111e2391ede891b8d"
|
||||
},
|
||||
"doom:STRIFE1.WAD": {
|
||||
"size": "28377364",
|
||||
"crc": "4234ace5",
|
||||
"md5": "2fed2031a5b03892106e0f117f17901f",
|
||||
"sha1": "64c13b951a845ca7f8081f68138a6181557458d1"
|
||||
},
|
||||
"doom:VOICES.WAD": {
|
||||
"size": "27319149",
|
||||
"crc": "cd12ebcf",
|
||||
"md5": "082234d6a3f7086424856478b5aa9e95",
|
||||
"sha1": "ec6883100d807b894a98f426d024d22c77b63e7f"
|
||||
},
|
||||
"doom:CHEX.WAD": {
|
||||
"size": "12361532",
|
||||
"crc": "298dd5b5",
|
||||
"md5": "25485721882b050afa96a56e5758dd52",
|
||||
"sha1": "eca9cff1014ce5081804e193588d96c6ddb35432"
|
||||
},
|
||||
"xbox:Complex_4627.bin": {
|
||||
"size": "1048576",
|
||||
"crc": "ccb97a84",
|
||||
"md5": "ec00e31e746de2473acfe7903c5a4cb7",
|
||||
"sha1": "6639b6693784574d204c42703a74fd8b088a3a5e"
|
||||
},
|
||||
"xbox:mcpx_1.0.bin": {
|
||||
"size": "512",
|
||||
"crc": "0b07d1f1",
|
||||
"md5": "d49c52a4102f6df7bcf8d0617ac475ed",
|
||||
"sha1": "5d270675b54eb8071b480e42d22a3015ac211cef"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import enum
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any, Sequence
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from config import FRONTEND_RESOURCES_PATH
|
||||
from models.base import BaseModel
|
||||
@@ -17,7 +17,6 @@ from sqlalchemy import (
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
func,
|
||||
)
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from utils.database import CustomJSON, safe_float, safe_int
|
||||
@@ -134,12 +133,19 @@ class Rom(BaseModel):
|
||||
primaryjoin="Rom.id == SiblingRom.rom_id",
|
||||
secondaryjoin="Rom.id == SiblingRom.sibling_rom_id",
|
||||
)
|
||||
files: Mapped[list[RomFile]] = relationship(back_populates="rom", lazy="immediate")
|
||||
files: Mapped[list[RomFile]] = relationship(lazy="immediate", back_populates="rom")
|
||||
saves: Mapped[list[Save]] = relationship(back_populates="rom")
|
||||
states: Mapped[list[State]] = relationship(back_populates="rom")
|
||||
screenshots: Mapped[list[Screenshot]] = relationship(back_populates="rom")
|
||||
rom_users: Mapped[list[RomUser]] = relationship(back_populates="rom")
|
||||
|
||||
collections: Mapped[list[Collection]] = relationship(
|
||||
"Collection",
|
||||
secondary="collections_roms",
|
||||
collection_class=set,
|
||||
back_populates="roms",
|
||||
)
|
||||
|
||||
@property
|
||||
def platform_slug(self) -> str:
|
||||
return self.platform.slug
|
||||
@@ -164,10 +170,6 @@ class Rom(BaseModel):
|
||||
def full_path(self) -> str:
|
||||
return f"{self.fs_path}/{self.fs_name}"
|
||||
|
||||
@cached_property
|
||||
def has_cover(self) -> bool:
|
||||
return bool(self.path_cover_s or self.path_cover_l)
|
||||
|
||||
@cached_property
|
||||
def has_manual(self) -> bool:
|
||||
return bool(self.path_manual)
|
||||
@@ -189,14 +191,30 @@ class Rom(BaseModel):
|
||||
def fs_size_bytes(self) -> int:
|
||||
return sum(f.file_size_bytes for f in self.files)
|
||||
|
||||
def get_collections(self) -> Sequence[Collection]:
|
||||
from handler.database import db_collection_handler
|
||||
@property
|
||||
def fs_resources_path(self) -> str:
|
||||
return f"roms/{str(self.platform_id)}/{str(self.id)}"
|
||||
|
||||
return db_collection_handler.get_collections_by_rom_id(
|
||||
self.id,
|
||||
order_by=[func.lower("name")],
|
||||
@property
|
||||
def path_cover_small(self) -> str:
|
||||
return (
|
||||
f"{FRONTEND_RESOURCES_PATH}/{self.path_cover_s}?ts={self.updated_at}"
|
||||
if self.path_cover_s
|
||||
else ""
|
||||
)
|
||||
|
||||
@property
|
||||
def path_cover_large(self) -> str:
|
||||
return (
|
||||
f"{FRONTEND_RESOURCES_PATH}/{self.path_cover_l}?ts={self.updated_at}"
|
||||
if self.path_cover_l
|
||||
else ""
|
||||
)
|
||||
|
||||
@property
|
||||
def is_unidentified(self) -> bool:
|
||||
return not self.igdb_id and not self.moby_id
|
||||
|
||||
# Metadata fields
|
||||
@property
|
||||
def youtube_video_id(self) -> str:
|
||||
@@ -251,7 +269,7 @@ class Rom(BaseModel):
|
||||
return []
|
||||
|
||||
@property
|
||||
def collections(self) -> list[str]:
|
||||
def meta_collections(self) -> list[str]:
|
||||
if self.igdb_metadata:
|
||||
return self.igdb_metadata.get("collections", [])
|
||||
return []
|
||||
@@ -274,10 +292,6 @@ class Rom(BaseModel):
|
||||
return [r["rating"] for r in self.igdb_metadata.get("age_ratings", [])]
|
||||
return []
|
||||
|
||||
@property
|
||||
def fs_resources_path(self) -> str:
|
||||
return f"roms/{str(self.platform_id)}/{str(self.id)}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.fs_name
|
||||
|
||||
|
||||
11
frontend/src/__generated__/index.ts
generated
11
frontend/src/__generated__/index.ts
generated
@@ -4,11 +4,11 @@
|
||||
/* eslint-disable */
|
||||
|
||||
export type { AddFirmwareResponse } from './models/AddFirmwareResponse';
|
||||
export type { Body_add_collection_api_collections_post } from './models/Body_add_collection_api_collections_post';
|
||||
export type { Body_add_firmware_api_firmware_post } from './models/Body_add_firmware_api_firmware_post';
|
||||
export type { Body_add_saves_api_saves_post } from './models/Body_add_saves_api_saves_post';
|
||||
export type { Body_add_screenshots_api_screenshots_post } from './models/Body_add_screenshots_api_screenshots_post';
|
||||
export type { Body_add_states_api_states_post } from './models/Body_add_states_api_states_post';
|
||||
export type { Body_add_collection_api_collections__post } from './models/Body_add_collection_api_collections__post';
|
||||
export type { Body_add_firmware_api_firmware__post } from './models/Body_add_firmware_api_firmware__post';
|
||||
export type { Body_add_saves_api_saves__post } from './models/Body_add_saves_api_saves__post';
|
||||
export type { Body_add_screenshots_api_screenshots__post } from './models/Body_add_screenshots_api_screenshots__post';
|
||||
export type { Body_add_states_api_states__post } from './models/Body_add_states_api_states__post';
|
||||
export type { Body_token_api_token_post } from './models/Body_token_api_token_post';
|
||||
export type { Body_update_collection_api_collections__id__put } from './models/Body_update_collection_api_collections__id__put';
|
||||
export type { Body_update_rom_api_roms__id__put } from './models/Body_update_rom_api_roms__id__put';
|
||||
@@ -58,6 +58,7 @@ export type { UploadedStatesResponse } from './models/UploadedStatesResponse';
|
||||
export type { UserNotesSchema } from './models/UserNotesSchema';
|
||||
export type { UserSchema } from './models/UserSchema';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
export type { VirtualCollectionSchema } from './models/VirtualCollectionSchema';
|
||||
export type { WatcherDict } from './models/WatcherDict';
|
||||
export type { WebrcadeFeedCategorySchema } from './models/WebrcadeFeedCategorySchema';
|
||||
export type { WebrcadeFeedItemPropsSchema } from './models/WebrcadeFeedItemPropsSchema';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_add_collection_api_collections_post = {
|
||||
export type Body_add_collection_api_collections__post = {
|
||||
artwork?: (Blob | null);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_add_firmware_api_firmware_post = {
|
||||
export type Body_add_firmware_api_firmware__post = {
|
||||
files: Array<Blob>;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_add_saves_api_saves_post = {
|
||||
export type Body_add_saves_api_saves__post = {
|
||||
saves: Array<Blob>;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_add_screenshots_api_screenshots_post = {
|
||||
export type Body_add_screenshots_api_screenshots__post = {
|
||||
screenshots: Array<Blob>;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type Body_add_states_api_states_post = {
|
||||
export type Body_add_states_api_states__post = {
|
||||
states: Array<Blob>;
|
||||
};
|
||||
|
||||
@@ -6,15 +6,17 @@ export type CollectionSchema = {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
path_cover_l: (string | null);
|
||||
path_cover_s: (string | null);
|
||||
has_cover: boolean;
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
path_covers_small: Array<string>;
|
||||
path_covers_large: Array<string>;
|
||||
url_cover: string;
|
||||
roms: Array<number>;
|
||||
rom_ids: Array<number>;
|
||||
rom_count: number;
|
||||
user_id: number;
|
||||
user__username: string;
|
||||
is_public: boolean;
|
||||
is_favorite: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
@@ -39,20 +39,21 @@ export type DetailedRomSchema = {
|
||||
alternative_names: Array<string>;
|
||||
genres: Array<string>;
|
||||
franchises: Array<string>;
|
||||
collections: Array<string>;
|
||||
meta_collections: Array<string>;
|
||||
companies: Array<string>;
|
||||
game_modes: Array<string>;
|
||||
age_ratings: Array<string>;
|
||||
igdb_metadata: (RomIGDBMetadata | null);
|
||||
moby_metadata: (RomMobyMetadata | null);
|
||||
ss_metadata: (RomMobyMetadata | null);
|
||||
path_cover_s: (string | null);
|
||||
path_cover_l: (string | null);
|
||||
has_cover: boolean;
|
||||
url_cover: (string | null);
|
||||
has_manual: boolean;
|
||||
path_manual: (string | null);
|
||||
url_manual: (string | null);
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
url_cover: (string | null);
|
||||
is_unidentified: boolean;
|
||||
revision: (string | null);
|
||||
regions: Array<string>;
|
||||
languages: Array<string>;
|
||||
|
||||
9
frontend/src/__generated__/models/RomSchema.ts
generated
9
frontend/src/__generated__/models/RomSchema.ts
generated
@@ -32,20 +32,21 @@ export type RomSchema = {
|
||||
alternative_names: Array<string>;
|
||||
genres: Array<string>;
|
||||
franchises: Array<string>;
|
||||
collections: Array<string>;
|
||||
meta_collections: Array<string>;
|
||||
companies: Array<string>;
|
||||
game_modes: Array<string>;
|
||||
age_ratings: Array<string>;
|
||||
igdb_metadata: (RomIGDBMetadata | null);
|
||||
moby_metadata: (RomMobyMetadata | null);
|
||||
ss_metadata: (RomMobyMetadata | null);
|
||||
path_cover_s: (string | null);
|
||||
path_cover_l: (string | null);
|
||||
has_cover: boolean;
|
||||
url_cover: (string | null);
|
||||
has_manual: boolean;
|
||||
path_manual: (string | null);
|
||||
url_manual: (string | null);
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
url_cover: (string | null);
|
||||
is_unidentified: boolean;
|
||||
revision: (string | null);
|
||||
regions: Array<string>;
|
||||
languages: Array<string>;
|
||||
|
||||
@@ -34,20 +34,21 @@ export type SimpleRomSchema = {
|
||||
alternative_names: Array<string>;
|
||||
genres: Array<string>;
|
||||
franchises: Array<string>;
|
||||
collections: Array<string>;
|
||||
meta_collections: Array<string>;
|
||||
companies: Array<string>;
|
||||
game_modes: Array<string>;
|
||||
age_ratings: Array<string>;
|
||||
igdb_metadata: (RomIGDBMetadata | null);
|
||||
moby_metadata: (RomMobyMetadata | null);
|
||||
ss_metadata: (RomMobyMetadata | null);
|
||||
path_cover_s: (string | null);
|
||||
path_cover_l: (string | null);
|
||||
has_cover: boolean;
|
||||
url_cover: (string | null);
|
||||
has_manual: boolean;
|
||||
path_manual: (string | null);
|
||||
url_manual: (string | null);
|
||||
path_cover_small: (string | null);
|
||||
path_cover_large: (string | null);
|
||||
url_cover: (string | null);
|
||||
is_unidentified: boolean;
|
||||
revision: (string | null);
|
||||
regions: Array<string>;
|
||||
languages: Array<string>;
|
||||
|
||||
15
frontend/src/__generated__/models/VirtualCollectionSchema.ts
generated
Normal file
15
frontend/src/__generated__/models/VirtualCollectionSchema.ts
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type VirtualCollectionSchema = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
description: string;
|
||||
rom_ids: Array<number>;
|
||||
rom_count: number;
|
||||
path_covers_small: Array<string>;
|
||||
path_covers_large: Array<string>;
|
||||
};
|
||||
|
||||
@@ -29,11 +29,7 @@ const unmatchedCoverImage = computed(() =>
|
||||
>
|
||||
<v-img
|
||||
id="background-image"
|
||||
:src="
|
||||
!currentRom.igdb_id && !currentRom.moby_id && !currentRom.has_cover
|
||||
? unmatchedCoverImage
|
||||
: `/assets/romm/resources/${currentRom.path_cover_l}?ts=${currentRom.updated_at}`
|
||||
"
|
||||
:src="currentRom?.path_cover_large || unmatchedCoverImage"
|
||||
lazy
|
||||
cover
|
||||
>
|
||||
|
||||
@@ -20,7 +20,7 @@ const router = useRouter();
|
||||
const filters = [
|
||||
{ value: "genres", name: t("rom.genres") },
|
||||
{ value: "franchises", name: t("rom.franchises") },
|
||||
{ value: "collections", name: t("rom.collections") },
|
||||
{ value: "meta_collections", name: t("rom.collections") },
|
||||
{ value: "companies", name: t("rom.companies") },
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ function setFilters() {
|
||||
galleryFilterStore.setFilterCollections([
|
||||
...new Set(
|
||||
romsStore.filteredRoms
|
||||
.flatMap((rom) => rom.collections.map((collection) => collection))
|
||||
.flatMap((rom) => rom.meta_collections.map((collection) => collection))
|
||||
.sort(),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -50,7 +50,7 @@ function setFilters() {
|
||||
galleryFilterStore.setFilterCollections([
|
||||
...new Set(
|
||||
romsStore.filteredRoms
|
||||
.flatMap((rom) => rom.collections.map((collection) => collection))
|
||||
.flatMap((rom) => rom.meta_collections.map((collection) => collection))
|
||||
.sort(),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -68,7 +68,7 @@ function resetSelection() {
|
||||
|
||||
async function addToFavourites() {
|
||||
if (!favCollection.value) return;
|
||||
favCollection.value.roms = favCollection.value.roms.concat(
|
||||
favCollection.value.rom_ids = favCollection.value.rom_ids.concat(
|
||||
selectedRoms.value.map((r) => r.id),
|
||||
);
|
||||
await collectionApi
|
||||
@@ -97,7 +97,7 @@ async function addToFavourites() {
|
||||
|
||||
async function removeFromFavourites() {
|
||||
if (!favCollection.value) return;
|
||||
favCollection.value.roms = favCollection.value.roms.filter(
|
||||
favCollection.value.rom_ids = favCollection.value.rom_ids.filter(
|
||||
(value) => !selectedRoms.value.map((r) => r.id).includes(value),
|
||||
);
|
||||
if (romsStore.currentCollection?.name.toLowerCase() == "favourites") {
|
||||
|
||||
@@ -36,7 +36,7 @@ const gridCollections = isNull(localStorage.getItem("settings.gridCollections"))
|
||||
<collection-card
|
||||
show-rom-count
|
||||
transform-scale
|
||||
:key="collection.updated_at"
|
||||
:key="collection.id"
|
||||
:collection="collection"
|
||||
with-link
|
||||
title-on-hover
|
||||
|
||||
48
frontend/src/components/Home/VirtualCollections.vue
Normal file
48
frontend/src/components/Home/VirtualCollections.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import VirtualCollectionCard from "@/components/common/Collection/Virtual/Card.vue";
|
||||
import RSection from "@/components/common/RSection.vue";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import { views } from "@/utils";
|
||||
import { isNull } from "lodash";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
const collections = storeCollections();
|
||||
const gridCollections = isNull(localStorage.getItem("settings.gridCollections"))
|
||||
? true
|
||||
: localStorage.getItem("settings.gridCollections") === "true";
|
||||
</script>
|
||||
<template>
|
||||
<r-section
|
||||
icon="mdi-bookmark-box-multiple"
|
||||
:title="t('common.virtual-collections')"
|
||||
>
|
||||
<template #content>
|
||||
<v-row
|
||||
:class="{ 'flex-nowrap overflow-x-auto': !gridCollections }"
|
||||
no-gutters
|
||||
>
|
||||
<v-col
|
||||
v-for="collection in collections.virtualCollections"
|
||||
:key="collection.name"
|
||||
class="pa-1"
|
||||
:cols="views[0]['size-cols']"
|
||||
:sm="views[0]['size-sm']"
|
||||
:md="views[0]['size-md']"
|
||||
:lg="views[0]['size-lg']"
|
||||
:xl="views[0]['size-xl']"
|
||||
>
|
||||
<virtual-collection-card
|
||||
show-rom-count
|
||||
show-title
|
||||
transform-scale
|
||||
:key="collection.id"
|
||||
:collection="collection"
|
||||
with-link
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</r-section>
|
||||
</template>
|
||||
@@ -1,12 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import InterfaceOption from "@/components/Settings/UserInterface/InterfaceOption.vue";
|
||||
import RSection from "@/components/common/RSection.vue";
|
||||
import collectionApi from "@/services/api/collection";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import { computed, ref } from "vue";
|
||||
import { isNull } from "lodash";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
const collectionsStore = storeCollections();
|
||||
|
||||
// Initializing refs from localStorage
|
||||
const storedShowRecentRoms = localStorage.getItem("settings.showRecentRoms");
|
||||
const showRecentRomsRef = ref(
|
||||
@@ -48,6 +52,22 @@ const storedGridCollections = localStorage.getItem("settings.gridCollections");
|
||||
const gridCollectionsRef = ref(
|
||||
isNull(storedGridCollections) ? true : storedGridCollections === "true",
|
||||
);
|
||||
const storedShowVirtualCollections = localStorage.getItem(
|
||||
"settings.showVirtualCollections",
|
||||
);
|
||||
const showVirtualCollectionsRef = ref(
|
||||
isNull(storedShowVirtualCollections)
|
||||
? true
|
||||
: storedShowVirtualCollections === "true",
|
||||
);
|
||||
const storedVirtualCollectionType = localStorage.getItem(
|
||||
"settings.virtualCollectionType",
|
||||
);
|
||||
const virtualCollectionTypeRef = ref(
|
||||
isNull(storedVirtualCollectionType)
|
||||
? "collection"
|
||||
: storedVirtualCollectionType,
|
||||
);
|
||||
|
||||
const storedGroupRoms = localStorage.getItem("settings.groupRoms");
|
||||
const groupRomsRef = ref(
|
||||
@@ -214,6 +234,20 @@ const toggleGridCollections = (value: boolean) => {
|
||||
gridCollectionsRef.value = value;
|
||||
localStorage.setItem("settings.gridCollections", value.toString());
|
||||
};
|
||||
const toggleShowVirtualCollections = (value: boolean) => {
|
||||
showVirtualCollectionsRef.value = value;
|
||||
localStorage.setItem("settings.showVirtualCollections", value.toString());
|
||||
};
|
||||
const setVirtualCollectionType = async (value: string) => {
|
||||
virtualCollectionTypeRef.value = value;
|
||||
localStorage.setItem("settings.virtualCollectionType", value);
|
||||
|
||||
await collectionApi
|
||||
.getVirtualCollections({ type: value })
|
||||
.then(({ data: virtualCollections }) => {
|
||||
collectionsStore.setVirtual(virtualCollections);
|
||||
});
|
||||
};
|
||||
|
||||
const toggleGroupRoms = (value: boolean) => {
|
||||
groupRomsRef.value = value;
|
||||
@@ -295,6 +329,50 @@ const toggleStatus = (value: boolean) => {
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-chip
|
||||
label
|
||||
variant="text"
|
||||
prepend-icon="mdi-view-grid"
|
||||
class="ml-2 mt-4"
|
||||
>{{ t("common.virtual-collections") }}</v-chip
|
||||
>
|
||||
<v-divider class="border-opacity-25 mx-2 mb-2" />
|
||||
<v-row class="py-1" no-gutters>
|
||||
<v-col cols="12" md="6">
|
||||
<interface-option
|
||||
class="mx-2"
|
||||
:title="t('settings.show-virtual-collections')"
|
||||
:description="t('settings.show-virtual-collections-desc')"
|
||||
:icon="
|
||||
showVirtualCollectionsRef
|
||||
? 'mdi-bookmark-box-multiple'
|
||||
: 'mdi-bookmark-box-multiple'
|
||||
"
|
||||
v-model="showVirtualCollectionsRef"
|
||||
@update:model-value="toggleShowVirtualCollections"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="virtualCollectionTypeRef"
|
||||
:items="[
|
||||
{ title: 'IGDB Collection', value: 'collection' },
|
||||
{ title: 'Franchise', value: 'franchise' },
|
||||
{ title: 'Genre', value: 'genre' },
|
||||
{ title: 'Play Mode', value: 'mode' },
|
||||
{ title: 'Developer', value: 'company' },
|
||||
{ title: 'All (slow)', value: 'all' },
|
||||
]"
|
||||
:label="t('settings.virtual-collection-type')"
|
||||
class="mx-2"
|
||||
dense
|
||||
outlined
|
||||
hide-details
|
||||
:disabled="!showVirtualCollectionsRef"
|
||||
@update:model-value="setVirtualCollectionType"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</r-section>
|
||||
</template>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { Collection } from "@/stores/collections";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { getCollectionCoverImage, getFavoriteCoverImage } from "@/utils/covers";
|
||||
import { computed } from "vue";
|
||||
|
||||
// Props
|
||||
const props = withDefaults(
|
||||
@@ -24,12 +24,65 @@ const props = withDefaults(
|
||||
);
|
||||
|
||||
const galleryViewStore = storeGalleryView();
|
||||
|
||||
const memoizedCovers = ref({
|
||||
large: ["", ""],
|
||||
small: ["", ""],
|
||||
});
|
||||
|
||||
const collectionCoverImage = computed(() =>
|
||||
getCollectionCoverImage(props.collection.name),
|
||||
);
|
||||
const favoriteCoverImage = computed(() =>
|
||||
getFavoriteCoverImage(props.collection.name),
|
||||
props.collection.name?.toLowerCase() == "favourites"
|
||||
? getFavoriteCoverImage(props.collection.name)
|
||||
: getCollectionCoverImage(props.collection.name),
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.src) {
|
||||
memoizedCovers.value = {
|
||||
large: [props.src, props.src],
|
||||
small: [props.src, props.src],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (props.collection.path_cover_large && props.collection.path_cover_small) {
|
||||
memoizedCovers.value = {
|
||||
large: [
|
||||
props.collection.path_cover_large,
|
||||
props.collection.path_cover_large,
|
||||
],
|
||||
small: [
|
||||
props.collection.path_cover_small,
|
||||
props.collection.path_cover_small,
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const largeCoverUrls = props.collection.path_covers_large || [];
|
||||
const smallCoverUrls = props.collection.path_covers_small || [];
|
||||
|
||||
if (largeCoverUrls.length < 2) {
|
||||
memoizedCovers.value = {
|
||||
large: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
small: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const shuffledLarge = [...largeCoverUrls].sort(() => Math.random() - 0.5);
|
||||
const shuffledSmall = [...smallCoverUrls].sort(() => Math.random() - 0.5);
|
||||
|
||||
memoizedCovers.value = {
|
||||
large: [shuffledLarge[0], shuffledLarge[1]],
|
||||
small: [shuffledSmall[0], shuffledSmall[1]],
|
||||
};
|
||||
});
|
||||
|
||||
const firstCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstSmallCover = computed(() => memoizedCovers.value.small[0]);
|
||||
const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,61 +102,35 @@ const favoriteCoverImage = computed(() =>
|
||||
}"
|
||||
:elevation="isHovering && transformScale ? 20 : 3"
|
||||
>
|
||||
<v-img
|
||||
cover
|
||||
:src="
|
||||
src
|
||||
? src
|
||||
: collection.has_cover
|
||||
? `/assets/romm/resources/${collection.path_cover_l}?ts=${collection.updated_at}`
|
||||
: collection.name && collection.name.toLowerCase() == 'favourites'
|
||||
? favoriteCoverImage
|
||||
: collectionCoverImage
|
||||
"
|
||||
:lazy-src="
|
||||
src
|
||||
? src
|
||||
: collection.has_cover
|
||||
? `/assets/romm/resources/${collection.path_cover_s}?ts=${collection.updated_at}`
|
||||
: collection.name && collection.name.toLowerCase() == 'favourites'
|
||||
? favoriteCoverImage
|
||||
: collectionCoverImage
|
||||
"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
>
|
||||
<template v-if="titleOnHover">
|
||||
<v-expand-transition>
|
||||
<div
|
||||
v-if="isHovering || !collection.has_cover"
|
||||
class="translucent-dark text-caption text-center text-white"
|
||||
>
|
||||
<v-list-item>{{ collection.name }}</v-list-item>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</template>
|
||||
|
||||
<div class="position-absolute append-inner">
|
||||
<slot name="append-inner"></slot>
|
||||
<v-row class="pa-1 justify-center bg-primary">
|
||||
<div
|
||||
:title="collection.name?.toString()"
|
||||
class="py-4 px-6 text-truncate text-caption"
|
||||
>
|
||||
<span>{{ collection.name }}</span>
|
||||
</div>
|
||||
|
||||
<template #error>
|
||||
</v-row>
|
||||
<div
|
||||
class="image-container"
|
||||
:style="{ aspectRatio: galleryViewStore.defaultAspectRatioCollection }"
|
||||
>
|
||||
<div class="split-image first-image">
|
||||
<v-img
|
||||
:src="collectionCoverImage"
|
||||
cover
|
||||
:src="firstCover"
|
||||
:lazy-src="firstSmallCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<div class="d-flex align-center justify-center fill-height">
|
||||
<v-progress-circular
|
||||
:width="2"
|
||||
:size="40"
|
||||
color="primary"
|
||||
indeterminate
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
<div class="split-image second-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="secondCover"
|
||||
:lazy-src="secondSmallCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<v-chip
|
||||
v-if="showRomCount"
|
||||
class="bg-background position-absolute"
|
||||
@@ -116,9 +143,28 @@ const favoriteCoverImage = computed(() =>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.append-inner {
|
||||
bottom: 0rem;
|
||||
right: 0rem;
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.split-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.first-image {
|
||||
clip-path: polygon(0 0, 100% 0, 0% 100%, 0 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.second-image {
|
||||
clip-path: polygon(0% 100%, 100% 0, 100% 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,7 +37,7 @@ const HEADERS = [
|
||||
|
||||
async function addRomsToCollection() {
|
||||
if (!selectedCollection.value) return;
|
||||
selectedCollection.value.roms.push(...roms.value.map((r) => r.id));
|
||||
selectedCollection.value.rom_ids.push(...roms.value.map((r) => r.id));
|
||||
await collectionApi
|
||||
.updateCollection({ collection: selectedCollection.value })
|
||||
.then(({ data }) => {
|
||||
|
||||
@@ -7,11 +7,13 @@ import type { UpdatedCollection } from "@/services/api/collection";
|
||||
import collectionApi from "@/services/api/collection";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import { ROUTES } from "@/plugins/router";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, ref, watch } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
@@ -21,9 +23,13 @@ const romsStore = storeRoms();
|
||||
const collectionsStore = storeCollections();
|
||||
const selectedCollection = ref<UpdatedCollection>();
|
||||
const roms = ref<SimpleRom[]>([]);
|
||||
const router = useRouter();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("showRemoveFromCollectionDialog", (romsToRemove) => {
|
||||
if (!romsStore.currentCollection) return;
|
||||
|
||||
roms.value = romsToRemove;
|
||||
selectedCollection.value = romsStore.currentCollection;
|
||||
show.value = true;
|
||||
});
|
||||
const HEADERS = [
|
||||
@@ -37,7 +43,7 @@ const HEADERS = [
|
||||
|
||||
async function removeRomsFromCollection() {
|
||||
if (!selectedCollection.value) return;
|
||||
selectedCollection.value.roms = selectedCollection.value.roms.filter(
|
||||
selectedCollection.value.rom_ids = selectedCollection.value.rom_ids.filter(
|
||||
(id) => !roms.value.map((r) => r.id).includes(id),
|
||||
);
|
||||
await collectionApi
|
||||
@@ -63,6 +69,10 @@ async function removeRomsFromCollection() {
|
||||
.finally(() => {
|
||||
emitter?.emit("showLoadingDialog", { loading: false, scrim: false });
|
||||
romsStore.resetSelection();
|
||||
if (selectedCollection.value?.rom_ids.length == 0) {
|
||||
router.push({ name: "home" });
|
||||
}
|
||||
closeDialog();
|
||||
});
|
||||
closeDialog();
|
||||
}
|
||||
@@ -84,9 +94,9 @@ function closeDialog() {
|
||||
>
|
||||
<template #header>
|
||||
<v-row no-gutters class="justify-center">
|
||||
<span>{{ t("rom.remove-from-collection-part1") }}</span>
|
||||
<span>{{ t("rom.removing-from-collection-part1") }}</span>
|
||||
<span class="text-primary mx-1">{{ roms.length }}</span>
|
||||
<span>{{ t("rom.remove-from-collection-part2") }}</span>
|
||||
<span>{{ t("rom.removing-from-collection-part2") }}</span>
|
||||
</v-row>
|
||||
</template>
|
||||
<template #prepend>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import type { Collection } from "@/stores/collections";
|
||||
import { getCollectionCoverImage, getFavoriteCoverImage } from "@/utils/covers";
|
||||
import { computed } from "vue";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ collection: Collection; size?: number }>(),
|
||||
@@ -9,29 +10,104 @@ const props = withDefaults(
|
||||
size: 45,
|
||||
},
|
||||
);
|
||||
const theme = useTheme();
|
||||
|
||||
const memoizedCovers = ref({
|
||||
large: ["", ""],
|
||||
small: ["", ""],
|
||||
});
|
||||
|
||||
const collectionCoverImage = computed(() =>
|
||||
getCollectionCoverImage(props.collection.name),
|
||||
);
|
||||
const favoriteCoverImage = computed(() =>
|
||||
getFavoriteCoverImage(props.collection.name),
|
||||
props.collection.is_favorite
|
||||
? getFavoriteCoverImage(props.collection.name)
|
||||
: getCollectionCoverImage(props.collection.name),
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.collection.path_cover_large && props.collection.path_cover_small) {
|
||||
memoizedCovers.value = {
|
||||
large: [
|
||||
props.collection.path_cover_large,
|
||||
props.collection.path_cover_large,
|
||||
],
|
||||
small: [
|
||||
props.collection.path_cover_small,
|
||||
props.collection.path_cover_small,
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const largeCoverUrls = props.collection.path_covers_large || [];
|
||||
const smallCoverUrls = props.collection.path_covers_small || [];
|
||||
|
||||
if (largeCoverUrls.length < 2) {
|
||||
memoizedCovers.value = {
|
||||
large: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
small: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const shuffledLarge = [...largeCoverUrls].sort(() => Math.random() - 0.5);
|
||||
const shuffledSmall = [...smallCoverUrls].sort(() => Math.random() - 0.5);
|
||||
|
||||
memoizedCovers.value = {
|
||||
large: [shuffledLarge[0], shuffledLarge[1]],
|
||||
small: [shuffledSmall[0], shuffledSmall[1]],
|
||||
};
|
||||
});
|
||||
|
||||
const firstCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstSmallCover = computed(() => memoizedCovers.value.small[0]);
|
||||
const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-avatar :size="size" rounded="0">
|
||||
<v-img
|
||||
:src="
|
||||
collection.has_cover
|
||||
? `/assets/romm/resources/${collection.path_cover_l}?ts=${collection.updated_at}`
|
||||
: collection.name?.toLowerCase() == 'favourites'
|
||||
? favoriteCoverImage
|
||||
: collectionCoverImage
|
||||
"
|
||||
>
|
||||
<template #error>
|
||||
<v-img :src="collectionCoverImage" />
|
||||
</template>
|
||||
</v-img>
|
||||
<v-avatar :rounded="0" :size="size">
|
||||
<div class="image-container" :style="{ aspectRatio: 1 / 1 }">
|
||||
<div class="split-image first-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="firstCover"
|
||||
:lazy-src="firstSmallCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="split-image second-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="secondCover"
|
||||
:lazy-src="secondSmallCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.split-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.first-image {
|
||||
clip-path: polygon(0 0, 100% 0, 0% 100%, 0 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.second-image {
|
||||
clip-path: polygon(0% 100%, 100% 0, 100% 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
149
frontend/src/components/common/Collection/Virtual/Card.vue
Normal file
149
frontend/src/components/common/Collection/Virtual/Card.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup lang="ts">
|
||||
import type { VirtualCollection } from "@/stores/collections";
|
||||
import storeGalleryView from "@/stores/galleryView";
|
||||
import { useTheme } from "vuetify";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
|
||||
// Props
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
collection: VirtualCollection;
|
||||
transformScale?: boolean;
|
||||
showTitle?: boolean;
|
||||
showRomCount?: boolean;
|
||||
withLink?: boolean;
|
||||
}>(),
|
||||
{
|
||||
transformScale: false,
|
||||
showTitle: false,
|
||||
showRomCount: false,
|
||||
withLink: false,
|
||||
},
|
||||
);
|
||||
|
||||
const theme = useTheme();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
|
||||
const memoizedCovers = ref({
|
||||
large: ["", ""],
|
||||
small: ["", ""],
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
const largeCoverUrls = props.collection.path_covers_large || [];
|
||||
const smallCoverUrls = props.collection.path_covers_small || [];
|
||||
|
||||
if (largeCoverUrls.length < 2) {
|
||||
memoizedCovers.value = {
|
||||
large: [
|
||||
`/assets/default/cover/big_${theme.global.name.value}_collection.png`,
|
||||
`/assets/default/cover/big_${theme.global.name.value}_collection.png`,
|
||||
],
|
||||
small: [
|
||||
`/assets/default/cover/small_${theme.global.name.value}_collection.png`,
|
||||
`/assets/default/cover/small_${theme.global.name.value}_collection.png`,
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const shuffledLarge = [...largeCoverUrls].sort(() => Math.random() - 0.5);
|
||||
const shuffledSmall = [...smallCoverUrls].sort(() => Math.random() - 0.5);
|
||||
|
||||
memoizedCovers.value = {
|
||||
large: [shuffledLarge[0], shuffledLarge[1]],
|
||||
small: [shuffledSmall[0], shuffledSmall[1]],
|
||||
};
|
||||
});
|
||||
|
||||
const firstCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstSmallCover = computed(() => memoizedCovers.value.small[0]);
|
||||
const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-hover v-slot="{ isHovering, props: hoverProps }">
|
||||
<v-card
|
||||
v-bind="{
|
||||
...hoverProps,
|
||||
...(withLink && collection
|
||||
? {
|
||||
to: { name: 'collection', params: { collection: collection.id } },
|
||||
}
|
||||
: {}),
|
||||
}"
|
||||
:class="{
|
||||
'on-hover': isHovering,
|
||||
'transform-scale': transformScale,
|
||||
}"
|
||||
:elevation="isHovering && transformScale ? 20 : 3"
|
||||
>
|
||||
<v-row v-if="showTitle" class="pa-1 justify-center bg-primary">
|
||||
<div
|
||||
:title="collection.name?.toString()"
|
||||
class="py-4 px-6 text-truncate text-caption"
|
||||
>
|
||||
<span>{{ collection.name }}</span>
|
||||
</div>
|
||||
</v-row>
|
||||
|
||||
<div
|
||||
class="image-container"
|
||||
:style="{ aspectRatio: galleryViewStore.defaultAspectRatioCollection }"
|
||||
>
|
||||
<div class="split-image first-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="firstCover"
|
||||
:lazy-src="firstSmallCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
</div>
|
||||
<div class="split-image second-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="secondCover"
|
||||
:lazy-src="secondSmallCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-chip
|
||||
v-if="showRomCount"
|
||||
class="bg-chip position-absolute"
|
||||
size="x-small"
|
||||
style="bottom: 0.5rem; right: 0.5rem"
|
||||
label
|
||||
>
|
||||
{{ collection.rom_count }}
|
||||
</v-chip>
|
||||
</v-card>
|
||||
</v-hover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.split-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.first-image {
|
||||
clip-path: polygon(0 0, 100% 0, 0% 100%, 0 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.second-image {
|
||||
clip-path: polygon(0% 100%, 100% 0, 100% 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import type { VirtualCollection } from "@/stores/collections";
|
||||
import RAvatar from "@/components/common/Collection/Virtual/RAvatar.vue";
|
||||
|
||||
// Props
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
collection: VirtualCollection;
|
||||
withTitle?: boolean;
|
||||
withDescription?: boolean;
|
||||
withRomCount?: boolean;
|
||||
withLink?: boolean;
|
||||
}>(),
|
||||
{
|
||||
withTitle: true,
|
||||
withDescription: true,
|
||||
withRomCount: true,
|
||||
withLink: false,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-list-item
|
||||
:key="collection.id"
|
||||
v-bind="{
|
||||
...(withLink && collection
|
||||
? {
|
||||
to: { name: 'collection', params: { collection: collection.id } },
|
||||
}
|
||||
: {}),
|
||||
}"
|
||||
:value="collection.name"
|
||||
class="py-1 pl-1"
|
||||
>
|
||||
<template #prepend>
|
||||
<r-avatar :size="48" :collection="collection" />
|
||||
</template>
|
||||
<v-row v-if="withTitle" no-gutters
|
||||
><v-col
|
||||
><span class="text-body-1">{{ collection.name }}</span></v-col
|
||||
></v-row
|
||||
>
|
||||
<v-row v-if="withDescription" no-gutters>
|
||||
<v-col>
|
||||
<span class="text-caption text-grey">{{ collection.description }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="withRomCount" #append>
|
||||
<v-chip class="ml-2" size="x-small" label>
|
||||
{{ collection.rom_count }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.not-found-icon {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
100
frontend/src/components/common/Collection/Virtual/RAvatar.vue
Normal file
100
frontend/src/components/common/Collection/Virtual/RAvatar.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup lang="ts">
|
||||
import type { VirtualCollection } from "@/stores/collections";
|
||||
import { computed, ref, watchEffect } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ collection: VirtualCollection; size?: number }>(),
|
||||
{
|
||||
size: 45,
|
||||
},
|
||||
);
|
||||
const theme = useTheme();
|
||||
|
||||
const memoizedCovers = ref({
|
||||
large: ["", ""],
|
||||
small: ["", ""],
|
||||
});
|
||||
|
||||
// Watch for collection changes and update the memoized selection
|
||||
watchEffect(() => {
|
||||
const largeCoverUrls = props.collection.path_covers_large || [];
|
||||
const smallCoverUrls = props.collection.path_covers_small || [];
|
||||
|
||||
if (largeCoverUrls.length < 2) {
|
||||
memoizedCovers.value = {
|
||||
large: [
|
||||
`/assets/default/cover/big_${theme.global.name.value}_collection.png`,
|
||||
`/assets/default/cover/big_${theme.global.name.value}_collection.png`,
|
||||
],
|
||||
small: [
|
||||
`/assets/default/cover/small_${theme.global.name.value}_collection.png`,
|
||||
`/assets/default/cover/small_${theme.global.name.value}_collection.png`,
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
const shuffledLarge = [...largeCoverUrls].sort(() => Math.random() - 0.5);
|
||||
const shuffledSmall = [...smallCoverUrls].sort(() => Math.random() - 0.5);
|
||||
|
||||
memoizedCovers.value = {
|
||||
large: [shuffledLarge[0], shuffledLarge[1]],
|
||||
small: [shuffledSmall[0], shuffledSmall[1]],
|
||||
};
|
||||
});
|
||||
|
||||
// Computed properties now use the memoized values
|
||||
const firstCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstSmallCover = computed(() => memoizedCovers.value.small[0]);
|
||||
const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-avatar :rounded="0" :size="size">
|
||||
<div class="image-container" :style="{ aspectRatio: 1 / 1 }">
|
||||
<div class="split-image first-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="firstCover"
|
||||
:lazy-src="firstSmallCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="split-image second-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="secondCover"
|
||||
:lazy-src="secondSmallCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</v-avatar>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.image-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.split-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.first-image {
|
||||
clip-path: polygon(0 0, 100% 0, 0% 100%, 0 100%);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.second-image {
|
||||
clip-path: polygon(0% 100%, 100% 0, 100% 100%);
|
||||
z-index: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -54,10 +54,10 @@ async function switchFromFavourites() {
|
||||
});
|
||||
}
|
||||
if (!collectionsStore.isFav(props.rom)) {
|
||||
favCollection.value?.roms.push(props.rom.id);
|
||||
favCollection.value?.rom_ids.push(props.rom.id);
|
||||
} else {
|
||||
if (favCollection.value) {
|
||||
favCollection.value.roms = favCollection.value.roms.filter(
|
||||
favCollection.value.rom_ids = favCollection.value.rom_ids.filter(
|
||||
(id) => id !== props.rom.id,
|
||||
);
|
||||
if (romsStore.currentCollection?.name.toLowerCase() == "favourites") {
|
||||
|
||||
@@ -74,11 +74,10 @@ const computedAspectRatio = computed(() => {
|
||||
galleryViewStore.defaultAspectRatioCover;
|
||||
return parseFloat(ratio.toString());
|
||||
});
|
||||
const unmatchedCoverImage = computed(() =>
|
||||
getUnmatchedCoverImage(props.rom.name || props.rom.slug || ""),
|
||||
);
|
||||
const missingCoverImage = computed(() =>
|
||||
getMissingCoverImage(props.rom.name || props.rom.slug || ""),
|
||||
const fallbackCoverImage = computed(() =>
|
||||
props.rom.igdb_id || props.rom.moby_id || props.rom.ss_id
|
||||
? getMissingCoverImage(props.rom.name || props.rom.slug || "")
|
||||
: getUnmatchedCoverImage(props.rom.name || props.rom.slug || ""),
|
||||
);
|
||||
</script>
|
||||
|
||||
@@ -125,31 +124,20 @@ const missingCoverImage = computed(() =>
|
||||
:src="
|
||||
src ||
|
||||
(romsStore.isSimpleRom(rom)
|
||||
? !rom.igdb_id && !rom.moby_id && !rom.ss_id && !rom.has_cover
|
||||
? unmatchedCoverImage
|
||||
: (rom.igdb_id || rom.moby_id || rom.ss_id) && !rom.has_cover
|
||||
? missingCoverImage
|
||||
: `/assets/romm/resources/${rom.path_cover_l}?ts=${rom.updated_at}`
|
||||
: !rom.igdb_url_cover &&
|
||||
!rom.moby_url_cover &&
|
||||
!rom.ss_url_cover
|
||||
? missingCoverImage
|
||||
: rom.igdb_url_cover ||
|
||||
rom.moby_url_cover ||
|
||||
rom.ss_url_cover)
|
||||
? rom.path_cover_large || fallbackCoverImage
|
||||
: rom.igdb_url_cover ||
|
||||
rom.moby_url_cover ||
|
||||
rom.ss_url_cover ||
|
||||
fallbackCoverImage)
|
||||
"
|
||||
:lazy-src="
|
||||
romsStore.isSimpleRom(rom)
|
||||
? !rom.igdb_id && !rom.moby_id && !rom.ss_id && !rom.has_cover
|
||||
? unmatchedCoverImage
|
||||
: (rom.igdb_id || rom.moby_id || rom.ss_id) && !rom.has_cover
|
||||
? missingCoverImage
|
||||
: `/assets/romm/resources/${rom.path_cover_s}?ts=${rom.updated_at}`
|
||||
: !rom.igdb_url_cover &&
|
||||
!rom.moby_url_cover &&
|
||||
!rom.ss_url_cover
|
||||
? missingCoverImage
|
||||
: rom.igdb_url_cover || rom.moby_url_cover || rom.ss_url_cover
|
||||
src ||
|
||||
(romsStore.isSimpleRom(rom)
|
||||
? rom.path_cover_small || fallbackCoverImage
|
||||
: rom.igdb_url_cover ||
|
||||
rom.moby_url_cover ||
|
||||
rom.ss_url_cover ||
|
||||
fallbackCoverImage)
|
||||
"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
>
|
||||
@@ -159,7 +147,7 @@ const missingCoverImage = computed(() =>
|
||||
<div
|
||||
v-if="
|
||||
isHovering ||
|
||||
(romsStore.isSimpleRom(rom) && !rom.has_cover) ||
|
||||
(romsStore.isSimpleRom(rom) && rom.is_unidentified) ||
|
||||
(!romsStore.isSimpleRom(rom) &&
|
||||
!rom.igdb_url_cover &&
|
||||
!rom.moby_url_cover &&
|
||||
@@ -223,7 +211,7 @@ const missingCoverImage = computed(() =>
|
||||
</div>
|
||||
<template #error>
|
||||
<v-img
|
||||
:src="missingCoverImage"
|
||||
:src="fallbackCoverImage"
|
||||
cover
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
></v-img>
|
||||
|
||||
@@ -47,10 +47,10 @@ async function switchFromFavourites() {
|
||||
});
|
||||
}
|
||||
if (!collectionsStore.isFav(props.rom)) {
|
||||
favCollection.value?.roms.push(props.rom.id);
|
||||
favCollection.value?.rom_ids.push(props.rom.id);
|
||||
} else {
|
||||
if (favCollection.value) {
|
||||
favCollection.value.roms = favCollection.value.roms.filter(
|
||||
favCollection.value.rom_ids = favCollection.value.rom_ids.filter(
|
||||
(id) => id !== props.rom.id,
|
||||
);
|
||||
if (romsStore.currentCollection?.name.toLowerCase() == "favourites") {
|
||||
|
||||
@@ -7,27 +7,18 @@ const props = withDefaults(defineProps<{ rom: SimpleRom; size?: number }>(), {
|
||||
size: 45,
|
||||
});
|
||||
|
||||
const unmatchedCoverImage = computed(() =>
|
||||
getUnmatchedCoverImage(props.rom.name || props.rom.fs_name),
|
||||
);
|
||||
const missingCoverImage = computed(() =>
|
||||
getMissingCoverImage(props.rom.name || props.rom.fs_name),
|
||||
const fallbackCoverImage = computed(() =>
|
||||
props.rom.igdb_id || props.rom.moby_id
|
||||
? getMissingCoverImage(props.rom.name || props.rom.fs_name)
|
||||
: getUnmatchedCoverImage(props.rom.name || props.rom.fs_name),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-avatar :width="size" rounded="0">
|
||||
<v-img
|
||||
:src="
|
||||
!rom.igdb_id && !rom.moby_id && !rom.has_cover
|
||||
? unmatchedCoverImage
|
||||
: rom.has_cover
|
||||
? `/assets/romm/resources/${rom.path_cover_s}?ts=${rom.updated_at}`
|
||||
: missingCoverImage
|
||||
"
|
||||
>
|
||||
<v-img :src="props.rom.path_cover_small || fallbackCoverImage">
|
||||
<template #error>
|
||||
<v-img :src="missingCoverImage" />
|
||||
<v-img :src="fallbackCoverImage" />
|
||||
</template>
|
||||
</v-img>
|
||||
</v-avatar>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import CollectionListItem from "@/components/common/Collection/ListItem.vue";
|
||||
import VirtualCollectionListItem from "@/components/common/Collection/Virtual/ListItem.vue";
|
||||
import storeCollections from "@/stores/collections";
|
||||
import CreateCollectionDialog from "@/components/common/Collection/Dialog/CreateCollection.vue";
|
||||
import storeNavigation from "@/stores/navigation";
|
||||
@@ -9,16 +10,24 @@ import { storeToRefs } from "pinia";
|
||||
import { inject } from "vue";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { isNull } from "lodash";
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
const navigationStore = storeNavigation();
|
||||
const { smAndDown } = useDisplay();
|
||||
const collectionsStore = storeCollections();
|
||||
const { filteredCollections, searchText } = storeToRefs(collectionsStore);
|
||||
const { filteredCollections, filteredVirtualCollections, searchText } =
|
||||
storeToRefs(collectionsStore);
|
||||
const { activeCollectionsDrawer } = storeToRefs(navigationStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
|
||||
const showVirtualCollections = isNull(
|
||||
localStorage.getItem("settings.showVirtualCollections"),
|
||||
)
|
||||
? true
|
||||
: localStorage.getItem("settings.showVirtualCollections") === "true";
|
||||
|
||||
async function addCollection() {
|
||||
emitter?.emit("showCreateCollectionDialog", null);
|
||||
}
|
||||
@@ -65,6 +74,16 @@ function clear() {
|
||||
:collection="collection"
|
||||
with-link
|
||||
/>
|
||||
<template v-if="showVirtualCollections">
|
||||
<v-list-subheader class="uppercase">{{
|
||||
t("common.virtual-collections").toUpperCase()
|
||||
}}</v-list-subheader>
|
||||
<virtual-collection-list-item
|
||||
v-for="collection in filteredVirtualCollections"
|
||||
:collection="collection"
|
||||
with-link
|
||||
/>
|
||||
</template>
|
||||
</v-list>
|
||||
<template #append>
|
||||
<v-btn
|
||||
|
||||
@@ -18,7 +18,8 @@ import storeNavigation from "@/stores/navigation";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, onBeforeMount } from "vue";
|
||||
import { inject, onBeforeMount, ref } from "vue";
|
||||
import { isNull } from "lodash";
|
||||
|
||||
// Props
|
||||
const navigationStore = storeNavigation();
|
||||
@@ -30,6 +31,21 @@ emitter?.on("refreshDrawer", async () => {
|
||||
platformsStore.set(platformData);
|
||||
});
|
||||
|
||||
const showVirtualCollections = isNull(
|
||||
localStorage.getItem("settings.showVirtualCollections"),
|
||||
)
|
||||
? true
|
||||
: localStorage.getItem("settings.showVirtualCollections") === "true";
|
||||
|
||||
const storedVirtualCollectionType = localStorage.getItem(
|
||||
"settings.virtualCollectionType",
|
||||
);
|
||||
const virtualCollectionTypeRef = ref(
|
||||
isNull(storedVirtualCollectionType)
|
||||
? "collection"
|
||||
: storedVirtualCollectionType,
|
||||
);
|
||||
|
||||
// Functions
|
||||
onBeforeMount(async () => {
|
||||
await platformApi
|
||||
@@ -40,6 +56,7 @@ onBeforeMount(async () => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
await collectionApi
|
||||
.getCollections()
|
||||
.then(({ data: collections }) => {
|
||||
@@ -53,6 +70,15 @@ onBeforeMount(async () => {
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
if (showVirtualCollections) {
|
||||
await collectionApi
|
||||
.getVirtualCollections({ type: virtualCollectionTypeRef.value })
|
||||
.then(({ data: virtualCollections }) => {
|
||||
collectionsStore.setVirtual(virtualCollections);
|
||||
});
|
||||
}
|
||||
|
||||
navigationStore.resetDrawers();
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Spiel | {n} Spiele",
|
||||
"collection": "Sammlung",
|
||||
"collections": "Sammlungen",
|
||||
"virtual-collection": "Autogenerierte Sammlung",
|
||||
"virtual-collections": "Autogenerierte Sammlungen",
|
||||
"save": "Speichern",
|
||||
"saves": "Speicherstände",
|
||||
"saves-n": "{n} Speichern | {n} Speicherstände",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Zu Favoriten hinzufügen",
|
||||
"remove-from-fav": "Aus Favoriten entfernen",
|
||||
"add-to-collection": "Zu Sammlung hinzufügen",
|
||||
"remove-from-collection": "Aus Sammlung entfernen",
|
||||
"adding-to-collection-part1": "Füge",
|
||||
"adding-to-collection-part2": "Roms zu Sammlung hinzu",
|
||||
"remove-from-collection": "Aus Sammlung entfernen",
|
||||
"remove-from-collection-part1": "Entferne",
|
||||
"remove-from-collection-part2": "Roms aus Sammlung",
|
||||
"removing-from-collection-part1": "Entferne",
|
||||
"removing-from-collection-part2": "Roms aus Sammlung",
|
||||
"delete-rom": "Löschen",
|
||||
"copy-link": "Download-Link kopieren",
|
||||
"cant-copy-link": "Link kann nicht in Zwischenablage kopiert werden. Bitte manuell kopieren.",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "Sammlungen werden auf der Startseite dargestellt",
|
||||
"show-collections-as-grid": "Sammlungen als Raster darstellen",
|
||||
"show-collections-as-grid-desc": "Sammlungen werden als Raster auf der Startseite gezeigt",
|
||||
"show-virtual-collections": "Zeige automatisch generierte Sammlungen",
|
||||
"show-virtual-collections-desc": "Wird auf der Startseite und in der Sammlungs-Seitenleiste angezeigt",
|
||||
"virtual-collection-type": "Typ der automatisch generierten Sammlung (ROM-Gruppierungsmethode)",
|
||||
"group-roms": "Roms gruppieren",
|
||||
"group-roms-desc": "Gruppiere verschiedene Versionen des gleichen Roms in der Galerie",
|
||||
"show-siblings": "Zeige Anzahl der Versionen",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Game | {n} Games",
|
||||
"collection": "Collection",
|
||||
"collections": "Collections",
|
||||
"virtual-collection": "Autogenerated collection",
|
||||
"virtual-collections": "Autogenerated collections",
|
||||
"save": "Save",
|
||||
"saves": "Saves",
|
||||
"saves-n": "{n} Save | {n} Saves",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Add to favourites",
|
||||
"remove-from-fav": "Remove from favourites",
|
||||
"add-to-collection": "Add to collection",
|
||||
"remove-from-collection": "Remove from collection",
|
||||
"adding-to-collection-part1": "Adding",
|
||||
"adding-to-collection-part2": "roms to collection",
|
||||
"remove-from-collection": "Remove from collection",
|
||||
"remove-from-collection-part1": "Removing",
|
||||
"remove-from-collection-part2": "roms from collection",
|
||||
"removing-from-collection-part1": "Removing",
|
||||
"removing-from-collection-part2": "roms from collection",
|
||||
"delete-rom": "Delete",
|
||||
"copy-link": "Copy download link",
|
||||
"cant-copy-link": "Can't copy link to clipboard, copy it manually",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "Show collections section at the home page",
|
||||
"show-collections-as-grid": "Collections as grid",
|
||||
"show-collections-as-grid-desc": "View collection cards as a grid at the home page",
|
||||
"show-virtual-collections": "Show autogenerated collections",
|
||||
"show-virtual-collections-desc": "Displayed in the homepage and collections sidebar.",
|
||||
"virtual-collection-type": "Virtual collection type (ROM grouping method)",
|
||||
"group-roms": "Group roms",
|
||||
"group-roms-desc": "Group versions of the same rom together in the gallery",
|
||||
"show-siblings": "Show siblings",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Game | {n} Games",
|
||||
"collection": "Collection",
|
||||
"collections": "Collections",
|
||||
"virtual-collection": "Autogenerated collection",
|
||||
"virtual-collections": "Autogenerated collections",
|
||||
"save": "Save",
|
||||
"saves": "Saves",
|
||||
"saves-n": "{n} Save | {n} Saves",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Add to favourites",
|
||||
"remove-from-fav": "Remove from favourites",
|
||||
"add-to-collection": "Add to collection",
|
||||
"remove-from-collection": "Remove from collection",
|
||||
"adding-to-collection-part1": "Adding",
|
||||
"adding-to-collection-part2": "roms to collection",
|
||||
"remove-from-collection": "Remove from collection",
|
||||
"remove-from-collection-part1": "Removing",
|
||||
"remove-from-collection-part2": "roms from collection",
|
||||
"removing-from-collection-part1": "Removing",
|
||||
"removing-from-collection-part2": "roms from collection",
|
||||
"delete-rom": "Delete",
|
||||
"copy-link": "Copy download link",
|
||||
"cant-copy-link": "Can't copy link to clipboard, copy it manually",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "Show collections section at the home page",
|
||||
"show-collections-as-grid": "Collections as grid",
|
||||
"show-collections-as-grid-desc": "View collection cards as a grid at the home page",
|
||||
"show-virtual-collections": "Show autogenerated collections",
|
||||
"show-virtual-collections-desc": "Displayed in the homepage and collections sidebar.",
|
||||
"virtual-collection-type": "Virtual collection type (ROM grouping method)",
|
||||
"group-roms": "Group roms",
|
||||
"group-roms-desc": "Group versions of the same rom together in the gallery",
|
||||
"show-siblings": "Show siblings",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Juego | {n} Juegos",
|
||||
"collection": "Colección",
|
||||
"collections": "Colecciones",
|
||||
"virtual-collection": "Autogenerada colección",
|
||||
"virtual-collections": "Autogeneradas colecciones",
|
||||
"save": "Guardado",
|
||||
"saves": "Guardados",
|
||||
"saves-n": "{n} Guardado | {n} Guardados",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Añadir a favoritos",
|
||||
"remove-from-fav": "Eliminar de favoritos",
|
||||
"add-to-collection": "Añadir a la colección",
|
||||
"remove-from-collection": "Eliminar de la colección",
|
||||
"adding-to-collection-part1": "Añadiendo",
|
||||
"adding-to-collection-part2": "roms a la colección",
|
||||
"remove-from-collection": "Quitar de la collección",
|
||||
"remove-from-collection-part1": "Quitando",
|
||||
"remove-from-collection-part2": "roms de la colección",
|
||||
"removing-from-collection-part1": "Quitando",
|
||||
"removing-from-collection-part2": "roms de la colección",
|
||||
"delete-rom": "Eliminar",
|
||||
"copy-link": "Copiar link de descarga",
|
||||
"cant-copy-link": "No se pudo copiar el link al portapapeles, copialo manualmente",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "Mostrar la sección de colecciones en la página principal",
|
||||
"show-collections-as-grid": "Mostrar colecciones como cuadrícula",
|
||||
"show-collections-as-grid-desc": "Mostrar la sección de colecciones como cuadrícula en la página principal",
|
||||
"show-virtual-collections": "Mostrar colecciones generadas automáticamente",
|
||||
"show-virtual-collections-desc": "Mostrado en la página principal y en la barra lateral de colecciones.",
|
||||
"virtual-collection-type": "Tipo de colección virtual (método de agrupación de ROM)",
|
||||
"group-roms": "Agrupar roms",
|
||||
"group-roms-desc": "Agrupar roms con la misma versión en una única entrada en la galería",
|
||||
"show-siblings": "Mostrar siblings",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Jeu | {n} Jeux",
|
||||
"collection": "Collection",
|
||||
"collections": "Collections",
|
||||
"virtual-collection": "Collection autogénérée",
|
||||
"virtual-collections": "Collections autogénérées",
|
||||
"save": "Sauvegarder",
|
||||
"saves": "Sauvegardes",
|
||||
"saves-n": "{n} Sauvegarder | {n} Sauvegardes",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Ajouter aux favoris",
|
||||
"remove-from-fav": "Retirer des favoris",
|
||||
"add-to-collection": "Ajouter à la collection",
|
||||
"remove-from-collection": "Retirer de la collection",
|
||||
"adding-to-collection-part1": "Ajout de",
|
||||
"adding-to-collection-part2": "roms à la collection",
|
||||
"remove-from-collection": "Retirer de la collection",
|
||||
"remove-from-collection-part1": "Retrait de",
|
||||
"remove-from-collection-part2": "roms de la collection",
|
||||
"removing-from-collection-part1": "Retrait de",
|
||||
"removing-from-collection-part2": "roms de la collection",
|
||||
"delete-rom": "Supprimer",
|
||||
"copy-link": "Copier le lien de téléchargement",
|
||||
"cant-copy-link": "Impossible de copier le lien dans le presse-papiers, copiez-le manuellement",
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
"show-collections-desc": "Afficher la section des collections sur la page d'accueil",
|
||||
"show-collections-as-grid": "Collections en grille",
|
||||
"show-collections-as-grid-desc": "Voir les cartes des collections en grille sur la page d'accueil",
|
||||
"show-virtual-collections": "Afficher les collections générées automatiquement",
|
||||
"show-virtual-collections-desc": "Affiché sur la page d'accueil et dans la barre latérale des collections.",
|
||||
"virtual-collection-type": "Type de collection virtuelle (méthode de regroupement des roms)",
|
||||
|
||||
"group-roms": "Grouper les roms",
|
||||
"group-roms-desc": "Grouper les versions du même rom ensemble dans la galerie",
|
||||
"show-siblings": "Afficher les frères et sœurs",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "게임 {n}개 | 게임 {n}개",
|
||||
"collection": "모음집",
|
||||
"collections": "모음집",
|
||||
"virtual-collection": "자동 생성된 모음집",
|
||||
"virtual-collections": "자동 생성된 모음집",
|
||||
"save": "세이브",
|
||||
"saves": "세이브",
|
||||
"saves-n": "세이브 {n}개 | 세이브 {n}개",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "즐겨찾기에 추가",
|
||||
"remove-from-fav": "즐겨찾기에서 제거",
|
||||
"add-to-collection": "모음집에 추가",
|
||||
"remove-from-collection": "모음집에서 제거",
|
||||
"adding-to-collection-part1": "",
|
||||
"adding-to-collection-part2": "개의 롬을 모음집에 추가합니다",
|
||||
"remove-from-collection": "모음집에서 제거",
|
||||
"remove-from-collection-part1": "",
|
||||
"remove-from-collection-part2": "개의 롬을 모음집에서 제거합니다",
|
||||
"removing-from-collection-part1": "",
|
||||
"removing-from-collection-part2": "개의 롬을 모음집에서 제거합니다",
|
||||
"delete-rom": "삭제",
|
||||
"copy-link": "다운로드 링크 복사",
|
||||
"cant-copy-link": "링크를 클립보드에 복사할 수 없습니다. 수동으로 복사합니다",
|
||||
|
||||
@@ -22,6 +22,9 @@
|
||||
"show-collections-desc": "홈페이지에 모음집 페이지를 보여줍니다",
|
||||
"show-collections-as-grid": "모음집을 그리드 뷰로",
|
||||
"show-collections-as-grid-desc": "홈페이지에 모음집들을 그리드 뷰로 보여줍니다",
|
||||
"show-virtual-collections": "자동 생성된 모음집 보이기",
|
||||
"show-virtual-collections-desc": "홈페이지와 모음집 사이드바에 표시됩니다",
|
||||
"virtual-collection-type": "자동 생성된 모음집 유형(롬 그룹화 방법)",
|
||||
"group-roms": "롬 묶음",
|
||||
"group-roms-desc": "갤러리에서 같은 롬의 다른 버전들을 하나로 묶습니다",
|
||||
"show-siblings": "파생롬 보이기",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Jogo | {n} Jogos",
|
||||
"collection": "Coleção",
|
||||
"collections": "Coleções",
|
||||
"virtual-collection": "Coleção autogerada",
|
||||
"virtual-collections": "Coleções autogeradas",
|
||||
"save": "Salvar",
|
||||
"saves": "Salvar",
|
||||
"saves-n": "{n} Salvar | {n} Salvar",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Adicionar aos favoritos",
|
||||
"remove-from-fav": "Remover dos favoritos",
|
||||
"add-to-collection": "Adicionar à coleção",
|
||||
"remove-from-collection": "Remover da coleção",
|
||||
"adding-to-collection-part1": "Adicionando",
|
||||
"adding-to-collection-part2": "roms à coleção",
|
||||
"remove-from-collection": "Remover da coleção",
|
||||
"remove-from-collection-part1": "Removendo",
|
||||
"remove-from-collection-part2": "roms da coleção",
|
||||
"removing-from-collection-part1": "Removendo",
|
||||
"removing-from-collection-part2": "roms da coleção",
|
||||
"delete-rom": "Excluir",
|
||||
"copy-link": "Copiar link de download",
|
||||
"cant-copy-link": "Não é possível copiar o link para a área de transferência, copie manualmente",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "Mostrar seção de coleções na página inicial",
|
||||
"show-collections-as-grid": "Coleções como grade",
|
||||
"show-collections-as-grid-desc": "Ver cartões de coleções como uma grade na página inicial",
|
||||
"show-virtual-collections": "Mostrar coleções geradas automaticamente",
|
||||
"show-virtual-collections-desc": "Exibido na página inicial e na barra lateral de coleções.",
|
||||
"virtual-collection-type": "Tipo de coleção virtual (método de agrupamento de ROM)",
|
||||
"group-roms": "Agrupar roms",
|
||||
"group-roms-desc": "Agrupar versões do mesmo rom juntos na galeria",
|
||||
"show-siblings": "Mostrar irmãos",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} Игра | {n} Игры",
|
||||
"collection": "Коллекция",
|
||||
"collections": "Коллекции",
|
||||
"virtual-collection": "Автосгенерированная коллекция",
|
||||
"virtual-collections": "Автосгенерированные коллекции",
|
||||
"save": "Сохранить",
|
||||
"saves": "Сохранения",
|
||||
"saves-n": "{n} Сохранить | {n} Сохранения",
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"add-to-fav": "Добавить в избранное",
|
||||
"remove-from-fav": "Удалить из избранного",
|
||||
"add-to-collection": "Добавить в коллекцию",
|
||||
"remove-from-collection": "Удалить из коллекции",
|
||||
"adding-to-collection-part1": "Добавление",
|
||||
"adding-to-collection-part2": "ромов в коллекцию",
|
||||
"remove-from-collection": "Удалить из коллекции",
|
||||
"remove-from-collection-part1": "Удаление",
|
||||
"remove-from-collection-part2": "ромов из коллекции",
|
||||
"removing-from-collection-part1": "Удаление",
|
||||
"removing-from-collection-part2": "ромов из коллекции",
|
||||
"delete-rom": "Удалить",
|
||||
"copy-link": "Скопировать ссылку для скачивания",
|
||||
"cant-copy-link": "Не удается скопировать ссылку в буфер обмена, скопируйте ее вручную",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "Показать раздел коллекций на главной странице",
|
||||
"show-collections-as-grid": "Коллекции в виде сетки",
|
||||
"show-collections-as-grid-desc": "Просмотр карточек коллекций в виде сетки на главной странице",
|
||||
"show-virtual-collections": "Показать автоматически созданные коллекции",
|
||||
"show-virtual-collections-desc": "Отображается на главной странице и в боковой панели коллекций.",
|
||||
"virtual-collection-type": "Тип виртуальной коллекции (метод группировки ромов)",
|
||||
"group-roms": "Группировать ромы",
|
||||
"group-roms-desc": "Группировать версии одного и того же рома вместе в галерее",
|
||||
"show-siblings": "Показать родственные",
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
"games-n": "{n} 游戏 | {n} 游戏",
|
||||
"collection": "收藏",
|
||||
"collections": "收藏",
|
||||
"virtual-collection": "自动生成的收藏",
|
||||
"virtual-collections": "自动生成的收藏",
|
||||
"save": "存档",
|
||||
"saves": "存档",
|
||||
"saves-n": "{n} 存档 | {n} 存档",
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
"add-to-fav": "添加至喜好",
|
||||
"remove-from-fav": "从喜好中移除",
|
||||
"add-to-collection": "添加至收藏",
|
||||
"remove-from-collection": "从收藏中移除",
|
||||
"adding-to-collection-part1": "添加",
|
||||
"adding-to-collection-part2": "Roms 至收藏",
|
||||
"remove-from-collection": "从收藏中移除",
|
||||
"remove-from-collection-part1": "从收藏中移除",
|
||||
"remove-from-collection-part2": "Roms",
|
||||
"removing-from-collection-part1": "从收藏中移除",
|
||||
"removing-from-collection-part2": "Roms",
|
||||
"delete-rom": "删除",
|
||||
"copy-link": "复制下载链接",
|
||||
"cant-copy-link": "无法将链接复制到剪贴板,请手动复制",
|
||||
|
||||
@@ -26,6 +26,9 @@
|
||||
"show-collections-desc": "在主页上显示收藏部分",
|
||||
"show-collections-as-grid": "收藏(网格)",
|
||||
"show-collections-as-grid-desc": "以网格形式在主页上显示收藏(卡片)部分",
|
||||
"show-virtual-collections": "显示自动生成的收藏",
|
||||
"show-virtual-collections-desc": "在主页和收藏侧边栏中显示",
|
||||
"virtual-collection-type": "虚拟收藏类型(Rom 文件分组方法)",
|
||||
"group-roms": "Rom 文件组",
|
||||
"group-roms-desc": "对游戏库中相同 Rom 文件的不同版本进行分组",
|
||||
"show-siblings": "显示版本数量",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { MessageResponse } from "@/__generated__";
|
||||
import api from "@/services/api/index";
|
||||
import type { Collection } from "@/stores/collections";
|
||||
import type { Collection, VirtualCollection } from "@/stores/collections";
|
||||
|
||||
export type UpdatedCollection = Collection & {
|
||||
artwork?: File;
|
||||
@@ -18,7 +18,7 @@ async function createCollection({
|
||||
formData.append("name", collection.name || "");
|
||||
formData.append("description", collection.description || "");
|
||||
formData.append("url_cover", collection.url_cover || "");
|
||||
formData.append("roms", JSON.stringify(collection.roms));
|
||||
formData.append("rom_ids", JSON.stringify(collection.rom_ids));
|
||||
if (collection.artwork) formData.append("artwork", collection.artwork);
|
||||
return api.post(`/collections`, formData);
|
||||
}
|
||||
@@ -27,12 +27,26 @@ async function getCollections(): Promise<{ data: Collection[] }> {
|
||||
return api.get("/collections");
|
||||
}
|
||||
|
||||
async function getCollection(
|
||||
id: number | undefined,
|
||||
): Promise<{ data: Collection }> {
|
||||
async function getVirtualCollections({
|
||||
type = "collection",
|
||||
limit = 100,
|
||||
}: {
|
||||
type?: string;
|
||||
limit?: number;
|
||||
}): Promise<{ data: VirtualCollection[] }> {
|
||||
return api.get("/collections/virtual", { params: { type, limit } });
|
||||
}
|
||||
|
||||
async function getCollection(id: number): Promise<{ data: Collection }> {
|
||||
return api.get(`/collections/${id}`);
|
||||
}
|
||||
|
||||
async function getVirtualCollection(
|
||||
id: string,
|
||||
): Promise<{ data: VirtualCollection }> {
|
||||
return api.get(`/collections/virtual/${id}`);
|
||||
}
|
||||
|
||||
async function updateCollection({
|
||||
collection,
|
||||
removeCover = false,
|
||||
@@ -44,7 +58,7 @@ async function updateCollection({
|
||||
formData.append("name", collection.name || "");
|
||||
formData.append("description", collection.description || "");
|
||||
formData.append("url_cover", collection.url_cover || "");
|
||||
formData.append("roms", JSON.stringify(collection.roms));
|
||||
formData.append("rom_ids", JSON.stringify(collection.rom_ids));
|
||||
if (collection.artwork) formData.append("artwork", collection.artwork);
|
||||
return api.put(`/collections/${collection.id}`, formData, {
|
||||
params: { is_public: collection.is_public, remove_cover: removeCover },
|
||||
@@ -62,7 +76,9 @@ async function deleteCollection({
|
||||
export default {
|
||||
createCollection,
|
||||
getCollections,
|
||||
getVirtualCollections,
|
||||
getCollection,
|
||||
getVirtualCollection,
|
||||
updateCollection,
|
||||
deleteCollection,
|
||||
};
|
||||
|
||||
@@ -58,12 +58,14 @@ async function uploadRoms({
|
||||
async function getRoms({
|
||||
platformId = null,
|
||||
collectionId = null,
|
||||
virtualCollectionId = null,
|
||||
searchTerm = "",
|
||||
orderBy = "name",
|
||||
orderDir = "asc",
|
||||
}: {
|
||||
platformId?: number | null;
|
||||
collectionId?: number | null;
|
||||
virtualCollectionId?: string | null;
|
||||
searchTerm?: string | null;
|
||||
orderBy?: string | null;
|
||||
orderDir?: string | null;
|
||||
@@ -72,6 +74,7 @@ async function getRoms({
|
||||
params: {
|
||||
platform_id: platformId,
|
||||
collection_id: collectionId,
|
||||
virtual_collection_id: virtualCollectionId,
|
||||
search_term: searchTerm,
|
||||
order_by: orderBy,
|
||||
order_dir: orderDir,
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import type { CollectionSchema } from "@/__generated__";
|
||||
import type {
|
||||
CollectionSchema,
|
||||
VirtualCollectionSchema,
|
||||
} from "@/__generated__";
|
||||
import { uniqBy } from "lodash";
|
||||
import { defineStore } from "pinia";
|
||||
import type { SimpleRom } from "./roms";
|
||||
|
||||
export type Collection = CollectionSchema;
|
||||
export type VirtualCollection = VirtualCollectionSchema;
|
||||
|
||||
export default defineStore("collections", {
|
||||
state: () => {
|
||||
return {
|
||||
allCollections: [] as Collection[],
|
||||
virtualCollections: [] as VirtualCollection[],
|
||||
favCollection: {} as Collection | undefined,
|
||||
searchText: "" as string,
|
||||
};
|
||||
@@ -18,6 +23,10 @@ export default defineStore("collections", {
|
||||
allCollections.filter((p) =>
|
||||
p.name.toLowerCase().includes(searchText.toLowerCase()),
|
||||
),
|
||||
filteredVirtualCollections: ({ virtualCollections, searchText }) =>
|
||||
virtualCollections.filter((p) =>
|
||||
p.name.toLowerCase().includes(searchText.toLowerCase()),
|
||||
),
|
||||
},
|
||||
actions: {
|
||||
_reorder() {
|
||||
@@ -32,6 +41,9 @@ export default defineStore("collections", {
|
||||
set(collections: Collection[]) {
|
||||
this.allCollections = collections;
|
||||
},
|
||||
setVirtual(collections: VirtualCollection[]) {
|
||||
this.virtualCollections = collections;
|
||||
},
|
||||
add(collection: Collection) {
|
||||
this.allCollections.push(collection);
|
||||
this._reorder();
|
||||
@@ -56,7 +68,7 @@ export default defineStore("collections", {
|
||||
return this.allCollections.find((p) => p.id === collectionId);
|
||||
},
|
||||
isFav(rom: SimpleRom) {
|
||||
return this.favCollection?.roms?.includes(rom.id);
|
||||
return this.favCollection?.rom_ids?.includes(rom.id);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ export type Platform = PlatformSchema;
|
||||
const filters = [
|
||||
"genres",
|
||||
"franchises",
|
||||
"collections",
|
||||
"meta_collections",
|
||||
"companies",
|
||||
"age_ratings",
|
||||
"status",
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { SearchRomSchema } from "@/__generated__";
|
||||
import type { DetailedRomSchema, SimpleRomSchema } from "@/__generated__/";
|
||||
import storeCollection, { type Collection } from "@/stores/collections";
|
||||
import storeCollection, {
|
||||
type Collection,
|
||||
type VirtualCollection,
|
||||
} from "@/stores/collections";
|
||||
import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import { type Platform } from "@/stores/platforms";
|
||||
import type { ExtractPiniaStoreType } from "@/types";
|
||||
@@ -18,6 +21,7 @@ export default defineStore("roms", {
|
||||
state: () => ({
|
||||
currentPlatform: null as Platform | null,
|
||||
currentCollection: null as Collection | null,
|
||||
currentVirtualCollection: null as VirtualCollection | null,
|
||||
currentRom: null as DetailedRom | null,
|
||||
allRoms: [] as SimpleRom[],
|
||||
_grouped: [] as SimpleRom[],
|
||||
@@ -98,6 +102,9 @@ export default defineStore("roms", {
|
||||
setCurrentCollection(collection: Collection | null) {
|
||||
this.currentCollection = collection;
|
||||
},
|
||||
setCurrentVirtualCollection(collection: VirtualCollection | null) {
|
||||
this.currentVirtualCollection = collection;
|
||||
},
|
||||
set(roms: SimpleRom[]) {
|
||||
this.allRoms = roms;
|
||||
this._reorder();
|
||||
@@ -233,7 +240,7 @@ export default defineStore("roms", {
|
||||
const byFavourites = new Set(
|
||||
this.filteredRoms
|
||||
.filter((rom) =>
|
||||
collectionStore.favCollection?.roms?.includes(rom.id),
|
||||
collectionStore.favCollection?.rom_ids?.includes(rom.id),
|
||||
)
|
||||
.map((roms) => roms.id),
|
||||
);
|
||||
@@ -277,7 +284,7 @@ export default defineStore("roms", {
|
||||
const byCollection = new Set(
|
||||
this.filteredRoms
|
||||
.filter((rom) =>
|
||||
rom.collections.some(
|
||||
rom.meta_collections.some(
|
||||
(collection) => collection === collectionToFilter,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -18,16 +18,14 @@ import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { onBeforeRouteUpdate, useRoute, useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
|
||||
// Props
|
||||
const { smAndDown } = useDisplay();
|
||||
const route = useRoute();
|
||||
const galleryViewStore = storeGalleryView();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { scrolledToTop, currentView } = storeToRefs(galleryViewStore);
|
||||
const collectionsStore = storeCollections();
|
||||
const { allCollections } = storeToRefs(collectionsStore);
|
||||
const { allCollections, virtualCollections } = storeToRefs(collectionsStore);
|
||||
const romsStore = storeRoms();
|
||||
const {
|
||||
allRoms,
|
||||
@@ -35,6 +33,7 @@ const {
|
||||
selectedRoms,
|
||||
currentPlatform,
|
||||
currentCollection,
|
||||
currentVirtualCollection,
|
||||
itemsPerBatch,
|
||||
gettingRoms,
|
||||
} = storeToRefs(romsStore);
|
||||
@@ -58,6 +57,7 @@ async function fetchRoms() {
|
||||
try {
|
||||
const { data } = await romApi.getRoms({
|
||||
collectionId: romsStore.currentCollection?.id,
|
||||
virtualCollectionId: romsStore.currentVirtualCollection?.id,
|
||||
});
|
||||
romsStore.set(data);
|
||||
romsStore.setFiltered(data, galleryFilterStore);
|
||||
@@ -112,7 +112,7 @@ function setFilters() {
|
||||
galleryFilterStore.setFilterCollections([
|
||||
...new Set(
|
||||
romsStore.filteredRoms
|
||||
.flatMap((rom) => rom.collections.map((collection) => collection))
|
||||
.flatMap((rom) => rom.meta_collections.map((collection) => collection))
|
||||
.sort(),
|
||||
),
|
||||
]);
|
||||
@@ -215,11 +215,44 @@ function resetGallery() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const routeCollectionId = Number(route.params.collection);
|
||||
const routeCollectionId = route.params.collection;
|
||||
currentPlatform.value = null;
|
||||
|
||||
watch(
|
||||
() => allCollections.value,
|
||||
async (collections) => {
|
||||
if (
|
||||
collections.length > 0 &&
|
||||
collections.some(
|
||||
(collection) => collection.id === Number(routeCollectionId),
|
||||
)
|
||||
) {
|
||||
const collection = collections.find(
|
||||
(collection) => collection.id === Number(routeCollectionId),
|
||||
);
|
||||
|
||||
// Check if the current platform is different or no ROMs have been loaded
|
||||
if (
|
||||
(currentVirtualCollection.value?.id !== routeCollectionId ||
|
||||
allRoms.value.length === 0) &&
|
||||
collection
|
||||
) {
|
||||
romsStore.setCurrentCollection(collection);
|
||||
romsStore.setCurrentVirtualCollection(null);
|
||||
resetGallery();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
|
||||
window.addEventListener("wheel", onScroll);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
}
|
||||
},
|
||||
{ immediate: true }, // Ensure watcher is triggered immediately
|
||||
);
|
||||
|
||||
watch(
|
||||
() => virtualCollections.value,
|
||||
async (collections) => {
|
||||
if (
|
||||
collections.length > 0 &&
|
||||
@@ -231,11 +264,12 @@ onMounted(async () => {
|
||||
|
||||
// Check if the current platform is different or no ROMs have been loaded
|
||||
if (
|
||||
(currentCollection.value?.id !== routeCollectionId ||
|
||||
(currentVirtualCollection.value?.id !== routeCollectionId ||
|
||||
allRoms.value.length === 0) &&
|
||||
collection
|
||||
) {
|
||||
romsStore.setCurrentCollection(collection);
|
||||
romsStore.setCurrentCollection(null);
|
||||
romsStore.setCurrentVirtualCollection(collection);
|
||||
resetGallery();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
@@ -256,10 +290,34 @@ onBeforeRouteUpdate(async (to, from) => {
|
||||
|
||||
resetGallery();
|
||||
|
||||
const routeCollectionId = Number(to.params.collection);
|
||||
const routeCollectionId = to.params.collection;
|
||||
|
||||
watch(
|
||||
() => allCollections.value,
|
||||
async (collections) => {
|
||||
if (collections.length > 0) {
|
||||
const collection = collections.find(
|
||||
(collection) => collection.id === Number(routeCollectionId),
|
||||
);
|
||||
|
||||
// Only trigger fetchRoms if switching platforms or ROMs are not loaded
|
||||
if (
|
||||
(currentCollection.value?.id !== Number(routeCollectionId) ||
|
||||
allRoms.value.length === 0) &&
|
||||
collection
|
||||
) {
|
||||
romsStore.setCurrentCollection(collection);
|
||||
romsStore.setCurrentVirtualCollection(null);
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true }, // Ensure watcher is triggered immediately
|
||||
);
|
||||
|
||||
watch(
|
||||
() => virtualCollections.value,
|
||||
async (collections) => {
|
||||
if (collections.length > 0) {
|
||||
const collection = collections.find(
|
||||
@@ -268,11 +326,12 @@ onBeforeRouteUpdate(async (to, from) => {
|
||||
|
||||
// Only trigger fetchRoms if switching platforms or ROMs are not loaded
|
||||
if (
|
||||
(currentCollection.value?.id !== routeCollectionId ||
|
||||
(currentVirtualCollection.value?.id !== routeCollectionId ||
|
||||
allRoms.value.length === 0) &&
|
||||
collection
|
||||
) {
|
||||
romsStore.setCurrentCollection(collection);
|
||||
romsStore.setCurrentCollection(null);
|
||||
romsStore.setCurrentVirtualCollection(collection);
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ function setFilters() {
|
||||
galleryFilterStore.setFilterCollections([
|
||||
...new Set(
|
||||
romsStore.filteredRoms
|
||||
.flatMap((rom) => rom.collections.map((collection) => collection))
|
||||
.flatMap((rom) => rom.meta_collections.map((collection) => collection))
|
||||
.sort(),
|
||||
),
|
||||
]);
|
||||
@@ -210,7 +210,7 @@ function resetGallery() {
|
||||
const filterToSetFilter: Record<FilterType, Function> = {
|
||||
genres: galleryFilterStore.setSelectedFilterGenre,
|
||||
franchises: galleryFilterStore.setSelectedFilterFranchise,
|
||||
collections: galleryFilterStore.setSelectedFilterCollection,
|
||||
meta_collections: galleryFilterStore.setSelectedFilterCollection,
|
||||
companies: galleryFilterStore.setSelectedFilterCompany,
|
||||
age_ratings: galleryFilterStore.setSelectedFilterAgeRating,
|
||||
status: galleryFilterStore.setSelectedFilterStatus,
|
||||
|
||||
@@ -67,7 +67,7 @@ function setFilters() {
|
||||
galleryFilterStore.setFilterCollections([
|
||||
...new Set(
|
||||
romsStore.filteredRoms
|
||||
.flatMap((rom) => rom.collections.map((collection) => collection))
|
||||
.flatMap((rom) => rom.meta_collections.map((collection) => collection))
|
||||
.sort(),
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import Collections from "@/components/Home/Collections.vue";
|
||||
import VirtualCollections from "@/components/Home/VirtualCollections.vue";
|
||||
import Platforms from "@/components/Home/Platforms.vue";
|
||||
import RecentSkeletonLoader from "@/components/Home/RecentSkeletonLoader.vue";
|
||||
import RecentAdded from "@/components/Home/RecentAdded.vue";
|
||||
@@ -22,7 +23,7 @@ const { recentRoms, continuePlayingRoms: recentPlayedRoms } =
|
||||
const platforms = storePlatforms();
|
||||
const { filledPlatforms } = storeToRefs(platforms);
|
||||
const collections = storeCollections();
|
||||
const { allCollections } = storeToRefs(collections);
|
||||
const { allCollections, virtualCollections } = storeToRefs(collections);
|
||||
const showRecentRoms = isNull(localStorage.getItem("settings.showRecentRoms"))
|
||||
? true
|
||||
: localStorage.getItem("settings.showRecentRoms") === "true";
|
||||
@@ -37,6 +38,11 @@ const showPlatforms = isNull(localStorage.getItem("settings.showPlatforms"))
|
||||
const showCollections = isNull(localStorage.getItem("settings.showCollections"))
|
||||
? true
|
||||
: localStorage.getItem("settings.showCollections") === "true";
|
||||
const showVirtualCollections = isNull(
|
||||
localStorage.getItem("settings.showVirtualCollections"),
|
||||
)
|
||||
? true
|
||||
: localStorage.getItem("settings.showVirtualCollections") === "true";
|
||||
const fetchingRecentAdded = ref(false);
|
||||
const fetchingContinuePlaying = ref(false);
|
||||
|
||||
@@ -93,4 +99,7 @@ onMounted(async () => {
|
||||
/>
|
||||
<platforms v-if="filledPlatforms.length > 0 && showPlatforms" />
|
||||
<collections v-if="allCollections.length > 0 && showCollections" />
|
||||
<virtual-collections
|
||||
v-if="virtualCollections.length > 0 && showVirtualCollections"
|
||||
/>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user