mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge branch 'master' into ui-theme-redesign
This commit is contained in:
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
1
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,6 @@
|
||||
name: Bug report
|
||||
about: Report a bug, issue or problem
|
||||
title: "[Bug] Bug title"
|
||||
labels: bug
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/custom.md
vendored
1
.github/ISSUE_TEMPLATE/custom.md
vendored
@@ -2,6 +2,5 @@
|
||||
name: Custom issue template
|
||||
about: Describe this issue template's purpose here.
|
||||
title: "[Other] Custom issue title"
|
||||
labels: other
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
1
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,6 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature] Feature title"
|
||||
labels: feature
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE/config.yml
vendored
1
.github/PULL_REQUEST_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_pull_request_template_enabled: false
|
||||
16
.github/dependabot.yml
vendored
Normal file
16
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "gantoine"
|
||||
target-branch: "master"
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
reviewers:
|
||||
- "adamantike"
|
||||
target-branch: "master"
|
||||
2
.github/workflows/pytest.yml
vendored
2
.github/workflows/pytest.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --sync
|
||||
poetry sync --extras test
|
||||
|
||||
- name: Initiate database
|
||||
run: |
|
||||
|
||||
@@ -45,14 +45,14 @@ Then create the virtual environment
|
||||
```sh
|
||||
# Fix disable parallel installation stuck: $> poetry config experimental.new-installer false
|
||||
# Fix Loading macOS/linux stuck: $> export PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring
|
||||
poetry install --sync
|
||||
poetry sync
|
||||
```
|
||||
|
||||
If you are on Arch Linux or another Arch-based distro, you need to run the command as follows:
|
||||
|
||||
```sh
|
||||
# https://bbs.archlinux.org/viewtopic.php?id=296542
|
||||
CFLAGS="-Wno-error=incompatible-pointer-types" poetry install --sync
|
||||
CFLAGS="-Wno-error=incompatible-pointer-types" poetry sync
|
||||
```
|
||||
|
||||
#### - Spin up mariadb in docker
|
||||
|
||||
@@ -9,7 +9,7 @@ Create Date: 2023-09-12 18:18:27.158732
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from utils.database import CustomJSON
|
||||
from utils.database import CustomJSON, is_postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0009_models_refactor"
|
||||
@@ -21,6 +21,10 @@ depends_on = None
|
||||
def upgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
|
||||
json_array_build_func = (
|
||||
"jsonb_build_array()" if is_postgresql(connection) else "JSON_ARRAY()"
|
||||
)
|
||||
|
||||
try:
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
@@ -87,13 +91,13 @@ def upgrade() -> None:
|
||||
"url_screenshots",
|
||||
existing_type=CustomJSON(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text("(JSON_ARRAY())"),
|
||||
existing_server_default=sa.text(f"({json_array_build_func})"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"path_screenshots",
|
||||
existing_type=CustomJSON(),
|
||||
nullable=True,
|
||||
existing_server_default=sa.text("(JSON_ARRAY())"),
|
||||
existing_server_default=sa.text(f"({json_array_build_func})"),
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -108,6 +112,10 @@ def upgrade() -> None:
|
||||
def downgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
|
||||
json_array_build_func = (
|
||||
"jsonb_build_array()" if is_postgresql(connection) else "JSON_ARRAY()"
|
||||
)
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"igdb_id",
|
||||
@@ -136,13 +144,13 @@ def downgrade() -> None:
|
||||
"path_screenshots",
|
||||
existing_type=CustomJSON(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text("(JSON_ARRAY())"),
|
||||
existing_server_default=sa.text(f"({json_array_build_func})"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"url_screenshots",
|
||||
existing_type=CustomJSON(),
|
||||
nullable=False,
|
||||
existing_server_default=sa.text("(JSON_ARRAY())"),
|
||||
existing_server_default=sa.text(f"({json_array_build_func})"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"file_size_units", existing_type=sa.VARCHAR(length=10), nullable=True
|
||||
|
||||
@@ -18,19 +18,22 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("regions", CustomJSON(), nullable=True))
|
||||
batch_op.add_column(sa.Column("languages", CustomJSON(), nullable=True))
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
# Set default values for languages and regions
|
||||
batch_op.execute("UPDATE roms SET languages = JSON_ARRAY()")
|
||||
batch_op.execute("UPDATE roms SET regions = JSON_ARRAY(region)")
|
||||
if is_postgresql(connection):
|
||||
batch_op.execute("UPDATE roms SET languages = jsonb_build_array()")
|
||||
batch_op.execute("UPDATE roms SET regions = jsonb_build_array(region)")
|
||||
else:
|
||||
batch_op.execute("UPDATE roms SET languages = JSON_ARRAY()")
|
||||
batch_op.execute("UPDATE roms SET regions = JSON_ARRAY(region)")
|
||||
batch_op.drop_column("region")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
connection = op.get_bind()
|
||||
|
||||
@@ -191,7 +191,10 @@ def upgrade() -> None:
|
||||
|
||||
# Move data around
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.execute("update roms set igdb_metadata = JSON_OBJECT()")
|
||||
if is_postgresql(connection):
|
||||
batch_op.execute("update roms set igdb_metadata = jsonb_build_object()")
|
||||
else:
|
||||
batch_op.execute("update roms set igdb_metadata = JSON_OBJECT()")
|
||||
batch_op.execute(
|
||||
"update roms set path_cover_s = '', path_cover_l = '', url_cover = '' where url_cover = 'https://images.igdb.com/igdb/image/upload/t_cover_big/nocover.png'"
|
||||
)
|
||||
|
||||
@@ -8,7 +8,7 @@ Create Date: 2024-02-13 17:57:25.936825
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from utils.database import CustomJSON
|
||||
from utils.database import CustomJSON, is_postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0015_mobygames_data"
|
||||
@@ -18,7 +18,8 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column("moby_id", sa.Integer(), nullable=True))
|
||||
|
||||
@@ -27,9 +28,10 @@ def upgrade() -> None:
|
||||
batch_op.add_column(sa.Column("moby_metadata", CustomJSON(), nullable=True))
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.execute("update roms set moby_metadata = JSON_OBJECT()")
|
||||
|
||||
# ### end Alembic commands ###
|
||||
if is_postgresql(connection):
|
||||
batch_op.execute("update roms set moby_metadata = jsonb_build_object()")
|
||||
else:
|
||||
batch_op.execute("update roms set moby_metadata = JSON_OBJECT()")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
25
backend/alembic/versions/0030_user_email_null.py
Normal file
25
backend/alembic/versions/0030_user_email_null.py
Normal file
@@ -0,0 +1,25 @@
|
||||
"""Change empty string in users.email to NULL.
|
||||
|
||||
Revision ID: 951473b0c581
|
||||
Revises: 0029_platforms_custom_name
|
||||
Create Date: 2025-01-14 01:30:39.696257
|
||||
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0030_user_email_null"
|
||||
down_revision = "0029_platforms_custom_name"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.execute("UPDATE users SET email = NULL WHERE email = ''")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.execute("UPDATE users SET email = '' WHERE email IS NULL")
|
||||
351
backend/alembic/versions/0031_datetime_to_timestamp.py
Normal file
351
backend/alembic/versions/0031_datetime_to_timestamp.py
Normal file
@@ -0,0 +1,351 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0031_datetime_to_timestamp
|
||||
Revises: 0030_user_email_null
|
||||
Create Date: 2025-01-14 04:13:33.209508
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0031_datetime_to_timestamp"
|
||||
down_revision = "0030_user_email_null"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("collections", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("firmware", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("rom_user", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"last_played",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("saves", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("screenshots", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("states", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"last_login",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"last_active",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=mysql.DATETIME(),
|
||||
type_=sa.TIMESTAMP(timezone=True),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("users", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"last_active",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"last_login",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
with op.batch_alter_table("states", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("screenshots", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("saves", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("roms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("rom_user", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"last_played",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=True,
|
||||
)
|
||||
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("firmware", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
with op.batch_alter_table("collections", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"updated_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"created_at",
|
||||
existing_type=sa.TIMESTAMP(timezone=True),
|
||||
type_=mysql.DATETIME(),
|
||||
existing_nullable=False,
|
||||
existing_server_default=sa.text("now()"),
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
66
backend/alembic/versions/0032_longer_fs_fields.py
Normal file
66
backend/alembic/versions/0032_longer_fs_fields.py
Normal file
@@ -0,0 +1,66 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 0032_longer_fs_fields
|
||||
Revises: 0031_datetime_to_timestamp
|
||||
Create Date: 2025-01-24 02:18:30.069263
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import mysql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "0032_longer_fs_fields"
|
||||
down_revision = "0031_datetime_to_timestamp"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"slug",
|
||||
existing_type=mysql.VARCHAR(length=50),
|
||||
type_=sa.String(length=100),
|
||||
existing_nullable=False,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"fs_slug",
|
||||
existing_type=mysql.VARCHAR(length=50),
|
||||
type_=sa.String(length=100),
|
||||
existing_nullable=False,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"category",
|
||||
existing_type=mysql.VARCHAR(length=50),
|
||||
type_=sa.String(length=100),
|
||||
existing_nullable=True,
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table("platforms", schema=None) as batch_op:
|
||||
batch_op.alter_column(
|
||||
"category",
|
||||
existing_type=sa.String(length=100),
|
||||
type_=mysql.VARCHAR(length=50),
|
||||
existing_nullable=True,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"fs_slug",
|
||||
existing_type=sa.String(length=100),
|
||||
type_=mysql.VARCHAR(length=50),
|
||||
existing_nullable=False,
|
||||
)
|
||||
batch_op.alter_column(
|
||||
"slug",
|
||||
existing_type=sa.String(length=100),
|
||||
type_=mysql.VARCHAR(length=50),
|
||||
existing_nullable=False,
|
||||
)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
@@ -8,7 +8,7 @@ Create Date: 2023-04-17 12:03:19.163501
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from utils.database import CustomJSON
|
||||
from utils.database import CustomJSON, is_postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "1.8.1"
|
||||
@@ -18,14 +18,19 @@ depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
connection = op.get_bind()
|
||||
|
||||
json_array_build_func = (
|
||||
"jsonb_build_array()" if is_postgresql(connection) else "JSON_ARRAY()"
|
||||
)
|
||||
|
||||
with op.batch_alter_table("roms") as batch_op:
|
||||
batch_op.add_column(
|
||||
sa.Column(
|
||||
"url_screenshots",
|
||||
CustomJSON(),
|
||||
nullable=False,
|
||||
server_default=sa.text("(JSON_ARRAY())"),
|
||||
server_default=sa.text(f"({json_array_build_func})"),
|
||||
)
|
||||
)
|
||||
batch_op.add_column(
|
||||
@@ -33,10 +38,9 @@ def upgrade() -> None:
|
||||
"path_screenshots",
|
||||
CustomJSON(),
|
||||
nullable=False,
|
||||
server_default=sa.text("(JSON_ARRAY())"),
|
||||
server_default=sa.text(f"({json_array_build_func})"),
|
||||
)
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
|
||||
@@ -117,7 +117,7 @@ DISABLE_RUFFLE_RS = str_to_bool(os.environ.get("DISABLE_RUFFLE_RS", "false"))
|
||||
UPLOAD_TIMEOUT = int(os.environ.get("UPLOAD_TIMEOUT", 600))
|
||||
|
||||
# LOGGING
|
||||
LOGLEVEL: Final = os.environ.get("LOGLEVEL", "INFO")
|
||||
LOGLEVEL: Final = os.environ.get("LOGLEVEL", "INFO").upper()
|
||||
FORCE_COLOR: Final = str_to_bool(os.environ.get("FORCE_COLOR", "false"))
|
||||
NO_COLOR: Final = str_to_bool(os.environ.get("NO_COLOR", "false"))
|
||||
|
||||
|
||||
@@ -283,6 +283,18 @@ async def get_rom_content(
|
||||
log.info(f"User {current_username} is downloading {rom.file_name}")
|
||||
|
||||
if not rom.multi:
|
||||
# Serve the file directly in development mode for emulatorjs
|
||||
if DEV_MODE:
|
||||
return FileResponse(
|
||||
path=rom_path,
|
||||
filename=rom.file_name,
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{quote(rom.file_name)}"',
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Length": str(rom.file_size_bytes),
|
||||
},
|
||||
)
|
||||
|
||||
return FileRedirectResponse(
|
||||
download_path=Path(f"/library/{rom.full_path}"),
|
||||
filename=rom.file_name,
|
||||
@@ -377,8 +389,8 @@ async def update_rom(
|
||||
)
|
||||
|
||||
cleaned_data = {
|
||||
"igdb_id": data.get("igdb_id", None),
|
||||
"moby_id": data.get("moby_id", None),
|
||||
"igdb_id": data.get("igdb_id", rom.igdb_id),
|
||||
"moby_id": data.get("moby_id", rom.moby_id),
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -558,7 +570,7 @@ async def delete_roms(
|
||||
@protected_route(router.put, "/roms/{id}/props", [Scope.ROMS_USER_WRITE])
|
||||
async def update_rom_user(request: Request, id: int) -> RomUserSchema:
|
||||
data = await request.json()
|
||||
data = data.get("data", {})
|
||||
rom_user_data = data.get("data", {})
|
||||
|
||||
rom = db_rom_handler.get_rom(id)
|
||||
|
||||
@@ -580,10 +592,13 @@ async def update_rom_user(request: Request, id: int) -> RomUserSchema:
|
||||
"difficulty",
|
||||
"completion",
|
||||
"status",
|
||||
"last_played",
|
||||
]
|
||||
|
||||
cleaned_data = {field: data[field] for field in fields_to_update if field in data}
|
||||
cleaned_data = {
|
||||
field: rom_user_data[field]
|
||||
for field in fields_to_update
|
||||
if field in rom_user_data
|
||||
}
|
||||
|
||||
if data.get("update_last_played", False):
|
||||
cleaned_data.update({"last_played": datetime.now(timezone.utc)})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Annotated
|
||||
from typing import Annotated, Any
|
||||
|
||||
from anyio import open_file
|
||||
from config import ASSETS_BASE_PATH
|
||||
@@ -51,8 +51,7 @@ def add_user(
|
||||
detail="Forbidden",
|
||||
)
|
||||
|
||||
existing_user_by_username = db_user_handler.get_user_by_username(username.lower())
|
||||
if existing_user_by_username:
|
||||
if db_user_handler.get_user_by_username(username.lower()):
|
||||
msg = f"Username {username.lower()} already exists"
|
||||
log.error(msg)
|
||||
raise HTTPException(
|
||||
@@ -60,9 +59,8 @@ def add_user(
|
||||
detail=msg,
|
||||
)
|
||||
|
||||
existing_user_by_email = db_user_handler.get_user_by_email(email.lower())
|
||||
if existing_user_by_email:
|
||||
msg = f"Uesr with email {email.lower()} already exists"
|
||||
if email and db_user_handler.get_user_by_email(email.lower()):
|
||||
msg = f"User with email {email.lower()} already exists"
|
||||
log.error(msg)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -72,7 +70,7 @@ def add_user(
|
||||
user = User(
|
||||
username=username.lower(),
|
||||
hashed_password=auth_handler.get_password_hash(password),
|
||||
email=email.lower(),
|
||||
email=email.lower() or None,
|
||||
role=Role[role.upper()],
|
||||
)
|
||||
|
||||
@@ -154,7 +152,7 @@ async def update_user(
|
||||
if db_user.id != request.user.id and request.user.role != Role.ADMIN:
|
||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Forbidden")
|
||||
|
||||
cleaned_data = {}
|
||||
cleaned_data: dict[str, Any] = {}
|
||||
|
||||
if form_data.username and form_data.username != db_user.username:
|
||||
existing_user = db_user_handler.get_user_by_username(form_data.username.lower())
|
||||
@@ -173,9 +171,10 @@ async def update_user(
|
||||
form_data.password
|
||||
)
|
||||
|
||||
if form_data.email and form_data.email != db_user.email:
|
||||
existing_user = db_user_handler.get_user_by_email(form_data.email.lower())
|
||||
if existing_user:
|
||||
if form_data.email is not None and form_data.email != db_user.email:
|
||||
if form_data.email and db_user_handler.get_user_by_email(
|
||||
form_data.email.lower()
|
||||
):
|
||||
msg = f"User with email {form_data.email} already exists"
|
||||
log.error(msg)
|
||||
raise HTTPException(
|
||||
@@ -183,7 +182,7 @@ async def update_user(
|
||||
detail=msg,
|
||||
)
|
||||
|
||||
cleaned_data["email"] = form_data.email.lower()
|
||||
cleaned_data["email"] = form_data.email.lower() or None
|
||||
|
||||
# You can't change your own role
|
||||
if form_data.role and request.user.id != id:
|
||||
|
||||
@@ -5,17 +5,26 @@ from joserfc import jwt
|
||||
from joserfc.errors import BadSignatureError
|
||||
from joserfc.jwk import OctKey
|
||||
from starlette.datastructures import MutableHeaders, Secret
|
||||
from starlette.requests import HTTPConnection
|
||||
from starlette.requests import HTTPConnection, Request
|
||||
from starlette.types import ASGIApp, Message, Receive, Scope, Send
|
||||
from starlette_csrf.middleware import CSRFMiddleware
|
||||
|
||||
|
||||
class CustomCSRFMiddleware(CSRFMiddleware):
|
||||
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
||||
# Skip CSRF check if not an HTTP request, like websockets
|
||||
if scope["type"] != "http":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
request = Request(scope, receive)
|
||||
|
||||
# Skip CSRF check if Authorization header is present
|
||||
auth_scheme = request.headers.get("Authorization", "").split(" ", 1)[0].lower()
|
||||
if auth_scheme == "bearer" or auth_scheme == "basic":
|
||||
await self.app(scope, receive, send)
|
||||
return
|
||||
|
||||
await super().__call__(scope, receive, send)
|
||||
|
||||
|
||||
|
||||
@@ -276,18 +276,19 @@ class DBRomsHandler(DBBaseHandler):
|
||||
|
||||
rom_user = self.get_rom_user_by_id(id)
|
||||
|
||||
if data.get("is_main_sibling", False):
|
||||
rom = self.get_rom(rom_user.rom_id)
|
||||
if not data.get("is_main_sibling", False):
|
||||
return rom_user
|
||||
|
||||
session.execute(
|
||||
update(RomUser)
|
||||
.where(
|
||||
and_(
|
||||
RomUser.rom_id.in_(r.id for r in rom.sibling_roms),
|
||||
RomUser.user_id == rom_user.user_id,
|
||||
)
|
||||
rom = self.get_rom(rom_user.rom_id)
|
||||
session.execute(
|
||||
update(RomUser)
|
||||
.where(
|
||||
and_(
|
||||
RomUser.rom_id.in_(r.id for r in rom.sibling_roms),
|
||||
RomUser.user_id == rom_user.user_id,
|
||||
)
|
||||
.values(is_main_sibling=False)
|
||||
)
|
||||
.values(is_main_sibling=False)
|
||||
)
|
||||
|
||||
return self.get_rom_user_by_id(id)
|
||||
|
||||
@@ -46,7 +46,7 @@ NON_HASHABLE_PLATFORMS = frozenset(
|
||||
"switch",
|
||||
"wiiu",
|
||||
"win",
|
||||
"xbox-360",
|
||||
"xbox360",
|
||||
"xboxone",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import socketio # type: ignore
|
||||
from config import REDIS_URL
|
||||
from utils import json as json_module
|
||||
|
||||
|
||||
class SocketHandler:
|
||||
@@ -7,6 +8,7 @@ class SocketHandler:
|
||||
self.socket_server = socketio.AsyncServer(
|
||||
cors_allowed_origins="*",
|
||||
async_mode="asgi",
|
||||
json=json_module,
|
||||
logger=False,
|
||||
engineio_logger=False,
|
||||
client_manager=socketio.AsyncRedisManager(str(REDIS_URL)),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, func
|
||||
from sqlalchemy import TIMESTAMP, func
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
||||
|
||||
|
||||
class BaseModel(DeclarativeBase):
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now()
|
||||
TIMESTAMP(timezone=True), server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
@@ -21,11 +21,11 @@ class Platform(BaseModel):
|
||||
igdb_id: Mapped[int | None]
|
||||
sgdb_id: Mapped[int | None]
|
||||
moby_id: Mapped[int | None]
|
||||
slug: Mapped[str] = mapped_column(String(length=50))
|
||||
fs_slug: Mapped[str] = mapped_column(String(length=50))
|
||||
slug: Mapped[str] = mapped_column(String(length=100))
|
||||
fs_slug: Mapped[str] = mapped_column(String(length=100))
|
||||
name: Mapped[str] = mapped_column(String(length=400))
|
||||
custom_name: Mapped[str | None] = mapped_column(String(length=400), default="")
|
||||
category: Mapped[str | None] = mapped_column(String(length=50), default="")
|
||||
category: Mapped[str | None] = mapped_column(String(length=100), default="")
|
||||
generation: Mapped[int | None]
|
||||
family_name: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
family_slug: Mapped[str | None] = mapped_column(String(length=1000), default="")
|
||||
|
||||
@@ -8,8 +8,8 @@ from typing import TYPE_CHECKING, Any, TypedDict
|
||||
from config import FRONTEND_RESOURCES_PATH
|
||||
from models.base import BaseModel
|
||||
from sqlalchemy import (
|
||||
TIMESTAMP,
|
||||
BigInteger,
|
||||
DateTime,
|
||||
Enum,
|
||||
ForeignKey,
|
||||
Index,
|
||||
@@ -72,7 +72,7 @@ class Rom(BaseModel):
|
||||
Text, default="", doc="URL to cover image stored in IGDB"
|
||||
)
|
||||
|
||||
revision: Mapped[str | None] = mapped_column(String(100))
|
||||
revision: Mapped[str | None] = mapped_column(String(length=100))
|
||||
regions: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[])
|
||||
languages: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[])
|
||||
tags: Mapped[list[str] | None] = mapped_column(CustomJSON(), default=[])
|
||||
@@ -84,9 +84,9 @@ class Rom(BaseModel):
|
||||
|
||||
multi: Mapped[bool] = mapped_column(default=False)
|
||||
files: Mapped[list[RomFile] | None] = mapped_column(CustomJSON(), default=[])
|
||||
crc_hash: Mapped[str | None] = mapped_column(String(100))
|
||||
md5_hash: Mapped[str | None] = mapped_column(String(100))
|
||||
sha1_hash: Mapped[str | None] = mapped_column(String(100))
|
||||
crc_hash: Mapped[str | None] = mapped_column(String(length=100))
|
||||
md5_hash: Mapped[str | None] = mapped_column(String(length=100))
|
||||
sha1_hash: Mapped[str | None] = mapped_column(String(length=100))
|
||||
|
||||
platform_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("platforms.id", ondelete="CASCADE")
|
||||
@@ -254,7 +254,7 @@ class RomUser(BaseModel):
|
||||
note_is_public: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
is_main_sibling: Mapped[bool] = mapped_column(default=False)
|
||||
last_played: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_played: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
|
||||
|
||||
backlogged: Mapped[bool] = mapped_column(default=False)
|
||||
now_playing: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from handler.auth.constants import DEFAULT_SCOPES, FULL_SCOPES, WRITE_SCOPES, Scope
|
||||
from models.base import BaseModel
|
||||
from sqlalchemy import DateTime, Enum, String
|
||||
from sqlalchemy import TIMESTAMP, Enum, String
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
from starlette.authentication import SimpleUser
|
||||
|
||||
@@ -36,8 +36,8 @@ class User(BaseModel, SimpleUser):
|
||||
enabled: Mapped[bool] = mapped_column(default=True)
|
||||
role: Mapped[Role] = mapped_column(Enum(Role), default=Role.VIEWER)
|
||||
avatar_path: Mapped[str] = mapped_column(String(length=255), default="")
|
||||
last_login: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_active: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
|
||||
last_login: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
|
||||
last_active: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
|
||||
|
||||
saves: Mapped[list[Save]] = relationship(back_populates="user")
|
||||
states: Mapped[list[State]] = relationship(back_populates="user")
|
||||
|
||||
@@ -31,7 +31,7 @@ def json_array_contains_value(
|
||||
return func.json_contains(column, value)
|
||||
|
||||
|
||||
def safe_float(value, default=0.0):
|
||||
def safe_float(value: Any, default: float = 0.0) -> float:
|
||||
"""Safely convert a value to float, returning default if conversion fails."""
|
||||
try:
|
||||
return float(value)
|
||||
@@ -39,7 +39,7 @@ def safe_float(value, default=0.0):
|
||||
return default
|
||||
|
||||
|
||||
def safe_int(value, default=0):
|
||||
def safe_int(value: Any, default: int = 0) -> int:
|
||||
"""Safely convert a value to int, returning default if conversion fails."""
|
||||
try:
|
||||
return int(value)
|
||||
|
||||
29
backend/utils/json.py
Normal file
29
backend/utils/json.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""JSON-compatible module with sane defaults.
|
||||
|
||||
Inspiration taken from `python-engineio`.
|
||||
https://github.com/miguelgrinberg/python-engineio/blob/main/src/engineio/json.py
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import decimal
|
||||
import json
|
||||
import uuid
|
||||
from json import * # noqa: F401, F403
|
||||
from json import dumps as __original_dumps
|
||||
from typing import Any
|
||||
|
||||
|
||||
class DefaultJSONEncoder(json.JSONEncoder):
|
||||
"""Custom JSON encoder that supports encoding additional types."""
|
||||
|
||||
def default(self, o: Any) -> Any:
|
||||
if isinstance(o, (datetime.date, datetime.datetime, datetime.time)):
|
||||
return o.isoformat()
|
||||
if isinstance(o, (decimal.Decimal, uuid.UUID)):
|
||||
return str(o)
|
||||
return super().default(o)
|
||||
|
||||
|
||||
def dumps(*args: Any, **kwargs: Any) -> str: # type: ignore[no-redef]
|
||||
kwargs.setdefault("cls", DefaultJSONEncoder)
|
||||
return __original_dumps(*args, **kwargs)
|
||||
@@ -1,10 +1,10 @@
|
||||
import sentry_sdk
|
||||
from config import SENTRY_DSN
|
||||
from handler.redis_handler import redis_client
|
||||
from rq import Connection, Queue, Worker
|
||||
from rq import Queue, Worker
|
||||
from utils import get_version
|
||||
|
||||
listen = ["high", "default", "low"]
|
||||
listen = ("high", "default", "low")
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=SENTRY_DSN,
|
||||
@@ -14,6 +14,5 @@ sentry_sdk.init(
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Start the worker
|
||||
with Connection(redis_client):
|
||||
worker = Worker(map(Queue, listen))
|
||||
worker.work()
|
||||
worker = Worker([Queue(name, connection=redis_client) for name in listen])
|
||||
worker.work()
|
||||
|
||||
@@ -55,7 +55,7 @@ RUN poetry install --no-ansi --no-cache --only main
|
||||
|
||||
FROM backend-build AS backend-dev-build
|
||||
|
||||
RUN poetry install --no-ansi --no-cache
|
||||
RUN poetry install --no-ansi --no-cache --all-extras
|
||||
|
||||
|
||||
# TODO: Upgrade Alpine to the same version as the other stages, when RAHasher is updated to work
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
__generated__
|
||||
*.config.js
|
||||
@@ -1,16 +1,30 @@
|
||||
/* eslint-disable */
|
||||
import eslint from "@eslint/js";
|
||||
import tseslint from "typescript-eslint";
|
||||
import globals from "globals";
|
||||
import vue from "eslint-plugin-vue";
|
||||
|
||||
let eslint = require("@eslint/js");
|
||||
let tseslint = require("typescript-eslint");
|
||||
let globals = require("globals");
|
||||
let vue = require("eslint-plugin-vue");
|
||||
|
||||
module.exports = tseslint.config(
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...vue.configs["flat/recommended"],
|
||||
{
|
||||
ignores: ["node_modules", "dist", "__generated__", "*.config.js"],
|
||||
ignores: [
|
||||
"logs",
|
||||
"*.log",
|
||||
"npm-debug.log*",
|
||||
"yarn-debug.log*",
|
||||
"yarn-error.log*",
|
||||
"pnpm-debug.log*",
|
||||
"lerna-debug.log*",
|
||||
"node_modules",
|
||||
".DS_Store",
|
||||
"dist",
|
||||
"dist-ssr",
|
||||
"coverage",
|
||||
"*.local",
|
||||
"__generated__",
|
||||
"*.config.js",
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
|
||||
1968
frontend/package-lock.json
generated
1968
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@
|
||||
"manager",
|
||||
"emulation"
|
||||
],
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "npm run typecheck && vite build",
|
||||
@@ -27,7 +28,7 @@
|
||||
"generate": "openapi --input http://127.0.0.1:5000/openapi.json --output ./src/__generated__ --client axios --useOptions --useUnionTypes --exportServices false --exportSchemas false --exportCore false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.0.96",
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.7.4",
|
||||
"core-js": "^3.37.1",
|
||||
"cronstrue": "^2.50.0",
|
||||
@@ -46,7 +47,7 @@
|
||||
"vue": "^3.4.27",
|
||||
"vue-i18n": "^10.0.5",
|
||||
"vue-router": "^4.3.2",
|
||||
"vuetify": "^3.7.4",
|
||||
"vuetify": "^3.7.7",
|
||||
"webfontloader": "^1.6.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -59,19 +60,19 @@
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/webfontloader": "^1.6.38",
|
||||
"@vitejs/plugin-vue": "^3.2.0",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/tsconfig": "^0.5.1",
|
||||
"esbuild": "^0.20.2",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"globals": "^15.3.0",
|
||||
"openapi-typescript-codegen": "^0.25.0",
|
||||
"openapi-typescript-codegen": "^0.29.0",
|
||||
"tslib": "^2.6.2",
|
||||
"typescript": "^5.4.5",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^7.11.0",
|
||||
"vite": "^3.2.11",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-plugin-vuetify": "^1.0.2",
|
||||
"vite": "^6.0.11",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-tsc": "^2.1.10"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -4,6 +4,7 @@ import CopyRomDownloadLinkDialog from "@/components/common/Game/Dialog/CopyDownl
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeDownload from "@/stores/download";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeConfig from "@/stores/config";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import {
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
} from "@/utils";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject, ref } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
@@ -22,13 +24,23 @@ const heartbeatStore = storeHeartbeat();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const playInfoIcon = ref("mdi-play");
|
||||
const qrCodeIcon = ref("mdi-qrcode");
|
||||
const configStore = storeConfig();
|
||||
const { config } = storeToRefs(configStore);
|
||||
|
||||
const ejsEmulationSupported = computed(() =>
|
||||
isEJSEmulationSupported(props.rom.platform_slug, heartbeatStore.value),
|
||||
);
|
||||
const ruffleEmulationSupported = computed(() =>
|
||||
isRuffleEmulationSupported(props.rom.platform_slug, heartbeatStore.value),
|
||||
const platformSlug = computed(() =>
|
||||
props.rom.platform_slug in config.value.PLATFORMS_VERSIONS
|
||||
? config.value.PLATFORMS_VERSIONS[props.rom.platform_slug]
|
||||
: props.rom.platform_slug,
|
||||
);
|
||||
|
||||
const ejsEmulationSupported = computed(() => {
|
||||
return isEJSEmulationSupported(platformSlug.value, heartbeatStore.value);
|
||||
});
|
||||
|
||||
const ruffleEmulationSupported = computed(() => {
|
||||
return isRuffleEmulationSupported(platformSlug.value, heartbeatStore.value);
|
||||
});
|
||||
|
||||
const is3DSRom = computed(() => {
|
||||
return is3DSCIARom(props.rom);
|
||||
});
|
||||
|
||||
@@ -6,13 +6,15 @@ import type { DetailedRom } from "@/stores/roms";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useDisplay, useTheme } from "vuetify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { MdPreview } from "md-editor-v3";
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{ rom: DetailedRom }>();
|
||||
const { xs } = useDisplay();
|
||||
const theme = useTheme();
|
||||
const show = ref(false);
|
||||
const carousel = ref(0);
|
||||
const router = useRouter();
|
||||
@@ -119,7 +121,13 @@ function onFilterClick(filter: FilterType, value: string) {
|
||||
<v-divider class="mx-2 my-4" />
|
||||
<v-row no-gutters>
|
||||
<v-col class="text-caption">
|
||||
<span>{{ rom.summary }}</span>
|
||||
<MdPreview
|
||||
:model-value="rom.summary ?? ''"
|
||||
:theme="theme.name.value == 'dark' ? 'dark' : 'light'"
|
||||
preview-theme="vuepress"
|
||||
code-theme="github"
|
||||
:readonly="true"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@ import romApi from "@/services/api/rom";
|
||||
import storeAuth from "@/stores/auth";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import type { RomUserStatus } from "@/__generated__";
|
||||
import { difficultyEmojis, getTextForStatus, getEmojiForStatus } from "@/utils";
|
||||
import { getTextForStatus, getEmojiForStatus } from "@/utils";
|
||||
import { MdEditor, MdPreview } from "md-editor-v3";
|
||||
import "md-editor-v3/lib/style.css";
|
||||
import { ref, watch } from "vue";
|
||||
@@ -125,39 +125,37 @@ watch(
|
||||
romUser.rating =
|
||||
typeof $event === 'number' ? $event : parseInt($event)
|
||||
"
|
||||
active-color="primary"
|
||||
active-color="yellow"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="d-flex align-center mt-4" no-gutters>
|
||||
<v-col cols="auto">
|
||||
<v-col cols="12" md="2">
|
||||
<v-label>{{ t("rom.difficulty") }}</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-slider
|
||||
:class="{ 'ml-4': mdAndUp }"
|
||||
<v-col cols="12" md="10">
|
||||
<v-rating
|
||||
:class="{ 'ml-2': mdAndUp }"
|
||||
hover
|
||||
ripple
|
||||
length="10"
|
||||
size="26"
|
||||
full-icon="mdi-chili-mild"
|
||||
empty-icon="mdi-chili-mild-outline"
|
||||
v-model="romUser.difficulty"
|
||||
min="1"
|
||||
max="10"
|
||||
step="1"
|
||||
hide-details
|
||||
track-fill-color="primary"
|
||||
><template #append>
|
||||
<v-label class="opacity-100">
|
||||
{{
|
||||
difficultyEmojis[Math.floor(romUser.difficulty) - 1] ??
|
||||
difficultyEmojis[3]
|
||||
}}
|
||||
</v-label>
|
||||
</template></v-slider
|
||||
>
|
||||
@update:model-value="
|
||||
romUser.difficulty =
|
||||
typeof $event === 'number' ? $event : parseInt($event)
|
||||
"
|
||||
active-color="red"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row class="d-flex align-center mt-4" no-gutters>
|
||||
<v-col cols="auto">
|
||||
<v-col cols="12" md="2">
|
||||
<v-label>{{ t("rom.completion") }} %</v-label>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-col cols="12" md="10">
|
||||
<v-slider
|
||||
:class="{ 'ml-4': mdAndUp }"
|
||||
v-model="romUser.completion"
|
||||
|
||||
@@ -5,15 +5,17 @@ import storeGalleryFilter from "@/stores/galleryFilter";
|
||||
import storePlatforms from "@/stores/platforms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject } from "vue";
|
||||
import { inject, onMounted, watch } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useRouter } from "vue-router";
|
||||
import type { Platform } from "@/stores/platforms";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Props
|
||||
const { xs } = useDisplay();
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const romsStore = storeRoms();
|
||||
const { gettingRoms } = storeToRefs(romsStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
@@ -61,47 +63,68 @@ function setFilters() {
|
||||
]);
|
||||
}
|
||||
|
||||
function fetchRoms() {
|
||||
async function fetchRoms() {
|
||||
if (searchText.value) {
|
||||
// Auto hide android keyboard
|
||||
const inputElement = document.getElementById("search-text-field");
|
||||
inputElement?.blur();
|
||||
gettingRoms.value = true;
|
||||
romApi
|
||||
.getRoms({ searchTerm: searchText.value })
|
||||
.then(({ data }) => {
|
||||
data = data.sort((a, b) => {
|
||||
return a.platform_name.localeCompare(b.platform_name);
|
||||
});
|
||||
romsStore.set(data);
|
||||
romsStore.setFiltered(data, galleryFilterStore);
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(`Couldn't fetch roms: ${error}`);
|
||||
})
|
||||
.finally(() => {
|
||||
gettingRoms.value = false;
|
||||
|
||||
// Update URL with search term
|
||||
router.replace({ query: { search: searchText.value } });
|
||||
|
||||
try {
|
||||
const { data } = await romApi.getRoms({ searchTerm: searchText.value });
|
||||
const sortedData = data.sort((a, b) => {
|
||||
return a.platform_name.localeCompare(b.platform_name);
|
||||
});
|
||||
galleryFilterStore.setFilterPlatforms([
|
||||
...new Map(
|
||||
romsStore.filteredRoms.map((rom) => {
|
||||
const platform = allPlatforms.value.find(
|
||||
(p) => p.id === rom.platform_id,
|
||||
);
|
||||
return [rom.platform_name, platform];
|
||||
}),
|
||||
).values(),
|
||||
] as Platform[]);
|
||||
setFilters();
|
||||
galleryFilterStore.activeFilterDrawer = false;
|
||||
romsStore.set(sortedData);
|
||||
romsStore.setFiltered(sortedData, galleryFilterStore);
|
||||
} catch (error) {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(`Couldn't fetch roms: ${error}`);
|
||||
} finally {
|
||||
gettingRoms.value = false;
|
||||
|
||||
galleryFilterStore.setFilterPlatforms([
|
||||
...new Map(
|
||||
romsStore.filteredRoms.map((rom) => {
|
||||
const platform = allPlatforms.value.find(
|
||||
(p) => p.id === rom.platform_id,
|
||||
);
|
||||
return [rom.platform_name, platform];
|
||||
}),
|
||||
).values(),
|
||||
] as Platform[]);
|
||||
setFilters();
|
||||
galleryFilterStore.activeFilterDrawer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { search: searchTerm } = router.currentRoute.value.query;
|
||||
if (searchTerm && searchTerm !== searchText.value) {
|
||||
searchText.value = searchTerm as string;
|
||||
fetchRoms();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
router.currentRoute.value.query,
|
||||
(query) => {
|
||||
if (query.search && query.search !== searchText.value) {
|
||||
searchText.value = query.search as string;
|
||||
fetchRoms();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -4,22 +4,45 @@ import type { Events } from "@/types/emitter";
|
||||
import { debounce } from "lodash";
|
||||
import type { Emitter } from "mitt";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject, nextTick } from "vue";
|
||||
import { inject, nextTick, onMounted, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const router = useRouter();
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { filterText } = storeToRefs(galleryFilterStore);
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
|
||||
const filterRoms = debounce(() => {
|
||||
// Update URL with search term
|
||||
router.replace({ query: { search: filterText.value } });
|
||||
emitter?.emit("filter", null);
|
||||
}, 500);
|
||||
|
||||
function clear() {
|
||||
filterText.value = "";
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { search: searchTerm } = router.currentRoute.value.query;
|
||||
if (searchTerm && searchTerm !== filterText.value) {
|
||||
filterText.value = searchTerm as string;
|
||||
filterRoms();
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
router.currentRoute.value.query,
|
||||
(query) => {
|
||||
if (query.search && query.search !== filterText.value) {
|
||||
filterText.value = query.search as string;
|
||||
filterRoms();
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -64,7 +64,7 @@ async function switchFromFavourites() {
|
||||
}
|
||||
await collectionApi
|
||||
.updateCollection({ collection: favCollection.value as Collection })
|
||||
.then(({ data }) => {
|
||||
.then(() => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `${props.rom.name} ${
|
||||
collectionsStore.isFav(props.rom) ? "added to" : "removed from"
|
||||
@@ -101,6 +101,8 @@ async function resetLastPlayed() {
|
||||
color: "green",
|
||||
timeout: 2000,
|
||||
});
|
||||
|
||||
romsStore.removeFromContinuePlaying(props.rom);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
|
||||
@@ -3,6 +3,7 @@ import AdminMenu from "@/components/common/Game/AdminMenu.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeDownload from "@/stores/download";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import storeConfig from "@/stores/config";
|
||||
import type { SimpleRom } from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import {
|
||||
@@ -12,22 +13,28 @@ import {
|
||||
} from "@/utils";
|
||||
import type { Emitter } from "mitt";
|
||||
import { computed, inject } from "vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
// Props
|
||||
const props = defineProps<{ rom: SimpleRom }>();
|
||||
const downloadStore = storeDownload();
|
||||
const heartbeatStore = storeHeartbeat();
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
const configStore = storeConfig();
|
||||
const { config } = storeToRefs(configStore);
|
||||
|
||||
const platformSlug = computed(() => {
|
||||
return props.rom.platform_slug in config.value.PLATFORMS_VERSIONS
|
||||
? config.value.PLATFORMS_VERSIONS[props.rom.platform_slug]
|
||||
: props.rom.platform_slug;
|
||||
});
|
||||
|
||||
const ejsEmulationSupported = computed(() => {
|
||||
return isEJSEmulationSupported(props.rom.platform_slug, heartbeatStore.value);
|
||||
return isEJSEmulationSupported(platformSlug.value, heartbeatStore.value);
|
||||
});
|
||||
|
||||
const ruffleEmulationSupported = computed(() => {
|
||||
return isRuffleEmulationSupported(
|
||||
props.rom.platform_slug,
|
||||
heartbeatStore.value,
|
||||
);
|
||||
return isRuffleEmulationSupported(platformSlug.value, heartbeatStore.value);
|
||||
});
|
||||
|
||||
const is3DSRom = computed(() => {
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import GameCard from "@/components/common/Game/Card/Base.vue";
|
||||
import PlatformIcon from "@/components/common/Platform/Icon.vue";
|
||||
import RDialog from "@/components/common/RDialog.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import type { SimpleRom } from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { inject, onBeforeUnmount, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useDisplay } from "vuetify";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
// Define types
|
||||
type Platform = {
|
||||
platform_name: string;
|
||||
platform_slug: string;
|
||||
};
|
||||
|
||||
type SelectItem = {
|
||||
raw: Platform;
|
||||
};
|
||||
|
||||
// Props
|
||||
const { t } = useI18n();
|
||||
const { lgAndUp } = useDisplay();
|
||||
const show = ref(false);
|
||||
const searching = ref(false);
|
||||
const searched = ref(false);
|
||||
const router = useRouter();
|
||||
const searchedRoms = ref<Platform[]>([]);
|
||||
const filteredRoms = ref<SimpleRom[]>([]);
|
||||
const platforms = ref<Platform[]>([]);
|
||||
const selectedPlatform = ref<Platform | null>(null);
|
||||
const searchValue = ref("");
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("showSearchRomDialog", () => {
|
||||
show.value = true;
|
||||
});
|
||||
|
||||
async function filterRoms() {
|
||||
if (!selectedPlatform.value) {
|
||||
filteredRoms.value = searchedRoms.value as SimpleRom[];
|
||||
} else {
|
||||
filteredRoms.value = searchedRoms.value.filter(
|
||||
(rom: { platform_name: string }) =>
|
||||
rom.platform_name == selectedPlatform.value?.platform_name,
|
||||
) as SimpleRom[];
|
||||
}
|
||||
}
|
||||
|
||||
function clearFilter() {
|
||||
selectedPlatform.value = null;
|
||||
filterRoms();
|
||||
}
|
||||
|
||||
async function searchRoms() {
|
||||
if (searchValue.value != "") {
|
||||
// Auto hide android keyboard
|
||||
const inputElement = document.getElementById("search-text-field");
|
||||
inputElement?.blur();
|
||||
searching.value = true;
|
||||
searched.value = true;
|
||||
searchedRoms.value = (
|
||||
await romApi.getRoms({ searchTerm: searchValue.value })
|
||||
).data.sort((a, b) => {
|
||||
return a.platform_name.localeCompare(b.platform_name);
|
||||
});
|
||||
platforms.value = [
|
||||
...new Map(
|
||||
searchedRoms.value.map((rom): [string, Platform] => [
|
||||
rom.platform_name,
|
||||
{
|
||||
platform_name: rom.platform_name,
|
||||
platform_slug: rom.platform_slug,
|
||||
},
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
filterRoms();
|
||||
searching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onGameClick(emitData: { rom: SimpleRom; event: MouseEvent }) {
|
||||
router.push({ name: "rom", params: { rom: emitData.rom.id } });
|
||||
closeDialog();
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
show.value = false;
|
||||
searched.value = false;
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
emitter?.off("showSearchRomDialog");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<r-dialog
|
||||
v-model="show"
|
||||
icon="mdi-magnify"
|
||||
:loading-condition="searching"
|
||||
:empty-state-condition="searchedRoms?.length == 0 && searched"
|
||||
empty-state-type="game"
|
||||
scroll-content
|
||||
:width="lgAndUp ? '60vw' : '95vw'"
|
||||
:height="lgAndUp ? '90vh' : '775px'"
|
||||
>
|
||||
<template #toolbar>
|
||||
<v-row class="align-center" no-gutters>
|
||||
<v-col cols="5" md="6" lg="7">
|
||||
<v-text-field
|
||||
autofocus
|
||||
id="search-text-field"
|
||||
@keyup.enter="searchRoms"
|
||||
@click:clear="searchRoms"
|
||||
v-model="searchValue"
|
||||
:disabled="searching"
|
||||
:label="t('common.search')"
|
||||
hide-details
|
||||
class="bg-toplayer"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="5" lg="4">
|
||||
<v-select
|
||||
@click:clear="clearFilter"
|
||||
:label="t('common.platform')"
|
||||
class="bg-toplayer"
|
||||
item-title="platform_name"
|
||||
:disabled="platforms.length == 0 || searching"
|
||||
hide-details
|
||||
clearable
|
||||
single-line
|
||||
return-object
|
||||
v-model="selectedPlatform"
|
||||
@update:model-value="filterRoms"
|
||||
:items="platforms"
|
||||
>
|
||||
<template #item="{ props, item }">
|
||||
<v-list-item
|
||||
class="py-2"
|
||||
v-bind="props"
|
||||
:title="(item as SelectItem).raw.platform_name ?? ''"
|
||||
>
|
||||
<template #prepend>
|
||||
<platform-icon
|
||||
:size="35"
|
||||
:key="(item as SelectItem).raw.platform_slug"
|
||||
:slug="(item as SelectItem).raw.platform_slug"
|
||||
:name="(item as SelectItem).raw.platform_name"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template #selection="{ item }">
|
||||
<v-list-item
|
||||
class="px-0"
|
||||
:title="(item as SelectItem).raw.platform_name ?? ''"
|
||||
>
|
||||
<template #prepend>
|
||||
<platform-icon
|
||||
:size="35"
|
||||
:key="(item as SelectItem).raw.platform_slug"
|
||||
:slug="(item as SelectItem).raw.platform_slug"
|
||||
:name="(item as SelectItem).raw.platform_name"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col>
|
||||
<v-btn
|
||||
type="submit"
|
||||
@click="searchRoms"
|
||||
class="bg-toplayer"
|
||||
variant="text"
|
||||
icon="mdi-magnify"
|
||||
block
|
||||
:disabled="searching"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template #content>
|
||||
<v-row no-gutters class="align-content-start align-center">
|
||||
<v-col
|
||||
class="pa-1 align-self-end"
|
||||
cols="4"
|
||||
sm="3"
|
||||
md="2"
|
||||
v-show="!searching"
|
||||
v-for="rom in filteredRoms"
|
||||
>
|
||||
<game-card
|
||||
:key="rom.updated_at"
|
||||
:rom="rom"
|
||||
@click="onGameClick({ rom, event: $event })"
|
||||
titleOnHover
|
||||
pointerOnHover
|
||||
withLink
|
||||
showFlags
|
||||
showFav
|
||||
transformScale
|
||||
showActionBar
|
||||
showPlatformIcon
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</r-dialog>
|
||||
</template>
|
||||
@@ -5,6 +5,7 @@ import RAvatarRom from "@/components/common/Game/RAvatar.vue";
|
||||
import romApi from "@/services/api/rom";
|
||||
import storeDownload from "@/stores/download";
|
||||
import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import storeConfig from "@/stores/config";
|
||||
import storeHeartbeat from "@/stores/heartbeat";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import {
|
||||
@@ -34,6 +35,8 @@ const downloadStore = storeDownload();
|
||||
const romsStore = storeRoms();
|
||||
const { filteredRoms, selectedRoms } = storeToRefs(romsStore);
|
||||
const heartbeatStore = storeHeartbeat();
|
||||
const configStore = storeConfig();
|
||||
const { config } = storeToRefs(configStore);
|
||||
const page = ref(parseInt(window.location.hash.slice(1)) || 1);
|
||||
const storedRomsPerPage = parseInt(localStorage.getItem("romsPerPage") ?? "");
|
||||
const itemsPerPage = ref(isNaN(storedRomsPerPage) ? 25 : storedRomsPerPage);
|
||||
@@ -106,12 +109,20 @@ function updateUrlHash() {
|
||||
window.location.hash = String(page.value);
|
||||
}
|
||||
|
||||
function getTruePlatformSlug(platformSlug: string) {
|
||||
return platformSlug in config.value.PLATFORMS_VERSIONS
|
||||
? config.value.PLATFORMS_VERSIONS[platformSlug]
|
||||
: platformSlug;
|
||||
}
|
||||
|
||||
function checkIfEJSEmulationSupported(platformSlug: string) {
|
||||
return isEJSEmulationSupported(platformSlug, heartbeatStore.value);
|
||||
const slug = getTruePlatformSlug(platformSlug);
|
||||
return isEJSEmulationSupported(slug, heartbeatStore.value);
|
||||
}
|
||||
|
||||
function checkIfRuffleEmulationSupported(platformSlug: string) {
|
||||
return isRuffleEmulationSupported(platformSlug, heartbeatStore.value);
|
||||
const slug = getTruePlatformSlug(platformSlug);
|
||||
return isRuffleEmulationSupported(slug, heartbeatStore.value);
|
||||
}
|
||||
|
||||
function updateSelectAll() {
|
||||
@@ -251,13 +262,13 @@ onMounted(() => {
|
||||
>
|
||||
{{ languageToEmoji(language) }}
|
||||
</span>
|
||||
<spa class="reglang-super">
|
||||
<span class="reglang-super">
|
||||
{{
|
||||
item.languages.length > 3
|
||||
? ` +${item.languages.length - 3}`
|
||||
: ""
|
||||
}}
|
||||
</spa>
|
||||
</span>
|
||||
</div>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<
|
||||
<script setup lang="ts">
|
||||
import HomeBtn from "@/components/common/Navigation/HomeBtn.vue";
|
||||
import PlatformsBtn from "@/components/common/Navigation/PlatformsBtn.vue";
|
||||
@@ -6,7 +7,6 @@ import ScanBtn from "@/components/common/Navigation/ScanBtn.vue";
|
||||
import SearchBtn from "@/components/common/Navigation/SearchBtn.vue";
|
||||
import UploadBtn from "@/components/common/Navigation/UploadBtn.vue";
|
||||
import UserBtn from "@/components/common/Navigation/UserBtn.vue";
|
||||
import SearchRomDialog from "@/components/common/Game/Dialog/SearchRom.vue";
|
||||
import PlatformsDrawer from "@/components/common/Navigation/PlatformsDrawer.vue";
|
||||
import CollectionsDrawer from "@/components/common/Navigation/CollectionsDrawer.vue";
|
||||
import UploadRomDialog from "@/components/common/Game/Dialog/UploadRom.vue";
|
||||
@@ -78,9 +78,9 @@ const { activePlatformsDrawer, activeCollectionsDrawer, activeSettingsDrawer } =
|
||||
</template>
|
||||
</v-navigation-drawer>
|
||||
|
||||
<search-rom-dialog />
|
||||
<platforms-drawer />
|
||||
<collections-drawer />
|
||||
<upload-rom-dialog />
|
||||
<settings-drawer />
|
||||
</template>
|
||||
>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"tags": "Tags",
|
||||
"genres": "Genres",
|
||||
"franchises": "Franchises",
|
||||
"collections": "Collecciones",
|
||||
"collections": "Collections",
|
||||
"companies": "Companies",
|
||||
"age-rating": "Age rating",
|
||||
"no-saves-found": "No saves found",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"tags": "Tags",
|
||||
"genres": "Genres",
|
||||
"franchises": "Franchises",
|
||||
"collections": "Collecciones",
|
||||
"collections": "Collections",
|
||||
"companies": "Companies",
|
||||
"age-rating": "Age rating",
|
||||
"no-saves-found": "No saves found",
|
||||
|
||||
@@ -188,7 +188,7 @@ async function updateUserRomProps({
|
||||
romId: number;
|
||||
data: Partial<RomUserSchema>;
|
||||
updateLastPlayed?: boolean;
|
||||
}): Promise<{ data: DetailedRom }> {
|
||||
}): Promise<{ data: RomUserSchema }> {
|
||||
return api.put(`/roms/${romId}/props`, {
|
||||
data: data,
|
||||
update_last_played: updateLastPlayed,
|
||||
|
||||
@@ -99,9 +99,17 @@ export default defineStore("roms", {
|
||||
addToRecent(rom: SimpleRom) {
|
||||
this.recentRoms = [rom, ...this.recentRoms];
|
||||
},
|
||||
removeFromRecent(rom: SimpleRom) {
|
||||
this.recentRoms = this.recentRoms.filter((value) => value.id !== rom.id);
|
||||
},
|
||||
addToContinuePlaying(rom: SimpleRom) {
|
||||
this.continuePlayingRoms = [rom, ...this.continuePlayingRoms];
|
||||
},
|
||||
removeFromContinuePlaying(rom: SimpleRom) {
|
||||
this.continuePlayingRoms = this.continuePlayingRoms.filter(
|
||||
(value) => value.id !== rom.id,
|
||||
);
|
||||
},
|
||||
update(rom: SimpleRom) {
|
||||
this.allRoms = this.allRoms.map((value) =>
|
||||
value.id === rom.id ? rom : value,
|
||||
|
||||
1
frontend/src/types/emitter.d.ts
vendored
1
frontend/src/types/emitter.d.ts
vendored
@@ -21,7 +21,6 @@ export type Events = {
|
||||
showMatchRomDialog: SimpleRom;
|
||||
showSearchCoverDialog: { term: string; aspectRatio: number | null };
|
||||
updateUrlCover: string;
|
||||
showSearchRomDialog: null;
|
||||
showEditRomDialog: SimpleRom;
|
||||
showCopyDownloadLinkDialog: string;
|
||||
showDeleteRomDialog: SimpleRom[];
|
||||
|
||||
@@ -360,10 +360,10 @@ const _EJS_CORES_MAP = {
|
||||
"game-boy-micro": ["mgba"],
|
||||
gbc: ["gambatte", "mgba"],
|
||||
"pc-fx": ["mednafen_pcfx"],
|
||||
ps: ["pcsx_rearmed", "mednafen_psx"],
|
||||
ps: ["pcsx_rearmed", "mednafen_psx_hw"],
|
||||
psp: ["ppsspp"],
|
||||
segacd: ["genesis_plus_gx", "picodrive"],
|
||||
// sega32: ["picodrive"], // Broken: https://github.com/EmulatorJS/EmulatorJS/issues/579
|
||||
sega32: ["picodrive"],
|
||||
gamegear: ["genesis_plus_gx"],
|
||||
sms: ["genesis_plus_gx"],
|
||||
"sega-mark-iii": ["genesis_plus_gx"],
|
||||
@@ -453,6 +453,41 @@ export function isEJSThreadsSupported(): boolean {
|
||||
return typeof SharedArrayBuffer !== "undefined";
|
||||
}
|
||||
|
||||
// This is a workaround to set the control scheme for Sega systems using the same cores
|
||||
const _EJS_CONTROL_SCHEMES = {
|
||||
segacd: "segaCD",
|
||||
sega32: "sega32x",
|
||||
gamegear: "segaGG",
|
||||
sms: "segaMS",
|
||||
"sega-mark-iii": "segaMS",
|
||||
"sega-master-system-ii": "segaMS",
|
||||
"master-system-super-compact": "segaMS",
|
||||
"master-system-girl": "segaMS",
|
||||
"genesis-slash-megadrive": "segaMD",
|
||||
"sega-mega-drive-2-slash-genesis": "segaMD",
|
||||
"sega-mega-jet": "segaMD",
|
||||
"mega-pc": "segaMD",
|
||||
"tera-drive": "segaMD",
|
||||
"sega-nomad": "segaMD",
|
||||
saturn: "segaSaturn",
|
||||
};
|
||||
|
||||
type EJSControlSlug = keyof typeof _EJS_CONTROL_SCHEMES;
|
||||
|
||||
/**
|
||||
* Get the control scheme for a given platform.
|
||||
*
|
||||
* @param platformSlug The platform slug.
|
||||
* @returns The control scheme.
|
||||
*/
|
||||
export function getControlSchemeForPlatform(
|
||||
platformSlug: string,
|
||||
): string | null {
|
||||
return platformSlug in _EJS_CONTROL_SCHEMES
|
||||
? _EJS_CONTROL_SCHEMES[platformSlug as EJSControlSlug]
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Ruffle emulation is supported for a given platform.
|
||||
*
|
||||
@@ -472,22 +507,6 @@ export function isRuffleEmulationSupported(
|
||||
|
||||
type PlayingStatus = RomUserStatus | "backlogged" | "now_playing" | "hidden";
|
||||
|
||||
/**
|
||||
* Array of difficulty emojis.
|
||||
*/
|
||||
export const difficultyEmojis = [
|
||||
"😴",
|
||||
"🥱",
|
||||
"😐",
|
||||
"😄",
|
||||
"🤔",
|
||||
"🤯",
|
||||
"😓",
|
||||
"😡",
|
||||
"🤬",
|
||||
"😵",
|
||||
];
|
||||
|
||||
/**
|
||||
* Map of ROM statuses to their corresponding emoji and text.
|
||||
*/
|
||||
|
||||
@@ -53,34 +53,36 @@ async function fetchRoms() {
|
||||
scrim: false,
|
||||
});
|
||||
|
||||
await romApi
|
||||
.getRoms({
|
||||
try {
|
||||
const { data } = await romApi.getRoms({
|
||||
collectionId: romsStore.currentCollection?.id,
|
||||
searchTerm: normalizeString(galleryFilterStore.filterText),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
romsStore.set(data);
|
||||
romsStore.setFiltered(data, galleryFilterStore);
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms for collection ID ${currentCollection.value?.id}: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(
|
||||
`Couldn't fetch roms for collection ID ${currentCollection.value?.id}: ${error}`,
|
||||
);
|
||||
noCollectionError.value = true;
|
||||
})
|
||||
.finally(() => {
|
||||
gettingRoms.value = false;
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: gettingRoms.value,
|
||||
scrim: false,
|
||||
});
|
||||
});
|
||||
romsStore.set(data);
|
||||
romsStore.setFiltered(data, galleryFilterStore);
|
||||
|
||||
gettingRoms.value = false;
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: gettingRoms.value,
|
||||
scrim: false,
|
||||
});
|
||||
} catch (error) {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms for collection ID ${currentCollection.value?.id}: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(
|
||||
`Couldn't fetch roms for collection ID ${currentCollection.value?.id}: ${error}`,
|
||||
);
|
||||
noCollectionError.value = true;
|
||||
} finally {
|
||||
gettingRoms.value = false;
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: gettingRoms.value,
|
||||
scrim: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setFilters() {
|
||||
@@ -216,7 +218,7 @@ onMounted(async () => {
|
||||
|
||||
watch(
|
||||
() => allCollections.value,
|
||||
(collections) => {
|
||||
async (collections) => {
|
||||
if (
|
||||
collections.length > 0 &&
|
||||
collections.some((collection) => collection.id === routeCollectionId)
|
||||
@@ -233,7 +235,7 @@ onMounted(async () => {
|
||||
) {
|
||||
romsStore.setCurrentCollection(collection);
|
||||
resetGallery();
|
||||
fetchRoms();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
|
||||
@@ -256,7 +258,7 @@ onBeforeRouteUpdate(async (to, from) => {
|
||||
|
||||
watch(
|
||||
() => allCollections.value,
|
||||
(collections) => {
|
||||
async (collections) => {
|
||||
if (collections.length > 0) {
|
||||
const collection = collections.find(
|
||||
(collection) => collection.id === routeCollectionId,
|
||||
@@ -269,7 +271,7 @@ onBeforeRouteUpdate(async (to, from) => {
|
||||
collection
|
||||
) {
|
||||
romsStore.setCurrentCollection(collection);
|
||||
fetchRoms();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("filter", onFilterChange);
|
||||
|
||||
// Functions
|
||||
function fetchRoms() {
|
||||
async function fetchRoms() {
|
||||
if (gettingRoms.value) return;
|
||||
|
||||
gettingRoms.value = true;
|
||||
@@ -51,34 +51,31 @@ function fetchRoms() {
|
||||
scrim: false,
|
||||
});
|
||||
|
||||
romApi
|
||||
.getRoms({
|
||||
try {
|
||||
const { data } = await romApi.getRoms({
|
||||
platformId: romsStore.currentPlatform?.id,
|
||||
searchTerm: normalizeString(galleryFilterStore.filterText),
|
||||
})
|
||||
.then(({ data }) => {
|
||||
romsStore.set(data);
|
||||
romsStore.setFiltered(data, galleryFilterStore);
|
||||
})
|
||||
.catch((error) => {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms for platform ID ${currentPlatform.value?.id}: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(
|
||||
`Couldn't fetch roms for platform ID ${currentPlatform.value?.id}: ${error}`,
|
||||
);
|
||||
noPlatformError.value = true;
|
||||
})
|
||||
.finally(() => {
|
||||
gettingRoms.value = false;
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: gettingRoms.value,
|
||||
scrim: false,
|
||||
});
|
||||
});
|
||||
|
||||
romsStore.set(data);
|
||||
romsStore.setFiltered(data, galleryFilterStore);
|
||||
} catch (error) {
|
||||
emitter?.emit("snackbarShow", {
|
||||
msg: `Couldn't fetch roms for platform ID ${currentPlatform.value?.id}: ${error}`,
|
||||
icon: "mdi-close-circle",
|
||||
color: "red",
|
||||
timeout: 4000,
|
||||
});
|
||||
console.error(
|
||||
`Couldn't fetch roms for platform ID ${currentPlatform.value?.id}: ${error}`,
|
||||
);
|
||||
noPlatformError.value = true;
|
||||
} finally {
|
||||
gettingRoms.value = false;
|
||||
emitter?.emit("showLoadingDialog", {
|
||||
loading: gettingRoms.value,
|
||||
scrim: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function setFilters() {
|
||||
@@ -223,7 +220,7 @@ onMounted(async () => {
|
||||
|
||||
watch(
|
||||
() => allPlatforms.value,
|
||||
(platforms) => {
|
||||
async (platforms) => {
|
||||
if (platforms.length > 0) {
|
||||
if (platforms.some((platform) => platform.id === routePlatformId)) {
|
||||
const platform = platforms.find(
|
||||
@@ -238,7 +235,7 @@ onMounted(async () => {
|
||||
) {
|
||||
romsStore.setCurrentPlatform(platform);
|
||||
resetGallery();
|
||||
fetchRoms();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
}
|
||||
|
||||
@@ -272,7 +269,7 @@ onBeforeRouteUpdate(async (to, from) => {
|
||||
|
||||
watch(
|
||||
() => allPlatforms.value,
|
||||
(platforms) => {
|
||||
async (platforms) => {
|
||||
if (platforms.length > 0) {
|
||||
const platform = platforms.find(
|
||||
(platform) => platform.id === routePlatformId,
|
||||
@@ -285,7 +282,7 @@ onBeforeRouteUpdate(async (to, from) => {
|
||||
platform
|
||||
) {
|
||||
romsStore.setCurrentPlatform(platform);
|
||||
fetchRoms();
|
||||
await fetchRoms();
|
||||
setFilters();
|
||||
} else {
|
||||
noPlatformError.value = true;
|
||||
|
||||
@@ -11,7 +11,6 @@ import storeRoms, { type SimpleRom } from "@/stores/roms";
|
||||
import type { Events } from "@/types/emitter";
|
||||
import type { Emitter } from "mitt";
|
||||
import { views } from "@/utils";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { inject, onBeforeUnmount, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
@@ -22,6 +21,8 @@ const { scrolledToTop, currentView } = storeToRefs(galleryViewStore);
|
||||
const galleryFilterStore = storeGalleryFilter();
|
||||
const { searchText } = storeToRefs(galleryFilterStore);
|
||||
const romsStore = storeRoms();
|
||||
const router = useRouter();
|
||||
const initialSearch = ref(false);
|
||||
const {
|
||||
allRoms,
|
||||
filteredRoms,
|
||||
@@ -31,13 +32,12 @@ const {
|
||||
itemsPerBatch,
|
||||
gettingRoms,
|
||||
} = storeToRefs(romsStore);
|
||||
|
||||
const itemsShown = ref(itemsPerBatch.value);
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
const emitter = inject<Emitter<Events>>("emitter");
|
||||
emitter?.on("filter", onFilterChange);
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const initialSearch = ref(false);
|
||||
|
||||
// Functions
|
||||
function setFilters() {
|
||||
|
||||
@@ -4,7 +4,11 @@ import saveApi, { saveApi as api } from "@/services/api/save";
|
||||
import screenshotApi from "@/services/api/screenshot";
|
||||
import stateApi from "@/services/api/state";
|
||||
import type { DetailedRom } from "@/stores/roms";
|
||||
import { areThreadsRequiredForEJSCore, getSupportedEJSCores } from "@/utils";
|
||||
import {
|
||||
areThreadsRequiredForEJSCore,
|
||||
getSupportedEJSCores,
|
||||
getControlSchemeForPlatform,
|
||||
} from "@/utils";
|
||||
import { onBeforeUnmount, onMounted, ref } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -39,6 +43,7 @@ declare global {
|
||||
EJS_startOnLoaded: boolean;
|
||||
EJS_fullscreenOnLoaded: boolean;
|
||||
EJS_threads: boolean;
|
||||
EJS_controlScheme: string | null;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
EJS_emulator: any;
|
||||
EJS_onGameStart: () => void;
|
||||
@@ -52,6 +57,9 @@ declare global {
|
||||
const supportedCores = getSupportedEJSCores(romRef.value.platform_slug);
|
||||
window.EJS_core =
|
||||
supportedCores.find((core) => core === props.core) ?? supportedCores[0];
|
||||
window.EJS_controlScheme = getControlSchemeForPlatform(
|
||||
romRef.value.platform_slug,
|
||||
);
|
||||
window.EJS_threads = areThreadsRequiredForEJSCore(window.EJS_core);
|
||||
window.EJS_gameID = romRef.value.id;
|
||||
window.EJS_gameUrl = `/api/roms/${romRef.value.id}/content/${romRef.value.file_name}`;
|
||||
|
||||
623
poetry.lock
generated
623
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
136
pyproject.toml
136
pyproject.toml
@@ -1,69 +1,79 @@
|
||||
[tool.poetry]
|
||||
package-mode = false
|
||||
[project]
|
||||
name = "romm"
|
||||
version = "0.0.1"
|
||||
description = "A beautiful, powerful, self-hosted rom manager"
|
||||
license = "GNU AGPLv3"
|
||||
repository = "https://github.com/rommapp/romm"
|
||||
authors = ["Zurdi <zurdi@romm.app>", "Arcane <arcane@romm.app>"]
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{ name = "Zurdi", email = "zurdi@romm.app" },
|
||||
{ name = "Arcane", email = "arcane@romm.app" },
|
||||
{ name = "Adamantike", email = "adamantike@romm.app" },
|
||||
]
|
||||
requires-python = "^3.12"
|
||||
dependencies = [
|
||||
"PyYAML == 6.0.1",
|
||||
"SQLAlchemy[mariadb-connector,mysql-connector,postgresql-psycopg] ~= 2.0",
|
||||
"Unidecode == 1.3.8",
|
||||
"alembic == 1.13.1",
|
||||
"anyio ~= 4.4",
|
||||
"authlib ~= 1.3",
|
||||
"certifi == 2024.07.04",
|
||||
"colorama ~= 0.4",
|
||||
"emoji == 2.10.1",
|
||||
"fastapi == 0.115.6",
|
||||
"gunicorn == 22.0.0",
|
||||
"httpx ~= 0.27",
|
||||
"itsdangerous ~= 2.1",
|
||||
"joserfc ~= 0.9",
|
||||
"passlib[bcrypt] ~= 1.7",
|
||||
"pillow ~= 10.3",
|
||||
"psycopg[c] ~= 3.2",
|
||||
"py7zr == 1.0.0rc2",
|
||||
"pydash ~= 7.0",
|
||||
"python-dotenv == 1.0.1",
|
||||
"python-magic ~= 0.4",
|
||||
"python-multipart ~= 0.0.18",
|
||||
"python-socketio == 5.11.1",
|
||||
"redis ~= 5.0",
|
||||
"rq ~= 2.1",
|
||||
"rq-scheduler ~= 0.14",
|
||||
"sentry-sdk ~= 2.19",
|
||||
"sqlakeyset ~= 2.0",
|
||||
"starlette-csrf ~= 3.0",
|
||||
# TODO: Move back to official releases once the following PR is merged and released:
|
||||
# https://github.com/python-poetry/poetry-core/pull/803
|
||||
"streaming-form-data @ git+https://github.com/gantoine/streaming-form-data.git@b8a49ba",
|
||||
"types-colorama ~= 0.4",
|
||||
"types-passlib ~= 1.7",
|
||||
"types-pyyaml ~= 6.0",
|
||||
"types-redis ~= 4.6",
|
||||
"uvicorn == 0.29.0",
|
||||
"watchdog ~= 4.0",
|
||||
"websockets == 12.0",
|
||||
"yarl ~= 1.14",
|
||||
"zipfile-deflate64 ~= 0.2",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
anyio = "^4.4"
|
||||
fastapi = "0.115.6"
|
||||
uvicorn = "0.29.0"
|
||||
gunicorn = "22.0.0"
|
||||
websockets = "12.0"
|
||||
python-socketio = "5.11.1"
|
||||
psycopg = { version = "^3.2", extras = ["c"] }
|
||||
SQLAlchemy = { version = "^2.0.30", extras = [
|
||||
"mariadb-connector",
|
||||
"mysql-connector",
|
||||
"postgresql-psycopg",
|
||||
] }
|
||||
alembic = "1.13.1"
|
||||
PyYAML = "6.0.1"
|
||||
Unidecode = "1.3.8"
|
||||
emoji = "2.10.1"
|
||||
python-dotenv = "1.0.1"
|
||||
sqlakeyset = "^2.0.1708907391"
|
||||
pydash = "^7.0.7"
|
||||
rq = "^1.16.1"
|
||||
redis = "^5.0"
|
||||
passlib = { extras = ["bcrypt"], version = "^1.7.4" }
|
||||
itsdangerous = "^2.1.2"
|
||||
rq-scheduler = "^0.13.1"
|
||||
starlette-csrf = "^3.0.0"
|
||||
httpx = "^0.27.0"
|
||||
python-multipart = "^0.0.18"
|
||||
watchdog = "^4.0.0"
|
||||
yarl = "^1.14"
|
||||
joserfc = "^0.9.0"
|
||||
pillow = "^10.3.0"
|
||||
certifi = "2024.07.04"
|
||||
authlib = "^1.3.1"
|
||||
python-magic = "^0.4.27"
|
||||
py7zr = "1.0.0rc2"
|
||||
sentry-sdk = "^2.19"
|
||||
# TODO: Move back to official releases once the following PR is merged and released:
|
||||
# https://github.com/python-poetry/poetry-core/pull/803
|
||||
streaming-form-data = { git = "https://github.com/gantoine/streaming-form-data.git", rev = "b8a49ba" }
|
||||
zipfile-deflate64 = "^0.2.0"
|
||||
colorama = "^0.4.6"
|
||||
types-colorama = "^0.4"
|
||||
types-passlib = "^1.7.7.20240311"
|
||||
types-pyyaml = "^6.0.12.20240311"
|
||||
types-redis = "^4.6.0.20240311 "
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ipdb ~= 0.13",
|
||||
"ipykernel ~= 6.29",
|
||||
"memray ~= 1.15",
|
||||
"mypy ~= 1.13",
|
||||
"pyinstrument ~= 5.0",
|
||||
]
|
||||
test = [
|
||||
"fakeredis ~= 2.21",
|
||||
"pytest ~= 8.3",
|
||||
"pytest-asyncio ~= 0.23",
|
||||
"pytest-env ~= 1.1",
|
||||
"pytest-mock ~= 3.12",
|
||||
"pytest-recording ~= 0.13",
|
||||
]
|
||||
|
||||
[tool.poetry.group.test.dependencies]
|
||||
fakeredis = "^2.21.3"
|
||||
pytest = "^8.3"
|
||||
pytest-env = "^1.1.3"
|
||||
pytest-mock = "^3.12.0"
|
||||
pytest-asyncio = "^0.23.5"
|
||||
pytest-recording = "^0.13"
|
||||
[project.urls]
|
||||
Homepage = "https://romm.app/"
|
||||
Source = "https://github.com/rommapp/romm"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ipdb = "^0.13.13"
|
||||
mypy = "^1.13"
|
||||
ipykernel = "^6.29.4"
|
||||
[tool.poetry]
|
||||
package-mode = false
|
||||
requires-poetry = ">=2.0"
|
||||
|
||||
Reference in New Issue
Block a user