mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Craete image to webp task
This commit is contained in:
@@ -13,6 +13,7 @@ from handler.auth.constants import Scope
|
||||
from handler.redis_handler import low_prio_queue
|
||||
from rq.job import Job
|
||||
from tasks.manual.cleanup_orphaned_resources import cleanup_orphaned_resources_task
|
||||
from tasks.scheduled.convert_images_to_webp import convert_images_to_webp_task
|
||||
from tasks.scheduled.scan_library import scan_library_task
|
||||
from tasks.scheduled.update_launchbox_metadata import update_launchbox_metadata_task
|
||||
from tasks.scheduled.update_switch_titledb import update_switch_titledb_task
|
||||
@@ -28,6 +29,7 @@ scheduled_tasks: dict[str, Task] = {
|
||||
"scan_library": scan_library_task,
|
||||
"update_launchbox_metadata": update_launchbox_metadata_task,
|
||||
"update_switch_titledb": update_switch_titledb_task,
|
||||
"convert_images_to_webp": convert_images_to_webp_task,
|
||||
}
|
||||
|
||||
manual_tasks: dict[str, Task] = {
|
||||
|
||||
@@ -50,6 +50,48 @@ class FSResourcesHandler(FSHandler):
|
||||
|
||||
small_img.save(save_path)
|
||||
|
||||
def _create_webp_version(self, image_path: Path, quality: int = 85) -> Path | None:
|
||||
"""Create a WebP version of the given image file.
|
||||
Args:
|
||||
image_path: Path to the original image file
|
||||
quality: WebP quality (0-100, default 85)
|
||||
Returns:
|
||||
Path to the created WebP file
|
||||
"""
|
||||
webp_path = image_path.with_suffix(".webp")
|
||||
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
# Convert to RGB if necessary (WebP doesn't support RGBA)
|
||||
if img.mode in ("RGBA", "LA", "P"):
|
||||
# Create white background for transparent images
|
||||
background = Image.new("RGB", img.size, (255, 255, 255))
|
||||
if img.mode == "P":
|
||||
img = img.convert("RGBA")
|
||||
background.paste(
|
||||
img, mask=img.split()[-1] if img.mode == "RGBA" else None
|
||||
)
|
||||
img = background
|
||||
elif img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
|
||||
img.save(webp_path, "WEBP", quality=quality, optimize=True)
|
||||
log.info(f"Created WebP version: {webp_path}")
|
||||
return webp_path
|
||||
except Exception as exc:
|
||||
log.error(f"Failed to create WebP version of {image_path}: {str(exc)}")
|
||||
return None
|
||||
|
||||
def _get_webp_path(self, original_path: Path) -> Path | None:
|
||||
"""Get the WebP version path for a given image file.
|
||||
Args:
|
||||
original_path: Path to the original image file
|
||||
Returns:
|
||||
Path to WebP file if it exists, None otherwise
|
||||
"""
|
||||
webp_path = original_path.with_suffix(".webp")
|
||||
return webp_path if webp_path.exists() else None
|
||||
|
||||
async def _store_cover(
|
||||
self, entity: Rom | Collection, url_cover: str, size: CoverSize
|
||||
) -> None:
|
||||
|
||||
215
backend/tasks/scheduled/convert_images_to_webp.py
Normal file
215
backend/tasks/scheduled/convert_images_to_webp.py
Normal file
@@ -0,0 +1,215 @@
|
||||
"""Background task to convert existing images to WebP format."""
|
||||
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from config import RESOURCES_BASE_PATH
|
||||
from logger.logger import log
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from tasks.tasks import Task
|
||||
|
||||
|
||||
@dataclass
|
||||
class ConversionResult:
|
||||
"""Result of image conversion operation."""
|
||||
|
||||
success: bool
|
||||
processed_count: int
|
||||
error_count: int
|
||||
total_files: int
|
||||
errors: List[str]
|
||||
|
||||
|
||||
class ImageConverter:
|
||||
"""Handles image format conversion to WebP."""
|
||||
|
||||
# Supported image formats
|
||||
SUPPORTED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif", ".gif"}
|
||||
|
||||
# Image mode conversion mapping
|
||||
MODE_CONVERSIONS = {
|
||||
"P": "RGBA", # Palette-based to RGBA (preserves transparency)
|
||||
"LA": "RGBA", # Grayscale with alpha to RGBA
|
||||
"L": "RGB", # Grayscale to RGB
|
||||
"CMYK": "RGB", # CMYK to RGB
|
||||
"YCbCr": "RGB", # YCbCr to RGB
|
||||
}
|
||||
|
||||
def __init__(self, quality: int = 90):
|
||||
self.quality = quality
|
||||
|
||||
def _convert_image_mode(self, img: Image.Image) -> Image.Image:
|
||||
"""Convert image to appropriate mode for WebP conversion.
|
||||
Args:
|
||||
img: PIL Image object
|
||||
Returns:
|
||||
Converted PIL Image object
|
||||
"""
|
||||
if img.mode in ("RGB", "RGBA"):
|
||||
return img
|
||||
|
||||
target_mode = self.MODE_CONVERSIONS.get(img.mode, "RGB")
|
||||
return img.convert(target_mode)
|
||||
|
||||
def convert_to_webp(self, image_path: Path) -> bool:
|
||||
"""Convert a single image to WebP format.
|
||||
Args:
|
||||
image_path: Path to the source image
|
||||
Returns:
|
||||
True if conversion was successful, False otherwise
|
||||
"""
|
||||
webp_path = image_path.with_suffix(".webp")
|
||||
|
||||
# Skip if WebP already exists
|
||||
if webp_path.exists():
|
||||
log.debug(f"WebP already exists for {image_path}")
|
||||
return True
|
||||
|
||||
try:
|
||||
with Image.open(image_path) as img:
|
||||
# Convert image mode if necessary
|
||||
img = self._convert_image_mode(img)
|
||||
|
||||
# Save as WebP
|
||||
img.save(webp_path, "WEBP", quality=self.quality, optimize=True)
|
||||
log.info(f"Created WebP version: {webp_path}")
|
||||
return True
|
||||
|
||||
except Exception as exc:
|
||||
log.error(f"Failed to create WebP version of {image_path}: {str(exc)}")
|
||||
return False
|
||||
|
||||
|
||||
class ConvertImagesToWebPTask(Task):
|
||||
"""Task to convert existing images to WebP format."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
title="Convert images to WebP",
|
||||
description="Convert existing image files (PNG, JPG, BMP, TIFF, GIF) to WebP format for better performance",
|
||||
enabled=True,
|
||||
manual_run=True,
|
||||
cron_string=None,
|
||||
)
|
||||
self.resources_path = Path(RESOURCES_BASE_PATH)
|
||||
self.converter = ImageConverter()
|
||||
self._reset_counters()
|
||||
|
||||
def _reset_counters(self) -> None:
|
||||
"""Reset processing counters."""
|
||||
self.processed_count = 0
|
||||
self.error_count = 0
|
||||
self.errors: list[str] = []
|
||||
|
||||
def _find_convertible_images(self) -> List[Path]:
|
||||
"""Find all convertible images in the resources directory.
|
||||
Returns:
|
||||
List of paths to image files that can be converted to WebP
|
||||
"""
|
||||
if not self.resources_path.exists():
|
||||
log.warning(f"Resources path does not exist: {self.resources_path}")
|
||||
return []
|
||||
|
||||
image_files: list[Path] = []
|
||||
for ext in ImageConverter.SUPPORTED_EXTENSIONS:
|
||||
# Only convert cover images
|
||||
image_files.extend(self.resources_path.rglob(f"**/cover/*{ext}"))
|
||||
|
||||
return sorted(image_files) # Sort for consistent processing order
|
||||
|
||||
def _process_single_image(self, image_path: Path) -> bool:
|
||||
"""Process a single image file.
|
||||
Args:
|
||||
image_path: Path to the image file
|
||||
Returns:
|
||||
True if processing was successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Validate image file first
|
||||
with Image.open(image_path) as img:
|
||||
img.verify()
|
||||
|
||||
# Convert to WebP
|
||||
if self.converter.convert_to_webp(image_path):
|
||||
self.processed_count += 1
|
||||
return True
|
||||
else:
|
||||
self.error_count += 1
|
||||
self.errors.append(f"Conversion failed: {image_path}")
|
||||
return False
|
||||
|
||||
except (UnidentifiedImageError, OSError) as exc:
|
||||
log.warning(f"Skipping invalid image file {image_path}: {str(exc)}")
|
||||
self.error_count += 1
|
||||
self.errors.append(f"Invalid image file: {image_path} - {str(exc)}")
|
||||
return False
|
||||
except Exception as exc:
|
||||
log.error(f"Unexpected error processing {image_path}: {str(exc)}")
|
||||
self.error_count += 1
|
||||
self.errors.append(f"Unexpected error: {image_path} - {str(exc)}")
|
||||
return False
|
||||
|
||||
def _get_progress_message(self) -> str:
|
||||
"""Get current progress message."""
|
||||
return f"Processed: {self.processed_count}, Errors: {self.error_count}"
|
||||
|
||||
async def run(self) -> Dict[str, Any]:
|
||||
"""Run the image conversion task.
|
||||
Returns:
|
||||
Dictionary with task results
|
||||
"""
|
||||
log.info("Starting image to WebP conversion task")
|
||||
|
||||
# Find all convertible images
|
||||
image_files = self._find_convertible_images()
|
||||
total_files = len(image_files)
|
||||
|
||||
if total_files == 0:
|
||||
log.info("No convertible images found")
|
||||
return self._create_result_dict(total_files)
|
||||
|
||||
log.info(f"Found {total_files} image files to process")
|
||||
|
||||
# Reset counters
|
||||
self._reset_counters()
|
||||
|
||||
# Process images
|
||||
for i, image_file in enumerate(image_files, 1):
|
||||
self._process_single_image(image_file)
|
||||
|
||||
# Log progress periodically
|
||||
if i % 50 == 0 or i == total_files:
|
||||
log.info(
|
||||
f"Progress: {i}/{total_files} - {self._get_progress_message()}"
|
||||
)
|
||||
|
||||
# Yield control to prevent blocking
|
||||
if i % 10 == 0:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Log final results
|
||||
log.info(f"Image to WebP conversion completed. {self._get_progress_message()}")
|
||||
|
||||
return self._create_result_dict(total_files)
|
||||
|
||||
def _create_result_dict(self, total_files: int) -> Dict[str, Any]:
|
||||
"""Create the result dictionary.
|
||||
Args:
|
||||
total_files: Total number of files found
|
||||
Returns:
|
||||
Dictionary with task results
|
||||
"""
|
||||
return {
|
||||
"task": "convert_images_to_webp",
|
||||
"status": "completed",
|
||||
"processed_count": self.processed_count,
|
||||
"error_count": self.error_count,
|
||||
"total_files": total_files,
|
||||
"errors": self.errors[:10], # Limit error list to prevent huge responses
|
||||
}
|
||||
|
||||
|
||||
# Task instance
|
||||
convert_images_to_webp_task = ConvertImagesToWebPTask()
|
||||
@@ -16,7 +16,6 @@ const props = withDefaults(
|
||||
showRomCount?: boolean;
|
||||
withLink?: boolean;
|
||||
enable3DTilt?: boolean;
|
||||
src?: string;
|
||||
}>(),
|
||||
{
|
||||
transformScale: false,
|
||||
@@ -25,7 +24,6 @@ const props = withDefaults(
|
||||
showRomCount: false,
|
||||
withLink: false,
|
||||
enable3DTilt: false,
|
||||
src: "",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -35,6 +33,8 @@ const galleryViewStore = storeGalleryView();
|
||||
const memoizedCovers = ref({
|
||||
large: ["", ""],
|
||||
small: ["", ""],
|
||||
largeWebp: ["", ""],
|
||||
smallWebp: ["", ""],
|
||||
});
|
||||
|
||||
const collectionCoverImage = computed(() =>
|
||||
@@ -44,14 +44,6 @@ const collectionCoverImage = computed(() =>
|
||||
);
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.src) {
|
||||
memoizedCovers.value = {
|
||||
large: [props.src, props.src],
|
||||
small: [props.src, props.src],
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a regular collection with covers or a smart collection with covers
|
||||
const isRegularOrSmartWithCovers =
|
||||
(!("is_virtual" in props.collection) || !props.collection.is_virtual) &&
|
||||
@@ -68,6 +60,18 @@ watchEffect(() => {
|
||||
props.collection.path_cover_small || "",
|
||||
props.collection.path_cover_small || "",
|
||||
],
|
||||
largeWebp: [
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
smallWebp: [
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -86,6 +90,18 @@ watchEffect(() => {
|
||||
memoizedCovers.value = {
|
||||
large: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
small: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
largeWebp: [
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
smallWebp: [
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -96,13 +112,25 @@ watchEffect(() => {
|
||||
memoizedCovers.value = {
|
||||
large: [shuffledLarge[0], shuffledLarge[1]],
|
||||
small: [shuffledSmall[0], shuffledSmall[1]],
|
||||
largeWebp: [
|
||||
shuffledLarge[0].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
shuffledLarge[1].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
],
|
||||
smallWebp: [
|
||||
shuffledSmall[0].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
shuffledSmall[1].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const firstCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstLargeCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondLargeCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstSmallCover = computed(() => memoizedCovers.value.small[0]);
|
||||
const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
const firstLargeWebpCover = computed(() => memoizedCovers.value.largeWebp[0]);
|
||||
const secondLargeWebpCover = computed(() => memoizedCovers.value.largeWebp[1]);
|
||||
const firstSmallWebpCover = computed(() => memoizedCovers.value.smallWebp[0]);
|
||||
const secondSmallWebpCover = computed(() => memoizedCovers.value.smallWebp[1]);
|
||||
|
||||
// Tilt 3D effect logic
|
||||
interface TiltHTMLElement extends HTMLElement {
|
||||
@@ -202,28 +230,40 @@ onBeforeUnmount(() => {
|
||||
<template
|
||||
v-if="
|
||||
('is_virtual' in collection && collection.is_virtual) ||
|
||||
!collection.path_cover_large
|
||||
!collection.path_cover_large ||
|
||||
!collection.path_cover_small
|
||||
"
|
||||
>
|
||||
<div class="split-image first-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="firstCover"
|
||||
:lazy-src="firstSmallWebpCover"
|
||||
:src="firstLargeWebpCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
>
|
||||
<template #error>
|
||||
<v-img :lazy-src="firstSmallCover" :src="firstLargeCover" />
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
<div class="split-image second-image">
|
||||
<v-img
|
||||
cover
|
||||
:src="secondCover"
|
||||
:lazy-src="secondSmallWebpCover"
|
||||
:src="secondLargeWebpCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
>
|
||||
<template #error>
|
||||
<v-img :lazy-src="secondSmallCover" :src="secondLargeCover" />
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-img
|
||||
cover
|
||||
:src="src || collection.path_cover_large"
|
||||
:lazy-src="firstSmallWebpCover"
|
||||
:src="firstLargeWebpCover"
|
||||
:aspect-ratio="galleryViewStore.defaultAspectRatioCollection"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -16,6 +16,8 @@ const props = withDefaults(
|
||||
const memoizedCovers = ref({
|
||||
large: ["", ""],
|
||||
small: ["", ""],
|
||||
largeWebp: ["", ""],
|
||||
smallWebp: ["", ""],
|
||||
});
|
||||
|
||||
const collectionCoverImage = computed(() =>
|
||||
@@ -39,6 +41,18 @@ watchEffect(() => {
|
||||
props.collection.path_cover_small,
|
||||
props.collection.path_cover_small,
|
||||
],
|
||||
largeWebp: [
|
||||
props.collection.path_cover_large?.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
props.collection.path_cover_large?.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
smallWebp: [
|
||||
props.collection.path_cover_small?.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
props.collection.path_cover_small?.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -50,6 +64,18 @@ watchEffect(() => {
|
||||
memoizedCovers.value = {
|
||||
large: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
small: [collectionCoverImage.value, collectionCoverImage.value],
|
||||
largeWebp: [
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
smallWebp: [
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
collectionCoverImage.value.split(".").slice(0, -1).join(".") +
|
||||
".webp" || "",
|
||||
],
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -60,6 +86,14 @@ watchEffect(() => {
|
||||
memoizedCovers.value = {
|
||||
large: [shuffledLarge[0], shuffledLarge[1]],
|
||||
small: [shuffledSmall[0], shuffledSmall[1]],
|
||||
largeWebp: [
|
||||
shuffledLarge[0].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
shuffledLarge[1].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
],
|
||||
smallWebp: [
|
||||
shuffledSmall[0].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
shuffledSmall[1].split(".").slice(0, -1).join(".") + ".webp" || "",
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -67,6 +101,10 @@ const firstLargeCover = computed(() => memoizedCovers.value.large[0]);
|
||||
const secondLargeCover = computed(() => memoizedCovers.value.large[1]);
|
||||
const firstSmallCover = computed(() => memoizedCovers.value.small[0]);
|
||||
const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
const firstLargeWebpCover = computed(() => memoizedCovers.value.largeWebp[0]);
|
||||
const secondLargeWebpCover = computed(() => memoizedCovers.value.largeWebp[1]);
|
||||
const firstSmallWebpCover = computed(() => memoizedCovers.value.smallWebp[0]);
|
||||
const secondSmallWebpCover = computed(() => memoizedCovers.value.smallWebp[1]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -80,9 +118,26 @@ const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
"
|
||||
>
|
||||
<div class="split-image first-image">
|
||||
<v-img cover :src="firstLargeCover" :aspect-ratio="1 / 1">
|
||||
<v-img cover :src="firstLargeWebpCover" :aspect-ratio="1 / 1">
|
||||
<template #placeholder>
|
||||
<v-img cover eager :src="firstSmallCover" :aspect-ratio="1 / 1" />
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="firstSmallWebpCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</template>
|
||||
<template #error>
|
||||
<v-img cover :src="firstLargeCover" :aspect-ratio="1 / 1">
|
||||
<template #placeholder>
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="firstSmallCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
@@ -96,6 +151,18 @@ const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</template>
|
||||
<template #error>
|
||||
<v-img cover :src="secondLargeCover" :aspect-ratio="1 / 1">
|
||||
<template #placeholder>
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="secondSmallCover"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</v-img>
|
||||
</div>
|
||||
</template>
|
||||
@@ -109,6 +176,22 @@ const secondSmallCover = computed(() => memoizedCovers.value.small[1]);
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</template>
|
||||
<template #error>
|
||||
<v-img
|
||||
cover
|
||||
:src="collection.path_cover_large"
|
||||
:aspect-ratio="1 / 1"
|
||||
>
|
||||
<template #placeholder>
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="collection.path_cover_small"
|
||||
:aspect-ratio="1 / 1"
|
||||
/>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -142,6 +142,20 @@ const largeCover = computed(() =>
|
||||
const smallCover = computed(() =>
|
||||
romsStore.isSimpleRom(props.rom) ? props.rom.path_cover_small : "",
|
||||
);
|
||||
const largeWebpCover = computed(() =>
|
||||
romsStore.isSimpleRom(props.rom)
|
||||
? props.rom.path_cover_large
|
||||
? props.rom.path_cover_large.split(".").slice(0, -1).join(".") + ".webp"
|
||||
: ""
|
||||
: "",
|
||||
);
|
||||
const smallWebpCover = computed(() =>
|
||||
romsStore.isSimpleRom(props.rom)
|
||||
? props.rom.path_cover_small
|
||||
? props.rom.path_cover_small.split(".").slice(0, -1).join(".") + ".webp"
|
||||
: ""
|
||||
: "",
|
||||
);
|
||||
|
||||
const showNoteDialog = (event: MouseEvent | KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -220,7 +234,7 @@ onBeforeUnmount(() => {
|
||||
content-class="d-flex flex-column justify-space-between"
|
||||
:class="{ pointer: pointerOnHover }"
|
||||
:key="romsStore.isSimpleRom(rom) ? rom.updated_at : ''"
|
||||
:src="largeCover || fallbackCoverImage"
|
||||
:src="largeWebpCover || fallbackCoverImage"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
>
|
||||
<template v-bind="props" v-if="titleOnHover">
|
||||
@@ -344,18 +358,11 @@ onBeforeUnmount(() => {
|
||||
/>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
<template #error>
|
||||
<v-img
|
||||
cover
|
||||
:src="fallbackCoverImage"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
></v-img>
|
||||
</template>
|
||||
<template #placeholder>
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="smallCover || fallbackCoverImage"
|
||||
:src="smallWebpCover || fallbackCoverImage"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
>
|
||||
<template #placeholder>
|
||||
@@ -367,6 +374,30 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
<template #error>
|
||||
<v-img
|
||||
cover
|
||||
:src="largeCover || fallbackCoverImage"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
>
|
||||
<template #placeholder>
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="smallCover || fallbackCoverImage"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
/>
|
||||
</template>
|
||||
<template #error>
|
||||
<v-img
|
||||
cover
|
||||
eager
|
||||
:src="fallbackCoverImage"
|
||||
:aspect-ratio="computedAspectRatio"
|
||||
/>
|
||||
</template>
|
||||
</v-img>
|
||||
</template>
|
||||
</v-img>
|
||||
</v-hover>
|
||||
</v-card-text>
|
||||
|
||||
Reference in New Issue
Block a user