mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge pull request #963 from rommapp/feature/fav_for_siblings
Select main sibling
This commit is contained in:
97
backend/alembic/versions/0021_rom_user.py
Normal file
97
backend/alembic/versions/0021_rom_user.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0021_rom_user
|
||||
Revises: 0020_created_and_updated
|
||||
Create Date: 2024-06-29 00:11:51.800988
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0021_rom_user"
|
||||
down_revision = "0020_created_and_updated"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"rom_user",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("note_raw_markdown", sa.Text(), nullable=False),
|
||||
sa.Column("note_is_public", sa.Boolean(), nullable=True),
|
||||
sa.Column("is_main_sibling", sa.Boolean(), nullable=False),
|
||||
sa.Column("rom_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["rom_id"], ["roms.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("rom_id", "user_id", name="unique_rom_user_props"),
|
||||
)
|
||||
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO rom_user (id, updated_at, note_raw_markdown, note_is_public, is_main_sibling, rom_id, user_id)
|
||||
SELECT id, updated_at, raw_markdown, is_public, FALSE, rom_id, user_id
|
||||
FROM rom_notes
|
||||
"""
|
||||
)
|
||||
|
||||
op.drop_table("rom_notes")
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"rom_notes",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("raw_markdown", sa.Text(), nullable=False),
|
||||
sa.Column("is_public", sa.Boolean(), nullable=True),
|
||||
sa.Column("rom_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(["rom_id"], ["roms.id"], ondelete="CASCADE"),
|
||||
sa.ForeignKeyConstraint(["user_id"], ["users.id"], ondelete="CASCADE"),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("rom_id", "user_id", name="unique_rom_user_note"),
|
||||
)
|
||||
|
||||
# Copy the data back from the new table to the old table
|
||||
op.execute(
|
||||
"""
|
||||
INSERT INTO rom_notes (id, updated_at, raw_markdown, is_public, rom_id, user_id)
|
||||
SELECT id, updated_at, note_raw_markdown, note_is_public, rom_id, user_id
|
||||
FROM rom_user
|
||||
"""
|
||||
)
|
||||
|
||||
# Drop the new table
|
||||
op.drop_table("rom_user")
|
||||
# ### end Alembic commands ###
|
||||
@@ -30,40 +30,37 @@ def platforms_webrcade_feed(request: Request) -> WebrcadeFeedSchema:
|
||||
|
||||
platforms = db_platform_handler.get_platforms()
|
||||
|
||||
with db_platform_handler.session.begin() as session:
|
||||
return {
|
||||
"title": "RomM Feed",
|
||||
"longTitle": "Custom RomM Feed",
|
||||
"description": "Custom feed from your RomM library",
|
||||
"thumbnail": "https://raw.githubusercontent.com/rommapp/romm/f2dd425d87ad8e21bf47f8258ae5dcf90f56fbc2/frontend/assets/isotipo.svg",
|
||||
"background": "https://raw.githubusercontent.com/rommapp/romm/release/.github/screenshots/gallery.png",
|
||||
"categories": [
|
||||
{
|
||||
"title": p.name,
|
||||
"longTitle": f"{p.name} Games",
|
||||
"background": f"{ROMM_HOST}/assets/webrcade/feed/{p.slug.lower()}-background.png",
|
||||
"thumbnail": f"{ROMM_HOST}/assets/webrcade/feed/{p.slug.lower()}-thumb.png",
|
||||
"description": "",
|
||||
"items": [
|
||||
{
|
||||
"title": rom.name,
|
||||
"description": rom.summary,
|
||||
"type": WEBRCADE_SLUG_TO_TYPE_MAP.get(p.slug, p.slug),
|
||||
"thumbnail": f"{ROMM_HOST}/assets/romm/resources/{rom.path_cover_s}",
|
||||
"background": f"{ROMM_HOST}/assets/romm/resources/{rom.path_cover_l}",
|
||||
"props": {
|
||||
"rom": f"{ROMM_HOST}/api/roms/{rom.id}/content/{rom.file_name}"
|
||||
},
|
||||
}
|
||||
for rom in session.scalars(
|
||||
db_rom_handler.get_roms(platform_id=p.id)
|
||||
).all()
|
||||
],
|
||||
}
|
||||
for p in platforms
|
||||
if p.slug in WEBRCADE_SUPPORTED_PLATFORM_SLUGS
|
||||
],
|
||||
}
|
||||
return {
|
||||
"title": "RomM Feed",
|
||||
"longTitle": "Custom RomM Feed",
|
||||
"description": "Custom feed from your RomM library",
|
||||
"thumbnail": "https://raw.githubusercontent.com/rommapp/romm/f2dd425d87ad8e21bf47f8258ae5dcf90f56fbc2/frontend/assets/isotipo.svg",
|
||||
"background": "https://raw.githubusercontent.com/rommapp/romm/release/.github/screenshots/gallery.png",
|
||||
"categories": [
|
||||
{
|
||||
"title": p.name,
|
||||
"longTitle": f"{p.name} Games",
|
||||
"background": f"{ROMM_HOST}/assets/webrcade/feed/{p.slug.lower()}-background.png",
|
||||
"thumbnail": f"{ROMM_HOST}/assets/webrcade/feed/{p.slug.lower()}-thumb.png",
|
||||
"description": "",
|
||||
"items": [
|
||||
{
|
||||
"title": rom.name,
|
||||
"description": rom.summary,
|
||||
"type": WEBRCADE_SLUG_TO_TYPE_MAP.get(p.slug, p.slug),
|
||||
"thumbnail": f"{ROMM_HOST}/assets/romm/resources/{rom.path_cover_s}",
|
||||
"background": f"{ROMM_HOST}/assets/romm/resources/{rom.path_cover_l}",
|
||||
"props": {
|
||||
"rom": f"{ROMM_HOST}/api/roms/{rom.id}/content/{rom.file_name}"
|
||||
},
|
||||
}
|
||||
for rom in db_rom_handler.get_roms(platform_id=p.id)
|
||||
],
|
||||
}
|
||||
for p in platforms
|
||||
if p.slug in WEBRCADE_SUPPORTED_PLATFORM_SLUGS
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@protected_route(router.get, "/tinfoil/feed", ["roms.read"])
|
||||
@@ -79,19 +76,16 @@ def tinfoil_index_feed(request: Request, slug: str = "switch") -> TinfoilFeedSch
|
||||
TinfoilFeedSchema: Tinfoil feed object schema
|
||||
"""
|
||||
switch = db_platform_handler.get_platform_by_fs_slug(slug)
|
||||
with db_rom_handler.session.begin() as session:
|
||||
files: list[Rom] = session.scalars(
|
||||
db_rom_handler.get_roms(platform_id=switch.id)
|
||||
).all()
|
||||
files: list[Rom] = db_rom_handler.get_roms(platform_id=switch.id)
|
||||
|
||||
return {
|
||||
"files": [
|
||||
{
|
||||
"url": f"{ROMM_HOST}/api/roms/{file.id}/content/{file.file_name}",
|
||||
"size": file.file_size_bytes,
|
||||
}
|
||||
for file in files
|
||||
],
|
||||
"directories": [],
|
||||
"success": "RomM Switch Library",
|
||||
}
|
||||
return {
|
||||
"files": [
|
||||
{
|
||||
"url": f"{ROMM_HOST}/api/roms/{file.id}/content/{file.file_name}",
|
||||
"size": file.file_size_bytes,
|
||||
}
|
||||
for file in files
|
||||
],
|
||||
"directories": [],
|
||||
"success": "RomM Switch Library",
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@ from datetime import datetime
|
||||
from decorators.auth import protected_route
|
||||
from endpoints.responses import MessageResponse
|
||||
from endpoints.responses.platform import PlatformSchema
|
||||
from exceptions.endpoint_exceptions import PlatformNotFoundInDatabaseException
|
||||
from exceptions.fs_exceptions import PlatformAlreadyExistsException
|
||||
from fastapi import APIRouter, HTTPException, Request, status
|
||||
from fastapi import APIRouter, Request
|
||||
from handler.database import db_platform_handler
|
||||
from handler.filesystem import fs_platform_handler
|
||||
from handler.metadata.igdb_handler import IGDB_PLATFORM_LIST
|
||||
@@ -100,7 +101,12 @@ def get_platform(request: Request, id: int) -> PlatformSchema:
|
||||
PlatformSchema: Platform
|
||||
"""
|
||||
|
||||
return db_platform_handler.get_platform(id)
|
||||
platform = db_platform_handler.get_platform(id)
|
||||
|
||||
if not platform:
|
||||
raise PlatformNotFoundInDatabaseException(id)
|
||||
|
||||
return platform
|
||||
|
||||
|
||||
@protected_route(router.put, "/platforms/{id}", ["platforms.write"])
|
||||
@@ -135,10 +141,9 @@ async def delete_platforms(request: Request, id: int) -> MessageResponse:
|
||||
"""
|
||||
|
||||
platform = db_platform_handler.get_platform(id)
|
||||
|
||||
if not platform:
|
||||
error = f"Platform id {id} not found"
|
||||
log.error(error)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error)
|
||||
raise PlatformNotFoundInDatabaseException(id)
|
||||
|
||||
log.info(f"Deleting {platform.name} [{platform.fs_slug}] from database")
|
||||
db_platform_handler.delete_platform(id)
|
||||
|
||||
@@ -28,25 +28,39 @@ RomMobyMetadata = TypedDict( # type: ignore[misc]
|
||||
)
|
||||
|
||||
|
||||
class RomNoteSchema(BaseModel):
|
||||
class RomUserSchema(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
rom_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
raw_markdown: str
|
||||
is_public: bool
|
||||
note_raw_markdown: str
|
||||
note_is_public: bool
|
||||
is_main_sibling: bool
|
||||
user__username: str
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@classmethod
|
||||
def for_user(cls, db_rom: Rom, user_id: int) -> list[RomNoteSchema]:
|
||||
def for_user(cls, db_rom: Rom, user_id: int) -> RomUserSchema | None:
|
||||
for n in db_rom.rom_users:
|
||||
if n.user_id == user_id:
|
||||
return cls.model_validate(n)
|
||||
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def notes_for_user(cls, db_rom: Rom, user_id: int) -> list[UserNotesSchema]:
|
||||
return [
|
||||
cls.model_validate(n)
|
||||
for n in db_rom.notes
|
||||
{
|
||||
"user_id": n.user_id,
|
||||
"username": n.user__username,
|
||||
"note_raw_markdown": n.note_raw_markdown,
|
||||
}
|
||||
for n in db_rom.rom_users
|
||||
# This is what filters out private notes
|
||||
if n.user_id == user_id or n.is_public
|
||||
if n.user_id == user_id or n.note_is_public
|
||||
]
|
||||
|
||||
|
||||
@@ -95,13 +109,29 @@ class RomSchema(BaseModel):
|
||||
multi: bool
|
||||
files: list[str]
|
||||
full_path: str
|
||||
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
rom_user: RomUserSchema | None = Field(default=None)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_request(cls, db_rom: Rom, request: Request) -> RomSchema:
|
||||
rom = cls.model_validate(db_rom)
|
||||
user_id = request.user.id
|
||||
|
||||
rom.rom_user = RomUserSchema.for_user(db_rom, user_id)
|
||||
|
||||
return rom
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_request_list(
|
||||
cls, db_roms: list[Rom], request: Request
|
||||
) -> list[RomSchema]:
|
||||
return [cls.from_orm_with_request(rom, request) for rom in db_roms]
|
||||
|
||||
@computed_field # type: ignore
|
||||
@property
|
||||
def sort_comparator(self) -> str:
|
||||
@@ -117,17 +147,20 @@ class RomSchema(BaseModel):
|
||||
|
||||
class DetailedRomSchema(RomSchema):
|
||||
merged_screenshots: list[str]
|
||||
rom_user: RomUserSchema | None = Field(default=None)
|
||||
sibling_roms: list[RomSchema] = Field(default_factory=list)
|
||||
user_saves: list[SaveSchema] = Field(default_factory=list)
|
||||
user_states: list[StateSchema] = Field(default_factory=list)
|
||||
user_screenshots: list[ScreenshotSchema] = Field(default_factory=list)
|
||||
user_notes: list[RomNoteSchema] = Field(default_factory=list)
|
||||
user_notes: list[UserNotesSchema] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_orm_with_request(cls, db_rom: Rom, request: Request) -> DetailedRomSchema:
|
||||
rom = cls.model_validate(db_rom)
|
||||
user_id = request.user.id
|
||||
|
||||
rom.rom_user = RomUserSchema.for_user(db_rom, user_id)
|
||||
rom.user_notes = RomUserSchema.notes_for_user(db_rom, user_id)
|
||||
rom.sibling_roms = [
|
||||
RomSchema.model_validate(r) for r in db_rom.get_sibling_roms()
|
||||
]
|
||||
@@ -142,11 +175,16 @@ class DetailedRomSchema(RomSchema):
|
||||
for s in db_rom.screenshots
|
||||
if s.user_id == user_id
|
||||
]
|
||||
rom.user_notes = RomNoteSchema.for_user(db_rom, user_id)
|
||||
|
||||
return rom
|
||||
|
||||
|
||||
class UserNotesSchema(TypedDict):
|
||||
user_id: int
|
||||
username: str
|
||||
note_raw_markdown: str
|
||||
|
||||
|
||||
class AddRomsResponse(TypedDict):
|
||||
uploaded_roms: list[str]
|
||||
skipped_roms: list[str]
|
||||
|
||||
@@ -16,9 +16,10 @@ from endpoints.responses.rom import (
|
||||
AddRomsResponse,
|
||||
CustomStreamingResponse,
|
||||
DetailedRomSchema,
|
||||
RomNoteSchema,
|
||||
RomSchema,
|
||||
RomUserSchema,
|
||||
)
|
||||
from exceptions.endpoint_exceptions import RomNotFoundInDatabaseException
|
||||
from exceptions.fs_exceptions import RomAlreadyExistsException
|
||||
from fastapi import APIRouter, File, HTTPException, Query, Request, UploadFile, status
|
||||
from fastapi.responses import FileResponse
|
||||
@@ -107,16 +108,15 @@ def get_roms(
|
||||
Returns:
|
||||
list[RomSchema]: List of roms stored in the database
|
||||
"""
|
||||
db_roms = db_rom_handler.get_roms(
|
||||
platform_id=platform_id,
|
||||
search_term=search_term.lower(),
|
||||
order_by=order_by.lower(),
|
||||
order_dir=order_dir.lower(),
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
with db_rom_handler.session.begin() as session:
|
||||
return session.scalars(
|
||||
db_rom_handler.get_roms(
|
||||
platform_id=platform_id,
|
||||
search_term=search_term.lower(),
|
||||
order_by=order_by.lower(),
|
||||
order_dir=order_dir.lower(),
|
||||
).limit(limit)
|
||||
).all()
|
||||
return RomSchema.from_orm_with_request_list(db_roms, request)
|
||||
|
||||
|
||||
@protected_route(
|
||||
@@ -134,7 +134,13 @@ def get_rom(request: Request, id: int) -> DetailedRomSchema:
|
||||
Returns:
|
||||
DetailedRomSchema: Rom stored in the database
|
||||
"""
|
||||
return DetailedRomSchema.from_orm_with_request(db_rom_handler.get_rom(id), request)
|
||||
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
if not rom:
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
return DetailedRomSchema.from_orm_with_request(rom, request)
|
||||
|
||||
|
||||
@protected_route(
|
||||
@@ -155,6 +161,10 @@ def head_rom_content(request: Request, id: int, file_name: str):
|
||||
"""
|
||||
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
if not rom:
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
|
||||
|
||||
return FileResponse(
|
||||
@@ -190,6 +200,10 @@ def get_rom_content(
|
||||
"""
|
||||
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
if not rom:
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
rom_path = f"{LIBRARY_BASE_PATH}/{rom.full_path}"
|
||||
files_to_download = files or rom.files
|
||||
|
||||
@@ -273,7 +287,10 @@ async def update_rom(
|
||||
|
||||
data = await request.form()
|
||||
|
||||
db_rom = db_rom_handler.get_rom(id)
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
if not rom:
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
cleaned_data = {}
|
||||
cleaned_data["igdb_id"] = data.get("igdb_id", None)
|
||||
@@ -291,25 +308,23 @@ async def update_rom(
|
||||
else:
|
||||
cleaned_data.update({"igdb_metadata": {}})
|
||||
|
||||
cleaned_data["name"] = data.get("name", db_rom.name)
|
||||
cleaned_data["summary"] = data.get("summary", db_rom.summary)
|
||||
cleaned_data["name"] = data.get("name", rom.name)
|
||||
cleaned_data["summary"] = data.get("summary", rom.summary)
|
||||
|
||||
fs_safe_file_name = (
|
||||
data.get("file_name", db_rom.file_name).strip().replace("/", "-")
|
||||
)
|
||||
fs_safe_file_name = data.get("file_name", rom.file_name).strip().replace("/", "-")
|
||||
fs_safe_name = cleaned_data["name"].strip().replace("/", "-")
|
||||
|
||||
if rename_as_source:
|
||||
fs_safe_file_name = db_rom.file_name.replace(
|
||||
db_rom.file_name_no_tags or db_rom.file_name_no_ext, fs_safe_name
|
||||
fs_safe_file_name = rom.file_name.replace(
|
||||
rom.file_name_no_tags or rom.file_name_no_ext, fs_safe_name
|
||||
)
|
||||
|
||||
try:
|
||||
if db_rom.file_name != fs_safe_file_name:
|
||||
if rom.file_name != fs_safe_file_name:
|
||||
fs_rom_handler.rename_file(
|
||||
old_name=db_rom.file_name,
|
||||
old_name=rom.file_name,
|
||||
new_name=fs_safe_file_name,
|
||||
file_path=db_rom.file_path,
|
||||
file_path=rom.file_path,
|
||||
)
|
||||
except RomAlreadyExistsException as exc:
|
||||
log.error(str(exc))
|
||||
@@ -326,7 +341,7 @@ async def update_rom(
|
||||
)
|
||||
|
||||
if remove_cover:
|
||||
cleaned_data.update(fs_resource_handler.remove_cover(rom=db_rom))
|
||||
cleaned_data.update(fs_resource_handler.remove_cover(rom=rom))
|
||||
cleaned_data.update({"url_cover": ""})
|
||||
else:
|
||||
if artwork is not None:
|
||||
@@ -335,7 +350,7 @@ async def update_rom(
|
||||
path_cover_l,
|
||||
path_cover_s,
|
||||
artwork_path,
|
||||
) = fs_resource_handler.build_artwork_path(db_rom, file_ext)
|
||||
) = fs_resource_handler.build_artwork_path(rom, file_ext)
|
||||
|
||||
cleaned_data["path_cover_l"] = path_cover_l
|
||||
cleaned_data["path_cover_s"] = path_cover_s
|
||||
@@ -350,22 +365,19 @@ async def update_rom(
|
||||
with open(file_location_l, "wb+") as artwork_l:
|
||||
artwork_l.write(artwork_file)
|
||||
else:
|
||||
cleaned_data["url_cover"] = data.get("url_cover", db_rom.url_cover)
|
||||
cleaned_data["url_cover"] = data.get("url_cover", rom.url_cover)
|
||||
path_cover_s, path_cover_l = fs_resource_handler.get_rom_cover(
|
||||
overwrite=True,
|
||||
rom=db_rom,
|
||||
rom=rom,
|
||||
url_cover=cleaned_data.get("url_cover", ""),
|
||||
)
|
||||
cleaned_data.update(
|
||||
{"path_cover_s": path_cover_s, "path_cover_l": path_cover_l}
|
||||
)
|
||||
|
||||
if (
|
||||
cleaned_data["igdb_id"] != db_rom.igdb_id
|
||||
or cleaned_data["moby_id"] != db_rom.moby_id
|
||||
):
|
||||
if cleaned_data["igdb_id"] != rom.igdb_id or cleaned_data["moby_id"] != rom.moby_id:
|
||||
path_screenshots = fs_resource_handler.get_rom_screenshots(
|
||||
rom=db_rom,
|
||||
rom=rom,
|
||||
url_screenshots=cleaned_data.get("url_screenshots", []),
|
||||
)
|
||||
cleaned_data.update({"path_screenshots": path_screenshots})
|
||||
@@ -398,10 +410,9 @@ async def delete_roms(
|
||||
|
||||
for id in roms_ids:
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
if not rom:
|
||||
error = f"Rom with id {id} not found"
|
||||
log.error(error)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=error)
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
log.info(f"Deleting {rom.file_name} from database")
|
||||
db_rom_handler.delete_rom(id)
|
||||
@@ -427,21 +438,28 @@ async def delete_roms(
|
||||
return {"msg": f"{len(roms_ids)} roms deleted successfully!"}
|
||||
|
||||
|
||||
@protected_route(router.put, "/roms/{id}/note", ["notes.write"])
|
||||
async def update_rom_note(request: Request, id: int) -> RomNoteSchema:
|
||||
db_note = db_rom_handler.get_rom_note(id, request.user.id)
|
||||
if not db_note:
|
||||
db_note = db_rom_handler.add_rom_note(id, request.user.id)
|
||||
|
||||
@protected_route(router.put, "/roms/{id}/props", ["roms.user.write"])
|
||||
async def update_rom_user(request: Request, id: int) -> RomUserSchema:
|
||||
data = await request.json()
|
||||
db_rom_handler.update_rom_note(
|
||||
db_note.id,
|
||||
{
|
||||
"updated_at": datetime.now(),
|
||||
"raw_markdown": data.get("raw_markdown", db_note.raw_markdown),
|
||||
"is_public": data.get("is_public", db_note.is_public),
|
||||
},
|
||||
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
if not rom:
|
||||
raise RomNotFoundInDatabaseException(id)
|
||||
|
||||
db_rom_user = db_rom_handler.get_rom_user(
|
||||
id, request.user.id
|
||||
) or db_rom_handler.add_rom_user(id, request.user.id)
|
||||
|
||||
cleaned_data = {}
|
||||
cleaned_data["note_raw_markdown"] = data.get(
|
||||
"note_raw_markdown", db_rom_user.note_raw_markdown
|
||||
)
|
||||
cleaned_data["note_is_public"] = data.get(
|
||||
"note_is_public", db_rom_user.note_is_public
|
||||
)
|
||||
cleaned_data["is_main_sibling"] = data.get(
|
||||
"is_main_sibling", db_rom_user.is_main_sibling
|
||||
)
|
||||
|
||||
db_note = db_rom_handler.get_rom_note(id, request.user.id)
|
||||
return db_note
|
||||
return db_rom_handler.update_rom_user(db_rom_user.id, cleaned_data)
|
||||
|
||||
@@ -29,7 +29,7 @@ from rq import Worker
|
||||
from rq.job import Job
|
||||
from sqlalchemy.inspection import inspect
|
||||
|
||||
EXCLUDED_FROM_DUMP: Final = {"created_at", "updated_at"}
|
||||
EXCLUDED_FROM_DUMP: Final = {"created_at", "updated_at", "rom_user"}
|
||||
|
||||
|
||||
class ScanStats:
|
||||
|
||||
24
backend/exceptions/endpoint_exceptions.py
Normal file
24
backend/exceptions/endpoint_exceptions.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import HTTPException, status
|
||||
from logger.logger import log
|
||||
|
||||
|
||||
class PlatformNotFoundInDatabaseException(Exception):
|
||||
def __init__(self, id):
|
||||
self.message = f"Platform with id '{id}' not found"
|
||||
super().__init__(self.message)
|
||||
log.critical(self.message)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=self.message)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class RomNotFoundInDatabaseException(Exception):
|
||||
def __init__(self, id):
|
||||
self.message = f"Rom with id '{id}' not found"
|
||||
super().__init__(self.message)
|
||||
log.critical(self.message)
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=self.message)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.message
|
||||
@@ -22,8 +22,8 @@ DEFAULT_SCOPES_MAP: Final = {
|
||||
"assets.read": "View assets",
|
||||
"assets.write": "Modify assets",
|
||||
"firmware.read": "View firmware",
|
||||
"notes.read": "View notes",
|
||||
"notes.write": "Modify notes",
|
||||
"roms.user.read": "View user-rom properties",
|
||||
"roms.user.write": "Modify user-rom properties",
|
||||
}
|
||||
|
||||
WRITE_SCOPES_MAP: Final = {
|
||||
|
||||
@@ -1,35 +1,52 @@
|
||||
import functools
|
||||
|
||||
from decorators.database import begin_session
|
||||
from models.rom import Rom, RomNote
|
||||
from sqlalchemy import Select, and_, delete, func, or_, select, update
|
||||
from models.rom import Rom, RomUser
|
||||
from sqlalchemy import and_, delete, func, or_, select, update
|
||||
from sqlalchemy.orm import Query, Session, selectinload
|
||||
|
||||
from .base_handler import DBBaseHandler
|
||||
|
||||
|
||||
def with_assets(func):
|
||||
def with_details(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
session = kwargs.get("session")
|
||||
if session is None:
|
||||
raise ValueError("session is required")
|
||||
raise TypeError(
|
||||
f"{func} is missing required kwarg 'session' with type 'Session'"
|
||||
)
|
||||
|
||||
kwargs["query"] = select(Rom).options(
|
||||
selectinload(Rom.saves),
|
||||
selectinload(Rom.states),
|
||||
selectinload(Rom.screenshots),
|
||||
selectinload(Rom.notes),
|
||||
selectinload(Rom.rom_users),
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def with_simple(func):
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
session = kwargs.get("session")
|
||||
if session is None:
|
||||
raise TypeError(
|
||||
f"{func} is missing required kwarg 'session' with type 'Session'"
|
||||
)
|
||||
|
||||
kwargs["query"] = select(Rom).options(selectinload(Rom.rom_users))
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class DBRomsHandler(DBBaseHandler):
|
||||
def _filter(self, data, platform_id: int | None, search_term: str):
|
||||
if platform_id:
|
||||
data = data.filter_by(platform_id=platform_id)
|
||||
data = data.filter(Rom.platform_id == platform_id)
|
||||
|
||||
if search_term:
|
||||
data = data.filter(
|
||||
@@ -53,7 +70,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
return data.order_by(_column.asc())
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
@with_details
|
||||
def add_rom(self, rom: Rom, query: Query = None, session: Session = None) -> Rom:
|
||||
rom = session.merge(rom)
|
||||
session.flush()
|
||||
@@ -61,12 +78,14 @@ class DBRomsHandler(DBBaseHandler):
|
||||
return session.scalar(query.filter_by(id=rom.id).limit(1))
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
@with_details
|
||||
def get_rom(
|
||||
self, id: int, *, query: Query = None, session: Session = None
|
||||
) -> Rom | None:
|
||||
return session.scalar(query.filter_by(id=id).limit(1))
|
||||
|
||||
@begin_session
|
||||
@with_simple
|
||||
def get_roms(
|
||||
self,
|
||||
*,
|
||||
@@ -74,15 +93,24 @@ class DBRomsHandler(DBBaseHandler):
|
||||
search_term: str = "",
|
||||
order_by: str = "name",
|
||||
order_dir: str = "asc",
|
||||
) -> Select[tuple[Rom]]:
|
||||
return self._order(
|
||||
self._filter(select(Rom), platform_id, search_term),
|
||||
order_by,
|
||||
order_dir,
|
||||
limit: int | None = None,
|
||||
query: Query = None,
|
||||
session: Session = None,
|
||||
) -> list[Rom]:
|
||||
return (
|
||||
session.scalars(
|
||||
self._order(
|
||||
self._filter(query, platform_id, search_term),
|
||||
order_by,
|
||||
order_dir,
|
||||
).limit(limit)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
@with_details
|
||||
def get_rom_by_filename(
|
||||
self,
|
||||
platform_id: int,
|
||||
@@ -95,7 +123,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
@with_details
|
||||
def get_rom_by_filename_no_tags(
|
||||
self, file_name_no_tags: str, query: Query = None, session: Session = None
|
||||
) -> Rom | None:
|
||||
@@ -104,7 +132,7 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_assets
|
||||
@with_details
|
||||
def get_rom_by_filename_no_ext(
|
||||
self, file_name_no_ext: str, query: Query = None, session: Session = None
|
||||
) -> Rom | None:
|
||||
@@ -112,6 +140,32 @@ class DBRomsHandler(DBBaseHandler):
|
||||
query.filter_by(file_name_no_ext=file_name_no_ext).limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
@with_simple
|
||||
def get_sibling_roms(
|
||||
self, rom: Rom, query: Query = None, session: Session = None
|
||||
) -> list[Rom]:
|
||||
return session.scalars(
|
||||
query.where(
|
||||
and_(
|
||||
Rom.platform_id == rom.platform_id,
|
||||
Rom.id != rom.id,
|
||||
or_(
|
||||
and_(
|
||||
Rom.igdb_id == rom.igdb_id,
|
||||
Rom.igdb_id.isnot(None),
|
||||
Rom.igdb_id != "",
|
||||
),
|
||||
and_(
|
||||
Rom.moby_id == rom.moby_id,
|
||||
Rom.moby_id.isnot(None),
|
||||
Rom.moby_id != "",
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
).all()
|
||||
|
||||
@begin_session
|
||||
def update_rom(self, id: int, data: dict, session: Session = None) -> Rom:
|
||||
return session.execute(
|
||||
@@ -140,24 +194,46 @@ class DBRomsHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_rom_note(
|
||||
def add_rom_user(
|
||||
self, rom_id: int, user_id: int, session: Session = None
|
||||
) -> RomNote | None:
|
||||
) -> RomUser:
|
||||
return session.merge(RomUser(rom_id=rom_id, user_id=user_id))
|
||||
|
||||
@begin_session
|
||||
def get_rom_user(
|
||||
self, rom_id: int, user_id: int, session: Session = None
|
||||
) -> RomUser | None:
|
||||
return session.scalar(
|
||||
select(RomNote).filter_by(rom_id=rom_id, user_id=user_id).limit(1)
|
||||
select(RomUser).filter_by(rom_id=rom_id, user_id=user_id).limit(1)
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def add_rom_note(
|
||||
self, rom_id: int, user_id: int, session: Session = None
|
||||
) -> RomNote:
|
||||
return session.merge(RomNote(rom_id=rom_id, user_id=user_id))
|
||||
def get_rom_user_by_id(self, id: int, session: Session = None) -> RomUser | None:
|
||||
return session.scalar(select(RomUser).filter_by(id=id).limit(1))
|
||||
|
||||
@begin_session
|
||||
def update_rom_note(self, id: int, data: dict, session: Session = None) -> RomNote:
|
||||
return session.execute(
|
||||
update(RomNote)
|
||||
.where(RomNote.id == id)
|
||||
def update_rom_user(self, id: int, data: dict, session: Session = None) -> RomUser:
|
||||
session.execute(
|
||||
update(RomUser)
|
||||
.where(RomUser.id == id)
|
||||
.values(**data)
|
||||
.execution_options(synchronize_session="evaluate")
|
||||
)
|
||||
|
||||
rom_user = self.get_rom_user_by_id(id)
|
||||
|
||||
if data["is_main_sibling"]:
|
||||
session.execute(
|
||||
update(RomUser)
|
||||
.where(
|
||||
and_(
|
||||
RomUser.rom_id.in_(
|
||||
[rom.id for rom in rom_user.rom.get_sibling_roms()]
|
||||
),
|
||||
RomUser.user_id == rom_user.user_id,
|
||||
)
|
||||
)
|
||||
.values(is_main_sibling=False)
|
||||
)
|
||||
|
||||
return self.get_rom_user_by_id(id)
|
||||
|
||||
@@ -29,7 +29,7 @@ class DBUsersHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_users(self, session: Session = None):
|
||||
def get_users(self, session: Session = None) -> list[User]:
|
||||
return session.scalars(select(User)).all()
|
||||
|
||||
@begin_session
|
||||
@@ -41,5 +41,5 @@ class DBUsersHandler(DBBaseHandler):
|
||||
)
|
||||
|
||||
@begin_session
|
||||
def get_admin_users(self, session: Session = None):
|
||||
def get_admin_users(self, session: Session = None) -> list[User]:
|
||||
return session.scalars(select(User).filter_by(role=Role.ADMIN)).all()
|
||||
|
||||
@@ -46,9 +46,8 @@ def test_roms(rom: Rom, platform: Platform):
|
||||
)
|
||||
)
|
||||
|
||||
with db_rom_handler.session.begin() as session:
|
||||
roms = session.scalars(db_rom_handler.get_roms(platform_id=platform.id)).all()
|
||||
assert len(roms) == 2
|
||||
roms = db_rom_handler.get_roms(platform_id=platform.id)
|
||||
assert len(roms) == 2
|
||||
|
||||
rom = db_rom_handler.get_rom(roms[0].id)
|
||||
assert rom is not None
|
||||
@@ -61,26 +60,23 @@ def test_roms(rom: Rom, platform: Platform):
|
||||
|
||||
db_rom_handler.delete_rom(rom.id)
|
||||
|
||||
with db_rom_handler.session.begin() as session:
|
||||
roms = session.scalars(db_rom_handler.get_roms(platform_id=platform.id)).all()
|
||||
assert len(roms) == 1
|
||||
roms = db_rom_handler.get_roms(platform_id=platform.id)
|
||||
assert len(roms) == 1
|
||||
|
||||
db_rom_handler.purge_roms(rom_2.platform_id, [rom_2.id])
|
||||
|
||||
with db_rom_handler.session.begin() as session:
|
||||
roms = session.scalars(db_rom_handler.get_roms(platform_id=platform.id)).all()
|
||||
assert len(roms) == 0
|
||||
roms = db_rom_handler.get_roms(platform_id=platform.id)
|
||||
assert len(roms) == 0
|
||||
|
||||
|
||||
def test_utils(rom: Rom, platform: Platform):
|
||||
with db_rom_handler.session.begin() as session:
|
||||
roms = session.scalars(db_rom_handler.get_roms(platform_id=platform.id)).all()
|
||||
assert (
|
||||
db_rom_handler.get_rom_by_filename(
|
||||
platform_id=platform.id, file_name=rom.file_name
|
||||
).id
|
||||
== roms[0].id
|
||||
)
|
||||
roms = db_rom_handler.get_roms(platform_id=platform.id)
|
||||
assert (
|
||||
db_rom_handler.get_rom_by_filename(
|
||||
platform_id=platform.id, file_name=rom.file_name
|
||||
).id
|
||||
== roms[0].id
|
||||
)
|
||||
|
||||
|
||||
def test_users(admin_user):
|
||||
|
||||
@@ -1,24 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from config import FRONTEND_RESOURCES_PATH
|
||||
from models.base import BaseModel
|
||||
from sqlalchemy import (
|
||||
JSON,
|
||||
BigInteger,
|
||||
DateTime,
|
||||
ForeignKey,
|
||||
String,
|
||||
Text,
|
||||
UniqueConstraint,
|
||||
and_,
|
||||
func,
|
||||
or_,
|
||||
select,
|
||||
)
|
||||
from sqlalchemy import JSON, BigInteger, ForeignKey, String, Text, UniqueConstraint
|
||||
from sqlalchemy.dialects.mysql.json import JSON as MySQLJSON
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -82,7 +69,7 @@ class Rom(BaseModel):
|
||||
saves: Mapped[list[Save]] = relationship(back_populates="rom")
|
||||
states: Mapped[list[State]] = relationship(back_populates="rom")
|
||||
screenshots: Mapped[list[Screenshot]] = relationship(back_populates="rom")
|
||||
notes: Mapped[list[RomNote]] = relationship(back_populates="rom")
|
||||
rom_users: Mapped[list[RomUser]] = relationship(back_populates="rom")
|
||||
|
||||
@property
|
||||
def platform_slug(self) -> str:
|
||||
@@ -114,27 +101,7 @@ class Rom(BaseModel):
|
||||
def get_sibling_roms(self) -> list[Rom]:
|
||||
from handler.database import db_rom_handler
|
||||
|
||||
with db_rom_handler.session.begin() as session:
|
||||
return session.scalars(
|
||||
select(Rom).where(
|
||||
and_(
|
||||
Rom.platform_id == self.platform_id,
|
||||
Rom.id != self.id,
|
||||
or_(
|
||||
and_(
|
||||
Rom.igdb_id == self.igdb_id,
|
||||
Rom.igdb_id is not None,
|
||||
Rom.igdb_id != "",
|
||||
),
|
||||
and_(
|
||||
Rom.moby_id == self.moby_id,
|
||||
Rom.moby_id is not None,
|
||||
Rom.moby_id != "",
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
).all()
|
||||
return db_rom_handler.get_sibling_roms(self)
|
||||
|
||||
# Metadata fields
|
||||
@property
|
||||
@@ -181,24 +148,24 @@ class Rom(BaseModel):
|
||||
return self.file_name
|
||||
|
||||
|
||||
class RomNote(BaseModel):
|
||||
__tablename__ = "rom_notes"
|
||||
class RomUser(BaseModel):
|
||||
__tablename__ = "rom_user"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("rom_id", "user_id", name="unique_rom_user_note"),
|
||||
UniqueConstraint("rom_id", "user_id", name="unique_rom_user_props"),
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
)
|
||||
raw_markdown: Mapped[str] = mapped_column(Text, default="")
|
||||
is_public: Mapped[bool | None] = mapped_column(default=False)
|
||||
|
||||
note_raw_markdown: Mapped[str] = mapped_column(Text, default="")
|
||||
note_is_public: Mapped[bool | None] = mapped_column(default=False)
|
||||
|
||||
is_main_sibling: Mapped[bool | None] = mapped_column(default=False)
|
||||
|
||||
rom_id: Mapped[int] = mapped_column(ForeignKey("roms.id", ondelete="CASCADE"))
|
||||
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
|
||||
|
||||
rom: Mapped[Rom] = relationship(lazy="joined", back_populates="notes")
|
||||
user: Mapped[User] = relationship(lazy="joined", back_populates="notes")
|
||||
rom: Mapped[Rom] = relationship(lazy="joined", back_populates="rom_users")
|
||||
user: Mapped[User] = relationship(lazy="joined", back_populates="rom_users")
|
||||
|
||||
@property
|
||||
def user__username(self) -> str:
|
||||
|
||||
@@ -11,7 +11,7 @@ from starlette.authentication import SimpleUser
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models.assets import Save, Screenshot, State
|
||||
from models.rom import RomNote
|
||||
from models.rom import RomUser
|
||||
|
||||
|
||||
class Role(enum.Enum):
|
||||
@@ -39,7 +39,7 @@ class User(BaseModel, SimpleUser):
|
||||
saves: Mapped[list[Save]] = relationship(back_populates="user")
|
||||
states: Mapped[list[State]] = relationship(back_populates="user")
|
||||
screenshots: Mapped[list[Screenshot]] = relationship(back_populates="user")
|
||||
notes: Mapped[list[RomNote]] = relationship(back_populates="user")
|
||||
rom_users: Mapped[list[RomUser]] = relationship(back_populates="user")
|
||||
|
||||
@property
|
||||
def oauth_scopes(self):
|
||||
|
||||
3
frontend/src/__generated__/index.ts
generated
3
frontend/src/__generated__/index.ts
generated
@@ -27,8 +27,8 @@ export type { PlatformSchema } from "./models/PlatformSchema";
|
||||
export type { Role } from "./models/Role";
|
||||
export type { RomIGDBMetadata } from "./models/RomIGDBMetadata";
|
||||
export type { RomMobyMetadata } from "./models/RomMobyMetadata";
|
||||
export type { RomNoteSchema } from "./models/RomNoteSchema";
|
||||
export type { RomSchema } from "./models/RomSchema";
|
||||
export type { RomUserSchema } from "./models/RomUserSchema";
|
||||
export type { SaveSchema } from "./models/SaveSchema";
|
||||
export type { SchedulerDict } from "./models/SchedulerDict";
|
||||
export type { ScreenshotSchema } from "./models/ScreenshotSchema";
|
||||
@@ -42,6 +42,7 @@ export type { TokenResponse } from "./models/TokenResponse";
|
||||
export type { UploadedSavesResponse } from "./models/UploadedSavesResponse";
|
||||
export type { UploadedScreenshotsResponse } from "./models/UploadedScreenshotsResponse";
|
||||
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 { WatcherDict } from "./models/WatcherDict";
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
|
||||
import type { RomIGDBMetadata } from "./RomIGDBMetadata";
|
||||
import type { RomMobyMetadata } from "./RomMobyMetadata";
|
||||
import type { RomNoteSchema } from "./RomNoteSchema";
|
||||
import type { RomSchema } from "./RomSchema";
|
||||
import type { RomUserSchema } from "./RomUserSchema";
|
||||
import type { SaveSchema } from "./SaveSchema";
|
||||
import type { ScreenshotSchema } from "./ScreenshotSchema";
|
||||
import type { StateSchema } from "./StateSchema";
|
||||
import type { UserNotesSchema } from "./UserNotesSchema";
|
||||
|
||||
export type DetailedRomSchema = {
|
||||
id: number;
|
||||
@@ -50,11 +51,12 @@ export type DetailedRomSchema = {
|
||||
full_path: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
rom_user?: RomUserSchema | null;
|
||||
merged_screenshots: Array<string>;
|
||||
sibling_roms?: Array<RomSchema>;
|
||||
user_saves?: Array<SaveSchema>;
|
||||
user_states?: Array<StateSchema>;
|
||||
user_screenshots?: Array<ScreenshotSchema>;
|
||||
user_notes?: Array<RomNoteSchema>;
|
||||
user_notes?: Array<UserNotesSchema>;
|
||||
readonly sort_comparator: string;
|
||||
};
|
||||
|
||||
2
frontend/src/__generated__/models/RomSchema.ts
generated
2
frontend/src/__generated__/models/RomSchema.ts
generated
@@ -5,6 +5,7 @@
|
||||
|
||||
import type { RomIGDBMetadata } from "./RomIGDBMetadata";
|
||||
import type { RomMobyMetadata } from "./RomMobyMetadata";
|
||||
import type { RomUserSchema } from "./RomUserSchema";
|
||||
|
||||
export type RomSchema = {
|
||||
id: number;
|
||||
@@ -45,5 +46,6 @@ export type RomSchema = {
|
||||
full_path: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
rom_user?: RomUserSchema | null;
|
||||
readonly sort_comparator: string;
|
||||
};
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type RomNoteSchema = {
|
||||
export type RomUserSchema = {
|
||||
id: number;
|
||||
user_id: number;
|
||||
rom_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
raw_markdown: string;
|
||||
is_public: boolean;
|
||||
note_raw_markdown: string;
|
||||
note_is_public: boolean;
|
||||
is_main_sibling: boolean;
|
||||
user__username: string;
|
||||
};
|
||||
10
frontend/src/__generated__/models/UserNotesSchema.ts
generated
Normal file
10
frontend/src/__generated__/models/UserNotesSchema.ts
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
/* generated using openapi-typescript-codegen -- do no edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type UserNotesSchema = {
|
||||
user_id: number;
|
||||
username: string;
|
||||
note_raw_markdown: string;
|
||||
};
|
||||
@@ -1,77 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import VersionSwitcher from "@/components/Details/VersionSwitcher.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import storeDownload from "@/stores/download";
|
||||
import type { Platform } from "@/stores/platforms";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { formatBytes } from "@/utils";
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
defineProps<{ rom: DetailedRom; platform: Platform }>();
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom; platform: Platform }>();
|
||||
const downloadStore = storeDownload();
|
||||
const auth = storeAuth();
|
||||
const romUser = ref(
|
||||
props.rom.rom_user ?? {
|
||||
id: null,
|
||||
user_id: auth.user?.id,
|
||||
rom_id: props.rom.id,
|
||||
updated_at: new Date(),
|
||||
note_raw_markdown: "",
|
||||
note_is_public: false,
|
||||
is_main_sibling: false,
|
||||
}
|
||||
);
|
||||
|
||||
// Functions
|
||||
async function toggleMainSibling() {
|
||||
ownProps.value.is_main_sibling = !ownProps.value.is_main_sibling;
|
||||
romApi.updateUserRomProps({
|
||||
romId: props.rom.id,
|
||||
noteRawMarkdown: ownProps.value.note_raw_markdown,
|
||||
noteIsPublic: ownProps.value.note_is_public,
|
||||
isMainSibling: ownProps.value.is_main_sibling,
|
||||
});
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.rom,
|
||||
async () => {
|
||||
ownProps.value = props.rom.rom_user ?? {
|
||||
id: null,
|
||||
user_id: auth.user?.id,
|
||||
rom_id: props.rom.id,
|
||||
updated_at: new Date(),
|
||||
note_raw_markdown: "",
|
||||
note_is_public: false,
|
||||
is_main_sibling: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<v-row
|
||||
v-if="rom.sibling_roms && rom.sibling_roms.length > 0"
|
||||
class="align-center my-3"
|
||||
no-gutters
|
||||
>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Ver.</span>
|
||||
</v-col>
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<version-switcher :rom="rom" :platform="platform" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!rom.multi" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>File</span>
|
||||
</v-col>
|
||||
<v-col class="text-body-1">
|
||||
<span>{{ rom.file_name }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="rom.multi" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Files</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="downloadStore.filesToDownloadMultiFileRom"
|
||||
:label="rom.file_name"
|
||||
item-title="file_name"
|
||||
:items="rom.files"
|
||||
class="my-2"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
return-object
|
||||
multiple
|
||||
hide-details
|
||||
clearable
|
||||
chips
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Size</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<span>{{ formatBytes(rom.file_size_bytes) }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="rom.tags.length > 0" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Tags</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-for="tag in rom.tags"
|
||||
:key="tag"
|
||||
class="mr-2 py-1"
|
||||
label
|
||||
variant="outlined"
|
||||
<v-row
|
||||
v-if="rom.sibling_roms && rom.sibling_roms.length > 0"
|
||||
class="align-center my-3"
|
||||
no-gutters
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Version</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-row class="align-center" no-gutters>
|
||||
<version-switcher :rom="rom" :platform="platform" />
|
||||
<v-tooltip
|
||||
location="top"
|
||||
class="tooltip"
|
||||
transition="fade-transition"
|
||||
text="Set as default version"
|
||||
open-delay="300"
|
||||
>
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
variant="flat"
|
||||
rounded="0"
|
||||
size="small"
|
||||
@click="toggleMainSibling"
|
||||
><v-icon
|
||||
:class="ownProps.is_main_sibling ? '' : 'mr-1'"
|
||||
:color="ownProps.is_main_sibling ? 'romm-accent-1' : ''"
|
||||
>{{
|
||||
ownProps.is_main_sibling
|
||||
? "mdi-checkbox-outline"
|
||||
: "mdi-checkbox-blank-outline"
|
||||
}}</v-icon
|
||||
>{{ ownProps.is_main_sibling ? "" : "Default" }}</v-btn
|
||||
>
|
||||
</template></v-tooltip
|
||||
>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!rom.multi" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>File</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<span class="text-body-1">{{ rom.file_name }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="rom.multi" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Files</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-select
|
||||
v-model="downloadStore.filesToDownloadMultiFileRom"
|
||||
:label="rom.file_name"
|
||||
item-title="file_name"
|
||||
:items="rom.files"
|
||||
rounded="0"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
return-object
|
||||
multiple
|
||||
hide-details
|
||||
clearable
|
||||
chips
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row no-gutters class="align-center my-3">
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Size</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip size="small" label>{{
|
||||
formatBytes(rom.file_size_bytes)
|
||||
}}</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="rom.tags.length > 0" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2">
|
||||
<span>Tags</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-for="tag in rom.tags"
|
||||
:key="tag"
|
||||
size="small"
|
||||
class="mr-2"
|
||||
label
|
||||
color="romm-accent-1"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ tag }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
@@ -10,108 +10,116 @@ const galleryFilter = storeGalleryFilter();
|
||||
const show = ref(false);
|
||||
</script>
|
||||
<template>
|
||||
<template v-for="filter in galleryFilter.filters" :key="filter">
|
||||
<template>
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-divider class="mx-2 my-4" />
|
||||
<v-row v-if="rom[filter].length > 0" class="align-center my-3" no-gutters>
|
||||
<v-col cols="3" xl="2" class="text-capitalize">
|
||||
<span>{{ filter }}</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-for="value in rom[filter]"
|
||||
:key="value"
|
||||
class="my-1 mr-2"
|
||||
label
|
||||
>
|
||||
{{ value }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="rom.summary != ''">
|
||||
<v-divider class="mx-2 my-4" />
|
||||
<v-row no-gutters>
|
||||
<v-col class="text-caption">
|
||||
<p v-text="rom.summary" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="rom.merged_screenshots.length > 0">
|
||||
<v-divider class="mx-2 my-4" />
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-carousel
|
||||
hide-delimiter-background
|
||||
delimiter-icon="mdi-square"
|
||||
class="bg-primary"
|
||||
show-arrows="hover"
|
||||
hide-delimiters
|
||||
progress="terciary"
|
||||
:height="xs ? '300' : '400'"
|
||||
<template v-for="filter in galleryFilter.filters" :key="filter">
|
||||
<v-row
|
||||
v-if="rom[filter].length > 0"
|
||||
class="align-center my-3"
|
||||
no-gutters
|
||||
>
|
||||
<template #prev="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-chevron-left"
|
||||
class="translucent-dark"
|
||||
@click="props.onClick"
|
||||
/>
|
||||
</template>
|
||||
<v-carousel-item
|
||||
v-for="screenshot_url in rom.merged_screenshots"
|
||||
:key="screenshot_url"
|
||||
:src="screenshot_url"
|
||||
class="pointer"
|
||||
@click="show = true"
|
||||
>
|
||||
</v-carousel-item>
|
||||
<template #next="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-chevron-right"
|
||||
class="translucent-dark"
|
||||
@click="props.onClick"
|
||||
/>
|
||||
</template>
|
||||
</v-carousel>
|
||||
<v-dialog v-model="show">
|
||||
<v-list-item>
|
||||
<template #append>
|
||||
<v-btn @click="show = false" icon variant="flat" size="large"
|
||||
><v-icon class="text-white text-shadow" size="25">mdi-close</v-icon></v-btn
|
||||
>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-carousel
|
||||
hide-delimiter-background
|
||||
delimiter-icon="mdi-square"
|
||||
show-arrows="hover"
|
||||
hide-delimiters
|
||||
:height="xs ? '500' : '600'"
|
||||
>
|
||||
<template #prev="{ props }">
|
||||
<v-btn
|
||||
@click="props.onClick"
|
||||
icon="mdi-chevron-left"
|
||||
class="translucent-dark"
|
||||
/>
|
||||
</template>
|
||||
<v-carousel-item
|
||||
v-for="screenshot_url in rom.merged_screenshots"
|
||||
:key="screenshot_url"
|
||||
:src="screenshot_url"
|
||||
<v-col cols="3" xl="2" class="text-capitalize">
|
||||
<span>{{ filter }}</span>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-chip
|
||||
v-for="value in rom[filter]"
|
||||
:key="value"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
class="my-1 mr-2"
|
||||
label
|
||||
>
|
||||
</v-carousel-item>
|
||||
<template #next="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-chevron-right"
|
||||
class="translucent-dark"
|
||||
@click="props.onClick"
|
||||
/>
|
||||
</template>
|
||||
</v-carousel>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
{{ value }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="rom.summary != ''">
|
||||
<v-divider class="mx-2 my-4" />
|
||||
<v-row no-gutters>
|
||||
<v-col class="text-caption">
|
||||
<p v-text="rom.summary" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="rom.merged_screenshots.length > 0">
|
||||
<v-divider class="mx-2 my-4" />
|
||||
<v-row no-gutters>
|
||||
<v-col>
|
||||
<v-carousel
|
||||
hide-delimiter-background
|
||||
delimiter-icon="mdi-square"
|
||||
class="bg-primary"
|
||||
show-arrows="hover"
|
||||
hide-delimiters
|
||||
progress="terciary"
|
||||
:height="xs ? '300' : '400'"
|
||||
>
|
||||
<template #prev="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-chevron-left"
|
||||
class="translucent-dark"
|
||||
@click="props.onClick"
|
||||
/>
|
||||
</template>
|
||||
<v-carousel-item
|
||||
v-for="screenshot_url in rom.merged_screenshots"
|
||||
:key="screenshot_url"
|
||||
:src="screenshot_url"
|
||||
class="pointer"
|
||||
@click="show = true"
|
||||
>
|
||||
</v-carousel-item>
|
||||
<template #next="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-chevron-right"
|
||||
class="translucent-dark"
|
||||
@click="props.onClick"
|
||||
/>
|
||||
</template>
|
||||
</v-carousel>
|
||||
<v-dialog v-model="show">
|
||||
<v-list-item>
|
||||
<template #append>
|
||||
<v-btn @click="show = false" icon variant="flat" size="large"
|
||||
><v-icon class="text-white text-shadow" size="25"
|
||||
>mdi-close</v-icon
|
||||
></v-btn
|
||||
>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-carousel
|
||||
hide-delimiter-background
|
||||
delimiter-icon="mdi-square"
|
||||
show-arrows="hover"
|
||||
hide-delimiters
|
||||
:height="xs ? '500' : '600'"
|
||||
>
|
||||
<template #prev="{ props }">
|
||||
<v-btn
|
||||
@click="props.onClick"
|
||||
icon="mdi-chevron-left"
|
||||
class="translucent-dark"
|
||||
/>
|
||||
</template>
|
||||
<v-carousel-item
|
||||
v-for="screenshot_url in rom.merged_screenshots"
|
||||
:key="screenshot_url"
|
||||
:src="screenshot_url"
|
||||
>
|
||||
</v-carousel-item>
|
||||
<template #next="{ props }">
|
||||
<v-btn
|
||||
icon="mdi-chevron-right"
|
||||
class="translucent-dark"
|
||||
@click="props.onClick"
|
||||
/>
|
||||
</template>
|
||||
</v-carousel>
|
||||
</v-dialog>
|
||||
</v-col>
|
||||
</v-row> </template></v-col
|
||||
></v-row>
|
||||
</template>
|
||||
|
||||
@@ -4,45 +4,65 @@ import storeAuth from "@/stores/auth";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { MdEditor, MdPreview } from "md-editor-v3";
|
||||
import "md-editor-v3/lib/style.css";
|
||||
import { ref } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { useTheme } from "vuetify";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const auth = storeAuth();
|
||||
const theme = useTheme();
|
||||
const editingNote = ref(false);
|
||||
const ownNote = ref(
|
||||
props.rom.user_notes?.find((note) => note.user_id === auth.user?.id) ?? {
|
||||
const romUser = ref(
|
||||
props.rom.rom_user ?? {
|
||||
id: null,
|
||||
user_id: auth.user?.id,
|
||||
rom_id: props.rom.id,
|
||||
updated_at: new Date(),
|
||||
raw_markdown: "",
|
||||
is_public: false,
|
||||
note_raw_markdown: "",
|
||||
note_is_public: false,
|
||||
is_main_sibling: false,
|
||||
}
|
||||
);
|
||||
const publicNotes =
|
||||
props.rom.user_notes?.filter((note) => note.user_id !== auth.user?.id) ?? [];
|
||||
|
||||
// Functions
|
||||
function togglePublic() {
|
||||
ownNote.value.is_public = !ownNote.value.is_public;
|
||||
romApi.updateRomNote({
|
||||
romUser.value.note_is_public = !romUser.value.note_is_public;
|
||||
romApi.updateUserRomProps({
|
||||
romId: props.rom.id,
|
||||
rawMarkdown: ownNote.value.raw_markdown,
|
||||
isPublic: ownNote.value.is_public,
|
||||
noteRawMarkdown: romUser.value.note_raw_markdown,
|
||||
noteIsPublic: romUser.value.note_is_public,
|
||||
isMainSibling: romUser.value.is_main_sibling,
|
||||
});
|
||||
}
|
||||
|
||||
function onEditNote() {
|
||||
function editNote() {
|
||||
if (editingNote.value) {
|
||||
romApi.updateRomNote({
|
||||
romApi.updateUserRomProps({
|
||||
romId: props.rom.id,
|
||||
rawMarkdown: ownNote.value.raw_markdown,
|
||||
isPublic: ownNote.value.is_public,
|
||||
noteRawMarkdown: romUser.value.note_raw_markdown,
|
||||
noteIsPublic: romUser.value.note_is_public,
|
||||
isMainSibling: romUser.value.is_main_sibling,
|
||||
});
|
||||
}
|
||||
editingNote.value = !editingNote.value;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.rom,
|
||||
async () => {
|
||||
romUser.value = props.rom.rom_user ?? {
|
||||
id: null,
|
||||
user_id: auth.user?.id,
|
||||
rom_id: props.rom.id,
|
||||
updated_at: new Date(),
|
||||
note_raw_markdown: "",
|
||||
note_is_public: false,
|
||||
is_main_sibling: false,
|
||||
};
|
||||
}
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<v-card rounded="0">
|
||||
@@ -55,7 +75,7 @@ function onEditNote() {
|
||||
location="top"
|
||||
class="tooltip"
|
||||
transition="fade-transition"
|
||||
:text="ownNote.is_public ? 'Make private' : 'Make public'"
|
||||
:text="romUser.note_is_public ? 'Make private' : 'Make public'"
|
||||
open-delay="500"
|
||||
><template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
@@ -64,7 +84,7 @@ function onEditNote() {
|
||||
class="bg-terciary"
|
||||
>
|
||||
<v-icon size="large">
|
||||
{{ ownNote.is_public ? "mdi-eye" : "mdi-eye-off" }}
|
||||
{{ romUser.note_is_public ? "mdi-eye" : "mdi-eye-off" }}
|
||||
</v-icon>
|
||||
</v-btn>
|
||||
</template></v-tooltip
|
||||
@@ -77,7 +97,7 @@ function onEditNote() {
|
||||
open-delay="500"
|
||||
><template #activator="{ props: tooltipProps }">
|
||||
<v-btn
|
||||
@click="onEditNote"
|
||||
@click="editNote"
|
||||
v-bind="tooltipProps"
|
||||
class="bg-terciary"
|
||||
>
|
||||
@@ -94,7 +114,7 @@ function onEditNote() {
|
||||
<v-card-text class="pa-2">
|
||||
<MdEditor
|
||||
v-if="editingNote"
|
||||
v-model="ownNote.raw_markdown"
|
||||
v-model="romUser.note_raw_markdown"
|
||||
:theme="theme.name.value == 'dark' ? 'dark' : 'light'"
|
||||
language="en-US"
|
||||
:preview="false"
|
||||
@@ -103,7 +123,7 @@ function onEditNote() {
|
||||
/>
|
||||
<MdPreview
|
||||
v-else
|
||||
:model-value="ownNote.raw_markdown"
|
||||
:model-value="romUser.note_raw_markdown"
|
||||
:theme="theme.name.value == 'dark' ? 'dark' : 'light'"
|
||||
preview-theme="vuepress"
|
||||
code-theme="github"
|
||||
@@ -111,7 +131,11 @@ function onEditNote() {
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card rounded="0" v-if="publicNotes.length > 0" class="mt-2">
|
||||
<v-card
|
||||
rounded="0"
|
||||
v-if="publicNotes && publicNotes.length > 0"
|
||||
class="mt-2"
|
||||
>
|
||||
<v-card-title class="bg-terciary">
|
||||
<v-list-item class="pl-2 pr-0">
|
||||
<span class="text-h6">Public notes</span>
|
||||
@@ -122,13 +146,13 @@ function onEditNote() {
|
||||
|
||||
<v-card-text class="pa-0">
|
||||
<v-expansion-panels multiple flat rounded="0" variant="accordion">
|
||||
<v-expansion-panel v-for="note in publicNotes" :key="note.id">
|
||||
<v-expansion-panel v-for="note in publicNotes">
|
||||
<v-expansion-panel-title class="bg-terciary">
|
||||
<span class="text-body-1">{{ note.user__username }}</span>
|
||||
<span class="text-body-1">{{ note.username }}</span>
|
||||
</v-expansion-panel-title>
|
||||
<v-expansion-panel-text class="bg-secondary">
|
||||
<MdPreview
|
||||
:model-value="note.raw_markdown"
|
||||
:model-value="note.note_raw_markdown"
|
||||
:theme="theme.name.value == 'dark' ? 'dark' : 'light'"
|
||||
preview-theme="vuepress"
|
||||
code-theme="github"
|
||||
|
||||
@@ -11,14 +11,8 @@ import { useDisplay } from "vuetify";
|
||||
// Props
|
||||
const { xs, smAndUp, mdAndUp } = useDisplay();
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const romRef = ref<DetailedRom>(props.rom);
|
||||
const selectedSaves = ref<SaveSchema[]>([]);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("romUpdated", (romUpdated) => {
|
||||
if (romUpdated?.id === romRef.value.id) {
|
||||
romRef.value.user_saves = romUpdated.user_saves;
|
||||
}
|
||||
});
|
||||
const HEADERS = [
|
||||
{
|
||||
title: "Name",
|
||||
@@ -46,9 +40,9 @@ async function downloasSaves() {
|
||||
}
|
||||
|
||||
function updateDataTablePages() {
|
||||
if (romRef.value.user_saves) {
|
||||
if (props.rom.user_saves) {
|
||||
pageCount.value = Math.ceil(
|
||||
romRef.value.user_saves.length / itemsPerPage.value
|
||||
props.rom.user_saves.length / itemsPerPage.value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +58,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-data-table
|
||||
:items="romRef.user_saves"
|
||||
:items="rom.user_saves"
|
||||
:width="mdAndUp ? '60vw' : '95vw'"
|
||||
:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="PER_PAGE_OPTIONS"
|
||||
@@ -80,7 +74,7 @@ onMounted(() => {
|
||||
<v-btn
|
||||
class="bg-secondary"
|
||||
size="small"
|
||||
@click="emitter?.emit('addSavesDialog', romRef)"
|
||||
@click="emitter?.emit('addSavesDialog', rom)"
|
||||
>
|
||||
<v-icon>mdi-upload</v-icon>
|
||||
</v-btn>
|
||||
@@ -152,7 +146,7 @@ onMounted(() => {
|
||||
</td>
|
||||
</template>
|
||||
<template #no-data
|
||||
><span>No saves found for {{ romRef?.name }}</span></template
|
||||
><span>No saves found for {{ rom.name }}</span></template
|
||||
>
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn-group divided density="compact">
|
||||
|
||||
@@ -11,14 +11,9 @@ import { useDisplay } from "vuetify";
|
||||
// Props
|
||||
const { xs, smAndUp, mdAndUp } = useDisplay();
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const romRef = ref<DetailedRom>(props.rom);
|
||||
const selectedStates = ref<StateSchema[]>([]);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("romUpdated", (romUpdated) => {
|
||||
if (romUpdated?.id === romRef.value.id) {
|
||||
romRef.value.user_states = romUpdated.user_states;
|
||||
}
|
||||
});
|
||||
// emitter?.on("romUpdated", (romUpdated) => {});
|
||||
const HEADERS = [
|
||||
{
|
||||
title: "Name",
|
||||
@@ -46,9 +41,9 @@ async function downloasStates() {
|
||||
}
|
||||
|
||||
function updateDataTablePages() {
|
||||
if (romRef.value.user_states) {
|
||||
if (props.rom.user_states) {
|
||||
pageCount.value = Math.ceil(
|
||||
romRef.value.user_states.length / itemsPerPage.value
|
||||
props.rom.user_states.length / itemsPerPage.value
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +59,7 @@ onMounted(() => {
|
||||
|
||||
<template>
|
||||
<v-data-table
|
||||
:items="romRef.user_states"
|
||||
:items="rom.user_states"
|
||||
:width="mdAndUp ? '60vw' : '95vw'"
|
||||
:items-per-page="itemsPerPage"
|
||||
:items-per-page-options="PER_PAGE_OPTIONS"
|
||||
@@ -80,7 +75,7 @@ onMounted(() => {
|
||||
<v-btn
|
||||
class="bg-secondary"
|
||||
size="small"
|
||||
@click="emitter?.emit('addStatesDialog', romRef)"
|
||||
@click="emitter?.emit('addStatesDialog', rom)"
|
||||
>
|
||||
<v-icon>mdi-upload</v-icon>
|
||||
</v-btn>
|
||||
@@ -152,7 +147,7 @@ onMounted(() => {
|
||||
</td>
|
||||
</template>
|
||||
<template #no-data
|
||||
><span>No states found for {{ romRef?.name }}</span></template
|
||||
><span>No states found for {{ rom.name }}</span></template
|
||||
>
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn-group divided density="compact">
|
||||
|
||||
@@ -31,9 +31,11 @@ function updateVersion() {
|
||||
<v-select
|
||||
v-model="version"
|
||||
label="Version"
|
||||
variant="outlined"
|
||||
single-line
|
||||
rounded="0"
|
||||
variant="solo-filled"
|
||||
density="compact"
|
||||
class="version-select"
|
||||
max-width="fit-content"
|
||||
hide-details
|
||||
:items="
|
||||
[rom, ...rom.sibling_roms].map((i) => ({
|
||||
@@ -44,9 +46,3 @@ function updateVersion() {
|
||||
@update:model-value="updateVersion"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.version-select {
|
||||
max-width: fit-content;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -145,6 +145,7 @@ onMounted(() => {
|
||||
<v-img
|
||||
:src="`/assets/default/cover/big_${theme.global.name.value}_missing_cover.png`"
|
||||
cover
|
||||
:aspect-ratio="2 / 3"
|
||||
></v-img>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
|
||||
@@ -214,7 +214,7 @@ onBeforeUnmount(() => {
|
||||
cover
|
||||
>
|
||||
<template #error>
|
||||
<v-img :src="resource.url" cover></v-img>
|
||||
<v-img :src="resource.url" cover :aspect-ratio="2 / 3"></v-img>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<div
|
||||
|
||||
@@ -166,18 +166,21 @@ async function deleteRoms({
|
||||
});
|
||||
}
|
||||
|
||||
async function updateRomNote({
|
||||
async function updateUserRomProps({
|
||||
romId,
|
||||
rawMarkdown,
|
||||
isPublic,
|
||||
noteRawMarkdown,
|
||||
noteIsPublic,
|
||||
isMainSibling,
|
||||
}: {
|
||||
romId: number;
|
||||
rawMarkdown: string;
|
||||
isPublic: boolean;
|
||||
noteRawMarkdown: string;
|
||||
noteIsPublic: boolean;
|
||||
isMainSibling: boolean;
|
||||
}): Promise<{ data: DetailedRom }> {
|
||||
return api.put(`/roms/${romId}/note`, {
|
||||
raw_markdown: rawMarkdown,
|
||||
is_public: isPublic,
|
||||
return api.put(`/roms/${romId}/props`, {
|
||||
note_raw_markdown: noteRawMarkdown,
|
||||
note_is_public: noteIsPublic,
|
||||
is_main_sibling: isMainSibling,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -191,5 +194,5 @@ export default {
|
||||
searchCover,
|
||||
updateRom,
|
||||
deleteRoms,
|
||||
updateRomNote,
|
||||
updateUserRomProps,
|
||||
};
|
||||
|
||||
@@ -60,10 +60,28 @@ export default defineStore("roms", {
|
||||
game.igdb_id || game.moby_id || nanoid(),
|
||||
),
|
||||
)
|
||||
.map((games) => ({
|
||||
...(games.shift() as SimpleRom),
|
||||
siblings: games,
|
||||
}))
|
||||
.map((games) => {
|
||||
// Find the index of the game where the 'rom_user' property has 'is_main_sibling' set to true.
|
||||
// If such a game is found, 'mainSiblingIndex' will be its index, otherwise it will be -1.
|
||||
const mainSiblingIndex = games.findIndex(
|
||||
(game) => game.rom_user?.is_main_sibling,
|
||||
);
|
||||
|
||||
// Determine the primary game:
|
||||
// - If 'mainSiblingIndex' is not -1 (i.e., a main sibling game was found),
|
||||
// remove that game from the 'games' array and set it as 'primaryGame'.
|
||||
// - If no main sibling game was found ('mainSiblingIndex' is -1),
|
||||
// remove the first game from the 'games' array and set it as 'primaryGame'.
|
||||
const primaryGame =
|
||||
mainSiblingIndex !== -1
|
||||
? games.splice(mainSiblingIndex, 1)[0]
|
||||
: games.shift();
|
||||
|
||||
return {
|
||||
...(primaryGame as SimpleRom),
|
||||
siblings: games,
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
return a.sort_comparator.localeCompare(b.sort_comparator);
|
||||
});
|
||||
|
||||
@@ -215,12 +215,14 @@ onMounted(async () => {
|
||||
} else {
|
||||
romsStore.setCurrentPlatform(routePlatform);
|
||||
}
|
||||
resetGallery();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
|
||||
window.addEventListener("wheel", onScroll);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
if (!noPlatformError.value) {
|
||||
resetGallery();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
window.addEventListener("wheel", onScroll);
|
||||
window.addEventListener("scroll", onScroll);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeRouteUpdate(async (to, from) => {
|
||||
|
||||
@@ -172,8 +172,10 @@ watch(
|
||||
<v-col cols="12">
|
||||
<v-window disabled v-model="tab" class="py-2">
|
||||
<v-window-item value="details">
|
||||
<file-info :rom="rom" :platform="platform" />
|
||||
<game-info :rom="rom" />
|
||||
<v-row no-gutters :class="{ 'mx-2': mdAndUp }">
|
||||
<file-info :rom="rom" :platform="platform" />
|
||||
<game-info :rom="rom" />
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="saves">
|
||||
<saves :rom="rom" />
|
||||
|
||||
@@ -116,6 +116,7 @@ onMounted(async () => {
|
||||
:disabled="gameRunning"
|
||||
v-model="coreRef"
|
||||
class="my-1"
|
||||
rounded="0"
|
||||
hide-details
|
||||
variant="outlined"
|
||||
clearable
|
||||
@@ -132,6 +133,7 @@ onMounted(async () => {
|
||||
:disabled="gameRunning"
|
||||
class="my-1"
|
||||
hide-details
|
||||
rounded="0"
|
||||
variant="outlined"
|
||||
clearable
|
||||
label="BIOS"
|
||||
@@ -149,6 +151,7 @@ onMounted(async () => {
|
||||
hide-details
|
||||
variant="outlined"
|
||||
clearable
|
||||
rounded="0"
|
||||
label="Save"
|
||||
:items="
|
||||
rom.user_saves?.map((s) => ({
|
||||
@@ -163,6 +166,7 @@ onMounted(async () => {
|
||||
:disabled="gameRunning"
|
||||
class="my-1"
|
||||
hide-details
|
||||
rounded="0"
|
||||
variant="outlined"
|
||||
clearable
|
||||
label="State"
|
||||
@@ -181,6 +185,7 @@ onMounted(async () => {
|
||||
hide-details
|
||||
variant="outlined"
|
||||
clearable
|
||||
rounded="0"
|
||||
disabled
|
||||
label="Patch"
|
||||
:items="[
|
||||
@@ -192,30 +197,43 @@ onMounted(async () => {
|
||||
</v-row>
|
||||
<v-row class="px-3 py-3 text-center" no-gutters>
|
||||
<v-col>
|
||||
<v-chip
|
||||
@click="onFullScreenChange"
|
||||
:disabled="gameRunning"
|
||||
:variant="fullScreenOnPlay ? 'flat' : 'outlined'"
|
||||
:color="fullScreenOnPlay ? 'romm-accent-1' : ''"
|
||||
><v-icon class="mr-1">{{
|
||||
fullScreenOnPlay
|
||||
? "mdi-checkbox-outline"
|
||||
: "mdi-checkbox-blank-outline"
|
||||
}}</v-icon
|
||||
>Full screen</v-chip
|
||||
>
|
||||
<v-divider class="my-4" />
|
||||
<v-btn
|
||||
color="romm-accent-1"
|
||||
block
|
||||
:disabled="gameRunning"
|
||||
rounded="0"
|
||||
variant="outlined"
|
||||
size="large"
|
||||
prepend-icon="mdi-play"
|
||||
@click="onPlay()"
|
||||
>Play
|
||||
</v-btn>
|
||||
<v-row class="align-center" no-gutters>
|
||||
<v-col>
|
||||
<v-btn
|
||||
block
|
||||
size="large"
|
||||
rounded="0"
|
||||
@click="onFullScreenChange"
|
||||
:disabled="gameRunning"
|
||||
:variant="fullScreenOnPlay ? 'flat' : 'outlined'"
|
||||
:color="fullScreenOnPlay ? 'romm-accent-1' : ''"
|
||||
><v-icon class="mr-1">{{
|
||||
fullScreenOnPlay
|
||||
? "mdi-checkbox-outline"
|
||||
: "mdi-checkbox-blank-outline"
|
||||
}}</v-icon
|
||||
>Full screen</v-btn
|
||||
>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
:sm="gameRunning ? 12 : 7"
|
||||
:xl="gameRunning ? 12 : 9"
|
||||
>
|
||||
<v-btn
|
||||
color="romm-accent-1"
|
||||
block
|
||||
:disabled="gameRunning"
|
||||
rounded="0"
|
||||
variant="outlined"
|
||||
size="large"
|
||||
prepend-icon="mdi-play"
|
||||
@click="onPlay()"
|
||||
>Play
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-btn
|
||||
class="mt-4"
|
||||
block
|
||||
|
||||
Reference in New Issue
Block a user