From f2d04a2069f300590bd2f3546d89dd5be4c9f053 Mon Sep 17 00:00:00 2001
From: zurdi
- {{ t("settings.folder-alias") }}: - {{ t("settings.folder-mappings-tooltip-aliases") }} -
-- {{ t("settings.platform-variant") }}: - {{ t("settings.folder-mappings-tooltip-variants") }} -
-- {{ t("settings.folder-mappings-mutually-exclusive") }} -
-+ {{ t("settings.folder-alias") }}: + {{ + t( + "settings.folder-mappings-tooltip-aliases", + ) + }} +
++ {{ + t("settings.platform-variant") + }}: + {{ + t( + "settings.folder-mappings-tooltip-variants", + ) + }} +
++ {{ + t( + "settings.folder-mappings-mutually-exclusive", + ) + }} +
+content", + } + + with patch( + "endpoints.walkthrough.fetch_walkthrough", + AsyncMock(return_value=fake_result), + ): + response = client.post( + "/api/walkthroughs/fetch", + headers={"Authorization": f"Bearer {access_token}"}, + json={"url": fake_result["url"], "format": "html"}, + ) + + assert response.status_code == status.HTTP_200_OK + payload = response.json() + assert payload["title"] == fake_result["title"] + assert payload["author"] == fake_result["author"] + assert payload["source"] == WalkthroughSource.GAMEFAQS + assert payload["format"] == WalkthroughFormat.HTML + assert payload["content"] == fake_result["content"] + + +def test_walkthrough_invalid_url(client, access_token): + with patch( + "endpoints.walkthrough.fetch_walkthrough", + AsyncMock(side_effect=InvalidWalkthroughURLError("bad url")), + ): + response = client.post( + "/api/walkthroughs/fetch", + headers={"Authorization": f"Bearer {access_token}"}, + json={"url": "https://example.com", "format": "html"}, + ) + + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +def test_walkthrough_not_found(client, access_token): + with patch( + "endpoints.walkthrough.fetch_walkthrough", + AsyncMock(side_effect=WalkthroughContentNotFound("missing content")), + ): + response = client.post( + "/api/walkthroughs/fetch", + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "url": "https://gamefaqs.gamespot.com/snes/563504-secret-of-mana/faqs/55474", + "format": "text", + }, + ) + + assert response.status_code == status.HTTP_404_NOT_FOUND + + +def test_create_walkthrough_for_rom(client, access_token, rom): + fake_result = { + "url": "https://gamefaqs.gamespot.com/snes/guide/faqs/1", + "title": "Guide title", + "author": "Guide author", + "source": WalkthroughSource.GAMEFAQS, + "format": WalkthroughFormat.HTML, + "content": "
content", + } + with patch( + "endpoints.walkthrough.fetch_walkthrough", + AsyncMock(return_value=fake_result), + ): + response = client.post( + f"/api/walkthroughs/roms/{rom.id}", + headers={"Authorization": f"Bearer {access_token}"}, + json={"url": fake_result["url"]}, + ) + + assert response.status_code == status.HTTP_201_CREATED + data = response.json() + assert data["rom_id"] == rom.id + assert data["url"] == fake_result["url"] + assert data["title"] == fake_result["title"] + assert data["author"] == fake_result["author"] + assert data["source"] == WalkthroughSource.GAMEFAQS + + # Fetch list + list_response = client.get( + f"/api/walkthroughs/roms/{rom.id}", + headers={"Authorization": f"Bearer {access_token}"}, + ) + assert list_response.status_code == status.HTTP_200_OK + items = list_response.json() + assert len(items) == 1 + assert items[0]["id"] == data["id"] + assert items[0]["title"] == fake_result["title"] + assert items[0]["author"] == fake_result["author"] + + # Delete + delete_response = client.delete( + f"/api/walkthroughs/{data['id']}", + headers={"Authorization": f"Bearer {access_token}"}, + ) + assert delete_response.status_code == status.HTTP_200_OK diff --git a/backend/tests/handler/test_walkthrough_handler.py b/backend/tests/handler/test_walkthrough_handler.py new file mode 100644 index 000000000..c5a9d347f --- /dev/null +++ b/backend/tests/handler/test_walkthrough_handler.py @@ -0,0 +1,83 @@ +from contextlib import asynccontextmanager + +import httpx +import pytest + +from handler.walkthrough_handler import ( + InvalidWalkthroughURLError, + WalkthroughContentNotFound, + WalkthroughFormat, + WalkthroughSource, + fetch_walkthrough, + is_gamefaqs_url, +) +from utils.context import ctx_httpx_client + + +@asynccontextmanager +async def mock_http_client(responses: dict[str, str]): + async def handler(request: httpx.Request) -> httpx.Response: + body = responses.get(str(request.url)) + if body is None: + return httpx.Response(status_code=404) + return httpx.Response(status_code=200, text=body) + + client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + token = ctx_httpx_client.set(client) + try: + yield + finally: + ctx_httpx_client.reset(token) + await client.aclose() + + +def test_url_matchers(): + assert is_gamefaqs_url( + "https://gamefaqs.gamespot.com/snes/563504-secret-of-mana/faqs/55474" + ) + assert not is_gamefaqs_url("https://example.com/guide") + assert not is_gamefaqs_url( + "https://steamcommunity.com/sharedfiles/filedetails/?id=3579263600" + ) + + +@pytest.mark.asyncio +async def test_fetch_gamefaqs_html_format(): + url = "https://gamefaqs.gamespot.com/snes/563504-secret-of-mana/faqs/55474" + html = """ +
First section
Second section
{{ content }}
+ {{ content }}
- tags
+ pre_tags = soup.find_all("pre")
+ if (
+ pre_tags and len(pre_tags) > 3
+ ): # Likely a GameFAQs page with multiple content blocks
+ # Extract content from all pre tags and join them
+ walkthrough_content = []
+ for pre in pre_tags:
+ content = pre.get_text().strip()
+ if content: # Only include non-empty pre tags
+ walkthrough_content.append(content)
+
+ if walkthrough_content:
+ # Join all content and wrap in a single pre tag
+ combined_content = "\n\n".join(walkthrough_content)
+ return f"{combined_content}"
+
+ # For other HTML, use the standard sanitization on the body content
_sanitize_node(soup)
return _serialize_html([soup])
diff --git a/frontend/src/components/Details/Walkthroughs/WalkthroughContent.vue b/frontend/src/components/Details/Walkthroughs/WalkthroughContent.vue
deleted file mode 100644
index 56915fb72..000000000
--- a/frontend/src/components/Details/Walkthroughs/WalkthroughContent.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
- storeHtmlProgress(walkthrough, e)"
- v-html="walkthrough.content"
- />
-
-
-
- PDF file missing
-
-
-
-
- {{ line || "\u00A0" }}
-
-
-
- Show all
-
-
-
-
-
-
-
diff --git a/frontend/src/components/Details/Walkthroughs/WalkthroughHeader.vue b/frontend/src/components/Details/Walkthroughs/WalkthroughHeader.vue
deleted file mode 100644
index 95c655942..000000000
--- a/frontend/src/components/Details/Walkthroughs/WalkthroughHeader.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
-
- {{ walkthrough.source }}
-
-
-
- {{ walkthrough.title?.split("by")[0] || walkthrough.url }}
-
-
- By {{ walkthrough.author }}
- {{ walkthrough.url }}
-
-
-
-
-
-
-
- {{ getProgressLabel(walkthrough) }}
-
-
-
-
-
-
diff --git a/frontend/src/components/Details/Walkthroughs/WalkthroughModal.vue b/frontend/src/components/Details/Walkthroughs/WalkthroughModal.vue
new file mode 100644
index 000000000..88da022a1
--- /dev/null
+++ b/frontend/src/components/Details/Walkthroughs/WalkthroughModal.vue
@@ -0,0 +1,267 @@
+
+
+
+
+
+
+
+
+ {{ walkthrough.source }}
+
+
+
+ {{ walkthrough.title?.split("by")[0] || walkthrough.url }}
+
+
+ By {{ walkthrough.author }}
+ {{ walkthrough.url }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ storeHtmlProgress(walkthrough, e)"
+ v-html="walkthrough.content"
+ />
+
+
+
+
+
+
+
+
+
+ PDF file is missing or could not be loaded.
+
+
+
+
+
+
+
+ {{ line || "\u00A0" }}
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/Details/Walkthroughs/WalkthroughProgress.vue b/frontend/src/components/Details/Walkthroughs/WalkthroughProgress.vue
new file mode 100644
index 000000000..5d34975ef
--- /dev/null
+++ b/frontend/src/components/Details/Walkthroughs/WalkthroughProgress.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+ {{ getProgressDisplay(walkthrough).text }}
+
+
+
+
+
diff --git a/frontend/src/components/Details/Walkthroughs/Walkthroughs.vue b/frontend/src/components/Details/Walkthroughs/Walkthroughs.vue
index 5c21d11f0..0b0c49fcd 100644
--- a/frontend/src/components/Details/Walkthroughs/Walkthroughs.vue
+++ b/frontend/src/components/Details/Walkthroughs/Walkthroughs.vue
@@ -2,8 +2,8 @@
import { computed, ref } from "vue";
import type { Walkthrough } from "@/composables/useWalkthrough";
import type { DetailedRom } from "@/stores/roms";
-import WalkthroughContent from "./WalkthroughContent.vue";
-import WalkthroughHeader from "./WalkthroughHeader.vue";
+import WalkthroughModal from "./WalkthroughModal.vue";
+import WalkthroughProgress from "./WalkthroughProgress.vue";
const props = defineProps<{
rom: DetailedRom;
@@ -13,27 +13,110 @@ const walkthroughs = computed(
() => (props.rom.walkthroughs || []) as Walkthrough[],
);
-const openPanels = ref([]);
+const selectedWalkthrough = ref(null);
+const isModalOpen = ref(false);
+const isLoading = ref(false);
-const isOpen = (id: number) => openPanels.value.includes(id);
+const openWalkthrough = async (walkthrough: Walkthrough) => {
+ isLoading.value = true;
+ try {
+ selectedWalkthrough.value = walkthrough;
+ isModalOpen.value = true;
+ } finally {
+ isLoading.value = false;
+ }
+};
+
+const closeModal = () => {
+ isModalOpen.value = false;
+ selectedWalkthrough.value = null;
+};
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
+ mdi-book-open-page-variant
+
+ No walkthroughs available
+
+ Add walkthroughs to help guide you through this game.
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ wt.source }}
+
+
+
+ {{ wt.title?.split("by")[0] || wt.url }}
+
+
+ By {{ wt.author }}
+ {{ wt.url }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/components/common/Game/Dialog/EditRom/WalkthroughPanel.vue b/frontend/src/components/common/Game/Dialog/EditRom/WalkthroughPanel.vue
index 394e1fd91..aced286a9 100644
--- a/frontend/src/components/common/Game/Dialog/EditRom/WalkthroughPanel.vue
+++ b/frontend/src/components/common/Game/Dialog/EditRom/WalkthroughPanel.vue
@@ -31,7 +31,7 @@ const { mdAndUp } = useDisplay();
Walkthrough
-
+
-
+
+
+ OR
+
+
-
+
diff --git a/frontend/src/composables/useWalkthrough.ts b/frontend/src/composables/useWalkthrough.ts
index 181ddb766..7a9ee0891 100644
--- a/frontend/src/composables/useWalkthrough.ts
+++ b/frontend/src/composables/useWalkthrough.ts
@@ -35,6 +35,7 @@ const CONFIG = {
// Utility functions
/**
* Splits walkthrough content into lines and caches the result
+ * Reformats text by preserving paragraph breaks but removing arbitrary line breaks
*/
function getWalkthroughLines(
walkthrough: Walkthrough,
@@ -44,7 +45,31 @@ function getWalkthroughLines(
return cache.get(walkthrough.id)!;
}
- const lines = walkthrough.content ? walkthrough.content.split(/\r?\n/) : [];
+ if (!walkthrough.content) {
+ const emptyLines: string[] = [];
+ cache.set(walkthrough.id, emptyLines);
+ return emptyLines;
+ }
+
+ let lines: string[];
+
+ if (walkthrough.format === "text") {
+ // For text files, reformat to improve readability
+ // Split into paragraphs (double line breaks)
+ const paragraphs = walkthrough.content.split(/\r?\n\s*\r?\n/);
+
+ lines = paragraphs
+ .map((paragraph) => {
+ // Within each paragraph, replace single line breaks with spaces
+ // but preserve the paragraph structure
+ return paragraph.replace(/\r?\n/g, " ").trim();
+ })
+ .filter((paragraph) => paragraph.length > 0); // Remove empty paragraphs
+ } else {
+ // For HTML and other formats, keep original line splitting
+ lines = walkthrough.content.split(/\r?\n/);
+ }
+
cache.set(walkthrough.id, lines);
return lines;
}
@@ -64,15 +89,6 @@ function calculateVisibleLineCount(
return currentVisible ?? savedLines ?? fallback;
}
-/**
- * Builds PDF URL from walkthrough file path
- */
-function buildPdfUrl(walkthrough: Walkthrough): string {
- return walkthrough.file_path
- ? `${FRONTEND_RESOURCES_PATH}/${walkthrough.file_path}`
- : "";
-}
-
/**
* Enhanced walkthrough composable with better organization and type safety
*/
@@ -119,9 +135,6 @@ export function useWalkthrough({
return currentVisible < totalLines;
}
- /**
- * Shows more content by incrementing visible lines
- */
function showMore(walkthrough: Walkthrough): void {
const totalLines = getWalkthroughLines(walkthrough, lineCache).length;
const currentVisible =
@@ -134,53 +147,52 @@ export function useWalkthrough({
visibleLines.value[walkthrough.id] = nextVisible;
}
- /**
- * Shows all content at once
- */
- function showAll(walkthrough: Walkthrough): void {
- const totalLines = getWalkthroughLines(walkthrough, lineCache).length;
- visibleLines.value[walkthrough.id] = totalLines;
- }
-
- /**
- * Gets PDF URL for the walkthrough
- */
function getPdfUrl(walkthrough: Walkthrough): string {
- return buildPdfUrl(walkthrough);
+ return walkthrough.file_path
+ ? `${FRONTEND_RESOURCES_PATH}/${walkthrough.file_path}`
+ : "";
}
// Progress tracking
- /**
- * Calculates progress percentage for a walkthrough
- */
function getProgressPercent(walkthrough: Walkthrough): number {
+ const storedProgress = progressData.value[walkthrough.id];
+ if (storedProgress?.percent !== undefined) {
+ return storedProgress.percent;
+ }
+
if (walkthrough.format === "text") {
const totalLines =
getWalkthroughLines(walkthrough, lineCache).length || 1;
- const currentVisible = visibleLines.value[walkthrough.id] ?? 0;
+ // Use stored lines progress or current visible lines, fallback to 0
+ const savedLines = storedProgress?.lines ?? 0;
+ const currentVisible = visibleLines.value[walkthrough.id] ?? savedLines;
return Math.min(
CONFIG.MAX_PROGRESS_PERCENT,
Math.round((currentVisible / totalLines) * CONFIG.MAX_PROGRESS_PERCENT),
);
}
- // For HTML and PDF formats, use stored percentage
- return progressData.value[walkthrough.id]?.percent ?? 0;
+ // For HTML and PDF formats without stored data, return 0
+ return 0;
}
- /**
- * Gets formatted progress label
- */
- function getProgressLabel(walkthrough: Walkthrough): string {
+ const getProgressDisplay = (walkthrough: Walkthrough) => {
const percent = getProgressPercent(walkthrough);
- return percent >= CONFIG.COMPLETION_THRESHOLD
- ? "Completed"
- : `${percent}% read`;
- }
- /**
- * Updates stored progress for a walkthrough
- */
+ return {
+ text: percent >= CONFIG.COMPLETION_THRESHOLD ? "Completed" : percent,
+ value: percent,
+ color:
+ percent === 0
+ ? "grey"
+ : percent < 50
+ ? "orange"
+ : percent < 100
+ ? "blue"
+ : "success",
+ };
+ };
+
function updateProgress(
walkthroughId: number,
updates: Partial,
@@ -195,9 +207,7 @@ export function useWalkthrough({
}
// Event handlers
- /**
- * Handles scroll events for text walkthroughs
- */
+
function handleScroll(walkthrough: Walkthrough, event: Event): void {
const target = event.target as HTMLElement;
if (!target) return;
@@ -221,9 +231,6 @@ export function useWalkthrough({
});
}
- /**
- * Stores HTML scroll progress
- */
function storeHtmlProgress(walkthrough: Walkthrough, event: Event): void {
const target = event.target as HTMLElement;
if (!target) return;
@@ -244,9 +251,6 @@ export function useWalkthrough({
}
// Element reference management
- /**
- * Sets or removes content element reference
- */
function setContentRef(id: number, element: HTMLElement | null): void {
if (element) {
contentRefs.set(id, element);
@@ -274,11 +278,8 @@ export function useWalkthrough({
return {
getVisibleText,
- canShowMore,
- showMore,
- showAll,
getPdfUrl,
- getProgressLabel,
+ getProgressDisplay,
handleScroll,
storeHtmlProgress,
setContentRef,
From aa65c8842456092b6b803bc98b733c71fbea8ace Mon Sep 17 00:00:00 2001
From: SaraVieira
Date: Thu, 29 Jan 2026 19:10:01 +0000
Subject: [PATCH 18/21] revert pyrtest change
---
backend/pytest.ini | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/backend/pytest.ini b/backend/pytest.ini
index 1398b4252..98b3089fb 100644
--- a/backend/pytest.ini
+++ b/backend/pytest.ini
@@ -3,10 +3,10 @@ asyncio_mode = auto
testpaths = tests
env =
ROMM_BASE_PATH=romm_test
- D:DB_HOST=127.0.0.1
- D:DB_NAME=romm_test
- D:DB_USER=romm_test
- D:DB_PASSWD=passwd
+ DB_HOST=127.0.0.1
+ DB_NAME=romm_test
+ DB_USER=romm_test
+ DB_PASSWD=passwd
ROMM_DB_DRIVER=mariadb
IGDB_CLIENT_ID=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
IGDB_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
From 3b85185a540c0f4e649557e97b6831412b297399 Mon Sep 17 00:00:00 2001
From: Sara Vieira
Date: Thu, 29 Jan 2026 19:10:42 +0000
Subject: [PATCH 19/21] Update backend/models/rom.py
Co-authored-by: Georges-Antoine Assi <3247106+gantoine@users.noreply.github.com>
---
backend/models/rom.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/models/rom.py b/backend/models/rom.py
index 76e608103..27497d237 100644
--- a/backend/models/rom.py
+++ b/backend/models/rom.py
@@ -264,7 +264,7 @@ class Rom(BaseModel):
lazy="raise", back_populates="rom"
)
rom_users: Mapped[list[RomUser]] = relationship(lazy="raise", back_populates="rom")
- walkthroughs: Mapped[list["Walkthrough"]] = relationship(
+ walkthroughs: Mapped[list[Walkthrough]] = relationship(
"Walkthrough", lazy="raise", back_populates="rom", cascade="all, delete-orphan"
)
notes: Mapped[list[RomNote]] = relationship(lazy="raise", back_populates="rom")
From 5c9ab0abcfc06799862811a233c1cac4db0f75d1 Mon Sep 17 00:00:00 2001
From: SaraVieira
Date: Thu, 29 Jan 2026 21:51:56 +0000
Subject: [PATCH 20/21] reuse WalkthroughSchema
---
backend/endpoints/walkthrough.py | 30 +++++++-----------------------
1 file changed, 7 insertions(+), 23 deletions(-)
diff --git a/backend/endpoints/walkthrough.py b/backend/endpoints/walkthrough.py
index 56abae54a..e53450baf 100644
--- a/backend/endpoints/walkthrough.py
+++ b/backend/endpoints/walkthrough.py
@@ -1,10 +1,10 @@
-from datetime import datetime
from pathlib import Path
from fastapi import HTTPException, Request, UploadFile, status
from pydantic import BaseModel, ConfigDict, Field
from decorators.auth import protected_route
+from endpoints.responses.rom import WalkthroughSchema
from handler.auth.constants import Scope
from handler.database import db_rom_handler, db_walkthrough_handler
from handler.filesystem import fs_resource_handler
@@ -77,22 +77,6 @@ class WalkthroughCreateRequest(BaseModel):
url: str = Field(..., description="Walkthrough URL from GameFAQs")
-class StoredWalkthroughResponse(BaseModel):
- id: int
- rom_id: int
- url: str
- title: str | None
- author: str | None
- source: WalkthroughSource
- format: WalkthroughFormat
- file_path: str | None
- content: str
- created_at: datetime
- updated_at: datetime
-
- model_config = ConfigDict(use_enum_values=True, from_attributes=True)
-
-
@protected_route(
router.post, "/fetch", [Scope.ROMS_READ], status_code=status.HTTP_200_OK
)
@@ -122,14 +106,14 @@ def _detect_format_from_extension(filename: str) -> WalkthroughFormat:
def list_walkthroughs_for_rom(
request: Request, # noqa: ARG001 - required for authentication decorator
rom_id: int,
-) -> list[StoredWalkthroughResponse]:
+) -> list[WalkthroughSchema]:
rom = db_rom_handler.get_rom(rom_id)
if not rom:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail="Rom not found"
)
walkthroughs = db_walkthrough_handler.get_walkthroughs_for_rom(rom_id)
- return [StoredWalkthroughResponse.model_validate(wt) for wt in walkthroughs]
+ return [WalkthroughSchema.model_validate(wt) for wt in walkthroughs]
@protected_route(
@@ -142,7 +126,7 @@ async def create_walkthrough_for_rom(
request: Request, # noqa: ARG001 - required for authentication decorator
rom_id: int,
payload: WalkthroughCreateRequest,
-) -> StoredWalkthroughResponse:
+) -> WalkthroughSchema:
rom = db_rom_handler.get_rom(rom_id)
if not rom:
raise HTTPException(
@@ -161,7 +145,7 @@ async def create_walkthrough_for_rom(
content=result["content"],
)
saved = db_walkthrough_handler.add_or_update_walkthrough(walkthrough)
- return StoredWalkthroughResponse.model_validate(saved)
+ return WalkthroughSchema.model_validate(saved)
@protected_route(
@@ -176,7 +160,7 @@ async def upload_walkthrough_for_rom(
file: UploadFile,
title: str | None = None,
author: str | None = None,
-) -> StoredWalkthroughResponse:
+) -> WalkthroughSchema:
rom = db_rom_handler.get_rom(rom_id)
if not rom:
raise HTTPException(
@@ -259,7 +243,7 @@ async def upload_walkthrough_for_rom(
detail="Failed to store walkthrough file.",
) from e
- return StoredWalkthroughResponse.model_validate(saved)
+ return WalkthroughSchema.model_validate(saved)
@protected_route(router.delete, "/{walkthrough_id}", [Scope.ROMS_WRITE])
From c9dfe9192bc7fdec087e525d1f64f11b903ac206 Mon Sep 17 00:00:00 2001
From: SaraVieira
Date: Thu, 29 Jan 2026 22:53:11 +0000
Subject: [PATCH 21/21] infer extension from file
---
...4_walkthroughs.py => 0068_walkthroughs.py} | 13 ++++---------
backend/endpoints/responses/rom.py | 19 ++++++++++++++++++-
backend/endpoints/walkthrough.py | 2 --
backend/models/walkthrough.py | 5 +----
.../UserInterface/LanguageSelector.vue | 10 +---------
5 files changed, 24 insertions(+), 25 deletions(-)
rename backend/alembic/versions/{0064_walkthroughs.py => 0068_walkthroughs.py} (83%)
diff --git a/backend/alembic/versions/0064_walkthroughs.py b/backend/alembic/versions/0068_walkthroughs.py
similarity index 83%
rename from backend/alembic/versions/0064_walkthroughs.py
rename to backend/alembic/versions/0068_walkthroughs.py
index 2828f53c5..2695bfe2e 100644
--- a/backend/alembic/versions/0064_walkthroughs.py
+++ b/backend/alembic/versions/0068_walkthroughs.py
@@ -1,7 +1,7 @@
"""Add walkthroughs table
-Revision ID: 0064_walkthroughs
-Revises: 0063_roms_metadata_player_count
+Revision ID: 0068_walkthroughs
+Revises: 0067_romfile_category_enum_cheat
Create Date: 2026-01-04 18:40:00.000000
"""
@@ -12,8 +12,8 @@
from sqlalchemy.dialects import mysql
# revision identifiers, used by Alembic.
-revision: str = "0064_walkthroughs"
-down_revision: Union[str, None] = "0063_roms_metadata_player_count"
+revision: str = "0068_walkthroughs"
+down_revision: Union[str, None] = "0067_romfile_category_enum_cheat"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
@@ -33,11 +33,6 @@ def upgrade() -> None:
sa.Enum("GAMEFAQS", "UPLOAD", name="walkthroughsource"),
nullable=False,
),
- sa.Column(
- "format",
- sa.Enum("html", "text", "pdf", name="walkthroughformat"),
- nullable=False,
- ),
sa.Column("file_path", sa.String(length=1000), nullable=True),
sa.Column(
"content",
diff --git a/backend/endpoints/responses/rom.py b/backend/endpoints/responses/rom.py
index bc4a2c434..ca718f9e8 100644
--- a/backend/endpoints/responses/rom.py
+++ b/backend/endpoints/responses/rom.py
@@ -2,6 +2,7 @@
import re
from datetime import datetime, timezone
+from pathlib import Path
from typing import NotRequired, TypedDict, get_type_hints
from fastapi import Request
@@ -108,7 +109,6 @@ class WalkthroughSchema(BaseModel):
title: str | None
author: str | None
source: WalkthroughSource
- format: WalkthroughFormat
file_path: str | None
content: str
created_at: datetime
@@ -118,6 +118,23 @@ class Config:
from_attributes = True
use_enum_values = True
+ @computed_field # type: ignore[misc]
+ @property
+ def format(self) -> WalkthroughFormat:
+ candidate = self.file_path or self.url or ""
+ ext = Path(candidate).suffix.lower()
+ if ext == ".pdf":
+ return WalkthroughFormat.PDF
+ if ext in {".html", ".htm"}:
+ return WalkthroughFormat.HTML
+ if ext in {".txt", ".text", ".md"}:
+ return WalkthroughFormat.TEXT
+ if self.source == WalkthroughSource.GAMEFAQS:
+ return WalkthroughFormat.TEXT
+ if self.content.lstrip().startswith("<"):
+ return WalkthroughFormat.HTML
+ return WalkthroughFormat.TEXT
+
def rom_user_schema_factory() -> RomUserSchema:
now = datetime.now(timezone.utc)
diff --git a/backend/endpoints/walkthrough.py b/backend/endpoints/walkthrough.py
index e53450baf..f13a5b000 100644
--- a/backend/endpoints/walkthrough.py
+++ b/backend/endpoints/walkthrough.py
@@ -140,7 +140,6 @@ async def create_walkthrough_for_rom(
title=result.get("title"),
author=result.get("author"),
source=result["source"],
- format=result["format"],
file_path=None,
content=result["content"],
)
@@ -215,7 +214,6 @@ async def upload_walkthrough_for_rom(
title=title or Path(filename).stem,
author=author,
source=WalkthroughSource.UPLOAD,
- format=fmt,
content=content,
file_path=None,
)
diff --git a/backend/models/walkthrough.py b/backend/models/walkthrough.py
index 3dc4e3c8a..4e0c4e576 100644
--- a/backend/models/walkthrough.py
+++ b/backend/models/walkthrough.py
@@ -4,7 +4,7 @@
from sqlalchemy.dialects import mysql
from sqlalchemy.orm import Mapped, mapped_column, relationship
-from handler.walkthrough_handler import WalkthroughFormat, WalkthroughSource
+from handler.walkthrough_handler import WalkthroughSource
from models.base import BaseModel
@@ -19,9 +19,6 @@ class Walkthrough(BaseModel):
source: Mapped[WalkthroughSource] = mapped_column(
Enum(WalkthroughSource, values_callable=lambda e: [item.value for item in e])
)
- format: Mapped[WalkthroughFormat] = mapped_column(
- Enum(WalkthroughFormat, values_callable=lambda e: [item.value for item in e])
- )
file_path: Mapped[str | None] = mapped_column(String(length=1000), default=None)
content: Mapped[str] = mapped_column(Text().with_variant(mysql.LONGTEXT(), "mysql"))
diff --git a/frontend/src/components/Settings/UserInterface/LanguageSelector.vue b/frontend/src/components/Settings/UserInterface/LanguageSelector.vue
index f837cd881..92c8716b1 100644
--- a/frontend/src/components/Settings/UserInterface/LanguageSelector.vue
+++ b/frontend/src/components/Settings/UserInterface/LanguageSelector.vue
@@ -7,16 +7,8 @@ import storeLanguage from "@/stores/language";
const { locale } = useI18n();
const languageStore = storeLanguage();
const { languages, selectedLanguage } = storeToRefs(languageStore);
-const localeStorage = useLocalStorage("settings.locale", "");
-withDefaults(
- defineProps<{
- density: "comfortable" | "compact" | "default";
- }>(),
- {
- density: "default",
- },
-);
+const { locale: localeStorage } = useUISettings();
withDefaults(
defineProps<{