fix: fix various issues before merging
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from .schema import IMAGE_STATUS_PENDING
|
||||
|
||||
PLACEHOLDER_RE = re.compile(
|
||||
r"\{\{\{\{IMAGE:(.*?)\}\}\}\}|\{\{IMAGE:(.*?)\}\}",
|
||||
re.DOTALL,
|
||||
@@ -40,7 +42,7 @@ def build_initial_image_assets(
|
||||
"description": item["description"],
|
||||
"prompt": None,
|
||||
"url": None,
|
||||
"status": "pending",
|
||||
"status": IMAGE_STATUS_PENDING,
|
||||
"provider": provider,
|
||||
"style": style,
|
||||
"size": size,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
from dataclasses import dataclass
|
||||
|
||||
DEFAULT_LIBLIB_TEMPLATE_UUID = "5d7e67009b344550bc1aa6ccbfa1d7f4"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MemoirImageSettings:
|
||||
@@ -17,13 +19,19 @@ class MemoirImageSettings:
|
||||
def from_env(cls) -> "MemoirImageSettings":
|
||||
return cls(
|
||||
enabled=os.getenv("MEMOIR_IMAGE_ENABLED", "").lower() in {"1", "true", "yes"},
|
||||
max_per_chapter=int(os.getenv("MEMOIR_IMAGE_MAX_PER_CHAPTER", "2")),
|
||||
max_per_chapter=_get_int_env("MEMOIR_IMAGE_MAX_PER_CHAPTER", 2),
|
||||
provider=os.getenv("MEMOIR_IMAGE_PROVIDER", "liblib"),
|
||||
default_style=os.getenv("MEMOIR_IMAGE_STYLE_DEFAULT", "watercolor"),
|
||||
default_size=os.getenv("MEMOIR_IMAGE_SIZE_DEFAULT", "1280x720"),
|
||||
poll_interval_seconds=int(os.getenv("MEMOIR_IMAGE_POLL_INTERVAL", "3")),
|
||||
max_attempts=int(os.getenv("MEMOIR_IMAGE_MAX_ATTEMPTS", "60")),
|
||||
liblib_template_uuid=os.getenv(
|
||||
"LIBLIB_TEMPLATE_UUID", "5d7e67009b344550bc1aa6ccbfa1d7f4"
|
||||
),
|
||||
poll_interval_seconds=_get_int_env("MEMOIR_IMAGE_POLL_INTERVAL", 3),
|
||||
max_attempts=_get_int_env("MEMOIR_IMAGE_MAX_ATTEMPTS", 60),
|
||||
liblib_template_uuid=os.getenv("LIBLIB_TEMPLATE_UUID") or DEFAULT_LIBLIB_TEMPLATE_UUID,
|
||||
)
|
||||
|
||||
|
||||
def _get_int_env(name: str, default: int) -> int:
|
||||
value = os.getenv(name, str(default))
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return default
|
||||
|
||||
@@ -64,6 +64,9 @@ def resolve_image_storage_key(image: dict | None) -> str | None:
|
||||
|
||||
|
||||
class TencentCosStorageService:
|
||||
_instance: "TencentCosStorageService | None" = None
|
||||
_instance_config: tuple[str, str, str, str, str] | None = None
|
||||
|
||||
def __init__(self, secret_id: str, secret_key: str, region: str, bucket: str, base_url: str):
|
||||
self.secret_id = secret_id
|
||||
self.secret_key = secret_key
|
||||
@@ -91,10 +94,20 @@ class TencentCosStorageService:
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> "TencentCosStorageService":
|
||||
return cls(
|
||||
secret_id=os.getenv("TENCENT_COS_SECRET_ID", ""),
|
||||
secret_key=os.getenv("TENCENT_COS_SECRET_KEY", ""),
|
||||
region=os.getenv("TENCENT_COS_REGION", ""),
|
||||
bucket=os.getenv("TENCENT_COS_BUCKET", ""),
|
||||
base_url=os.getenv("TENCENT_COS_BASE_URL", ""),
|
||||
config = (
|
||||
os.getenv("TENCENT_COS_SECRET_ID", ""),
|
||||
os.getenv("TENCENT_COS_SECRET_KEY", ""),
|
||||
os.getenv("TENCENT_COS_REGION", ""),
|
||||
os.getenv("TENCENT_COS_BUCKET", ""),
|
||||
os.getenv("TENCENT_COS_BASE_URL", ""),
|
||||
)
|
||||
if cls._instance is None or cls._instance_config != config:
|
||||
cls._instance = cls(
|
||||
secret_id=config[0],
|
||||
secret_key=config[1],
|
||||
region=config[2],
|
||||
bucket=config[3],
|
||||
base_url=config[4],
|
||||
)
|
||||
cls._instance_config = config
|
||||
return cls._instance
|
||||
|
||||
@@ -2,23 +2,23 @@
|
||||
PDF 生成服务
|
||||
"""
|
||||
import logging
|
||||
import re
|
||||
from io import BytesIO
|
||||
from typing import List
|
||||
|
||||
import httpx
|
||||
from PIL import Image
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, PageBreak, Image as ReportLabImage
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
|
||||
from io import BytesIO
|
||||
from services.memoir_images.parser import PLACEHOLDER_RE
|
||||
from services.memoir_images.schema import IMAGE_STATUS_COMPLETED, normalize_image_assets
|
||||
from services.memoir_images.storage import TencentCosStorageService, resolve_image_storage_key
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PLACEHOLDER_RE = re.compile(r"\{\{\{\{IMAGE:.*?\}\}\}\}|\{\{IMAGE:.*?\}\}", re.DOTALL)
|
||||
|
||||
|
||||
def strip_image_placeholders(text: str) -> str:
|
||||
return PLACEHOLDER_RE.sub("", text or "").strip()
|
||||
@@ -35,7 +35,7 @@ def split_content_blocks(content: str, images: list[dict]) -> list[dict]:
|
||||
cleaned_before = strip_image_placeholders(before)
|
||||
if cleaned_before:
|
||||
blocks.append({"type": "text", "value": cleaned_before})
|
||||
if image.get("status") == "completed" and image.get("url"):
|
||||
if image.get("status") == IMAGE_STATUS_COMPLETED and image.get("url"):
|
||||
blocks.append({"type": "image", "url": image["url"]})
|
||||
cleaned_remaining = strip_image_placeholders(remaining)
|
||||
if cleaned_remaining:
|
||||
@@ -47,10 +47,10 @@ def _prepare_pdf_image_assets(images: list[dict]) -> list[dict]:
|
||||
storage = TencentCosStorageService.from_env()
|
||||
prepared_assets: list[dict] = []
|
||||
|
||||
for item in images or []:
|
||||
for item in normalize_image_assets(images):
|
||||
asset = dict(item)
|
||||
storage_key = resolve_image_storage_key(asset)
|
||||
if asset.get("status") == "completed" and storage_key:
|
||||
if asset.get("status") == IMAGE_STATUS_COMPLETED and storage_key:
|
||||
try:
|
||||
asset["url"] = storage.get_download_url(storage_key)
|
||||
except Exception as exc:
|
||||
@@ -60,6 +60,16 @@ def _prepare_pdf_image_assets(images: list[dict]) -> list[dict]:
|
||||
return prepared_assets
|
||||
|
||||
|
||||
def _fit_image_size(image_bytes: bytes, max_width: float, max_height: float) -> tuple[float, float]:
|
||||
with Image.open(BytesIO(image_bytes)) as image:
|
||||
width, height = image.size
|
||||
if width <= 0 or height <= 0:
|
||||
return max_width, max_height
|
||||
|
||||
scale = min(max_width / width, max_height / height)
|
||||
return width * scale, height * scale
|
||||
|
||||
|
||||
class PDFService:
|
||||
"""PDF 生成服务"""
|
||||
|
||||
@@ -140,7 +150,12 @@ class PDFService:
|
||||
image_bytes = await self._fetch_image_bytes(block["url"])
|
||||
if image_bytes:
|
||||
try:
|
||||
img = ReportLabImage(BytesIO(image_bytes), width=5 * inch, height=3.75 * inch)
|
||||
width, height = _fit_image_size(
|
||||
image_bytes,
|
||||
max_width=5 * inch,
|
||||
max_height=3.75 * inch,
|
||||
)
|
||||
img = ReportLabImage(BytesIO(image_bytes), width=width, height=height)
|
||||
story.append(img)
|
||||
story.append(Spacer(1, 0.2 * inch))
|
||||
except Exception as exc:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from api.services.memoir_images.prompting import MemoirImagePromptService
|
||||
from api.services.memoir_images.settings import MemoirImageSettings
|
||||
|
||||
|
||||
class MemoirImagePromptingTest(unittest.TestCase):
|
||||
def test_prompt_service_uses_category_style_and_plain_fallback_without_llm(self):
|
||||
def test_prompt_service_uses_english_fallback_without_llm(self):
|
||||
settings = MemoirImageSettings(
|
||||
enabled=True,
|
||||
max_per_chapter=2,
|
||||
@@ -28,7 +28,9 @@ class MemoirImagePromptingTest(unittest.TestCase):
|
||||
|
||||
self.assertEqual(result["style"], "watercolor")
|
||||
self.assertEqual(result["size"], "1024x1024")
|
||||
self.assertIn("奶奶坐在院子里的藤椅上", result["prompt"])
|
||||
self.assertIn("childhood memory", result["prompt"])
|
||||
self.assertIn("watercolor", result["prompt"])
|
||||
self.assertNotIn("奶奶坐在院子里的藤椅上", result["prompt"])
|
||||
self.assertIn("childhood", result["prompt_context"])
|
||||
|
||||
def test_prompt_service_parses_structured_llm_response(self):
|
||||
@@ -59,3 +61,32 @@ class MemoirImagePromptingTest(unittest.TestCase):
|
||||
self.assertEqual(result["prompt"], "A grandmother in a quiet courtyard, summer cicadas, soft watercolor")
|
||||
self.assertEqual(result["style"], "watercolor")
|
||||
self.assertEqual(result["size"], "1024x1024")
|
||||
|
||||
@patch("api.services.memoir_images.prompting.logger")
|
||||
def test_prompt_service_logs_warning_and_falls_back_when_llm_response_is_invalid(
|
||||
self, logger_mock
|
||||
):
|
||||
settings = MemoirImageSettings(
|
||||
enabled=True,
|
||||
max_per_chapter=2,
|
||||
provider="liblib",
|
||||
default_style="watercolor",
|
||||
default_size="1024x1024",
|
||||
poll_interval_seconds=3,
|
||||
max_attempts=20,
|
||||
liblib_template_uuid="tpl-uuid",
|
||||
)
|
||||
llm = Mock()
|
||||
llm.invoke.return_value.content = "not-json"
|
||||
service = MemoirImagePromptService(llm=llm, settings=settings)
|
||||
|
||||
result = service.build_prompt(
|
||||
chapter_title="童年的夏天",
|
||||
chapter_category="childhood",
|
||||
description="奶奶坐在院子里的藤椅上",
|
||||
context_excerpt="梧桐树下很安静,夏天总有蝉鸣。",
|
||||
)
|
||||
|
||||
self.assertIn("childhood memory", result["prompt"])
|
||||
self.assertNotIn("奶奶坐在院子里的藤椅上", result["prompt"])
|
||||
logger_mock.warning.assert_called_once()
|
||||
|
||||
33
api/tests/test_memoir_image_schema.py
Normal file
33
api/tests/test_memoir_image_schema.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import unittest
|
||||
|
||||
from api.services.memoir_images.schema import (
|
||||
IMAGE_STATUS_FAILED,
|
||||
IMAGE_STATUS_PENDING,
|
||||
normalize_image_asset,
|
||||
)
|
||||
|
||||
|
||||
class MemoirImageSchemaTest(unittest.TestCase):
|
||||
def test_normalize_image_asset_coerces_invalid_status_to_failed(self):
|
||||
asset = normalize_image_asset(
|
||||
{
|
||||
"index": 0,
|
||||
"placeholder": "{{IMAGE:南方小镇的青石板路}}",
|
||||
"description": "南方小镇的青石板路",
|
||||
"status": "mystery",
|
||||
"url": "https://cos.example.com/0.png",
|
||||
}
|
||||
)
|
||||
|
||||
self.assertEqual(asset["status"], IMAGE_STATUS_FAILED)
|
||||
self.assertEqual(asset["error"], "invalid image status: mystery")
|
||||
|
||||
def test_normalize_image_asset_requires_placeholder_and_description(self):
|
||||
asset = normalize_image_asset(
|
||||
{
|
||||
"index": 0,
|
||||
"status": IMAGE_STATUS_PENDING,
|
||||
}
|
||||
)
|
||||
|
||||
self.assertIsNone(asset)
|
||||
33
api/tests/test_memoir_image_settings.py
Normal file
33
api/tests/test_memoir_image_settings.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.services.memoir_images.settings import (
|
||||
DEFAULT_LIBLIB_TEMPLATE_UUID,
|
||||
MemoirImageSettings,
|
||||
)
|
||||
|
||||
|
||||
class MemoirImageSettingsTest(unittest.TestCase):
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"MEMOIR_IMAGE_MAX_PER_CHAPTER": "not-an-int",
|
||||
"MEMOIR_IMAGE_POLL_INTERVAL": "bad",
|
||||
"MEMOIR_IMAGE_MAX_ATTEMPTS": "oops",
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
def test_from_env_falls_back_to_defaults_for_invalid_integers(self):
|
||||
settings = MemoirImageSettings.from_env()
|
||||
|
||||
self.assertEqual(settings.max_per_chapter, 2)
|
||||
self.assertEqual(settings.poll_interval_seconds, 3)
|
||||
self.assertEqual(settings.max_attempts, 60)
|
||||
|
||||
@patch.dict(os.environ, {}, clear=False)
|
||||
def test_from_env_uses_shared_template_uuid_default(self):
|
||||
with patch.dict(os.environ, {"LIBLIB_TEMPLATE_UUID": ""}, clear=False):
|
||||
settings = MemoirImageSettings.from_env()
|
||||
|
||||
self.assertEqual(settings.liblib_template_uuid, DEFAULT_LIBLIB_TEMPLATE_UUID)
|
||||
@@ -1,3 +1,4 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
@@ -9,6 +10,29 @@ from api.services.memoir_images.storage import (
|
||||
|
||||
|
||||
class MemoirImageStorageTest(unittest.TestCase):
|
||||
@patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"TENCENT_COS_SECRET_ID": "id",
|
||||
"TENCENT_COS_SECRET_KEY": "key",
|
||||
"TENCENT_COS_REGION": "ap-shanghai",
|
||||
"TENCENT_COS_BUCKET": "memoir-1250000000",
|
||||
"TENCENT_COS_BASE_URL": "https://memoir-1250000000.cos.ap-shanghai.myqcloud.com",
|
||||
},
|
||||
clear=False,
|
||||
)
|
||||
@patch("api.services.memoir_images.storage.CosS3Client")
|
||||
def test_from_env_reuses_singleton_for_same_config(self, client_cls):
|
||||
TencentCosStorageService._instance = None
|
||||
TencentCosStorageService._instance_config = None
|
||||
client_cls.return_value = Mock()
|
||||
|
||||
first = TencentCosStorageService.from_env()
|
||||
second = TencentCosStorageService.from_env()
|
||||
|
||||
self.assertIs(first, second)
|
||||
client_cls.assert_called_once()
|
||||
|
||||
@patch("api.services.memoir_images.storage.CosS3Client")
|
||||
def test_upload_bytes_returns_persistent_cos_url(self, client_cls):
|
||||
client = Mock()
|
||||
|
||||
39
api/tests/test_memoir_tasks_redis.py
Normal file
39
api/tests/test_memoir_tasks_redis.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from api.tasks import memoir_tasks
|
||||
from api.tasks.memoir_tasks import (
|
||||
_acquire_chapter_lock,
|
||||
_release_chapter_lock,
|
||||
_update_task_status_sync,
|
||||
)
|
||||
|
||||
|
||||
class MemoirTasksRedisReuseTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
memoir_tasks._REDIS_CLIENTS.clear()
|
||||
|
||||
@patch("api.tasks.memoir_tasks.redis.from_url")
|
||||
def test_chapter_lock_helpers_reuse_same_redis_client(self, from_url_mock):
|
||||
client = Mock()
|
||||
client.set.return_value = True
|
||||
from_url_mock.return_value = client
|
||||
|
||||
self.assertTrue(_acquire_chapter_lock("user-1", "childhood"))
|
||||
_release_chapter_lock("user-1", "childhood")
|
||||
|
||||
self.assertEqual(from_url_mock.call_count, 1)
|
||||
client.set.assert_called_once()
|
||||
client.delete.assert_called_once()
|
||||
|
||||
@patch("api.tasks.memoir_tasks.redis.from_url")
|
||||
def test_task_status_updates_reuse_decode_response_client(self, from_url_mock):
|
||||
client = Mock()
|
||||
client.hget.return_value = None
|
||||
from_url_mock.return_value = client
|
||||
|
||||
_update_task_status_sync("user-1", "task-1", "running")
|
||||
_update_task_status_sync("user-1", "task-1", "success", {"processed": 1})
|
||||
|
||||
self.assertEqual(from_url_mock.call_count, 1)
|
||||
client.hset.assert_called()
|
||||
@@ -1,10 +1,64 @@
|
||||
from io import BytesIO
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, patch, MagicMock
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from api.services.pdf_service import PDFService
|
||||
|
||||
|
||||
class PDFServiceImagesTest(unittest.IsolatedAsyncioTestCase):
|
||||
@patch("api.services.pdf_service.ReportLabImage")
|
||||
@patch("api.services.pdf_service.httpx.AsyncClient")
|
||||
@patch("api.services.pdf_service.TencentCosStorageService")
|
||||
async def test_generate_pdf_preserves_image_aspect_ratio(
|
||||
self,
|
||||
storage_cls,
|
||||
async_client_cls,
|
||||
reportlab_image_cls,
|
||||
):
|
||||
image_buffer = BytesIO()
|
||||
Image.new("RGB", (2, 1), color="white").save(image_buffer, format="PNG")
|
||||
png_bytes = image_buffer.getvalue()
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = png_bytes
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.get.return_value = mock_response
|
||||
async_client_cls.return_value.__aenter__ = AsyncMock(return_value=mock_client)
|
||||
async_client_cls.return_value.__aexit__ = AsyncMock(return_value=False)
|
||||
storage = MagicMock()
|
||||
storage.get_download_url.return_value = "https://signed.example.com/0.png?sig=123"
|
||||
storage_cls.from_env.return_value = storage
|
||||
reportlab_image_cls.return_value = MagicMock()
|
||||
|
||||
service = PDFService()
|
||||
book = type("BookStub", (), {"title": "我的回忆录"})()
|
||||
chapter = type(
|
||||
"ChapterStub",
|
||||
(),
|
||||
{
|
||||
"title": "童年的夏天",
|
||||
"content": "{{{{IMAGE:南方小镇的青石板路}}}}",
|
||||
"images": [
|
||||
{
|
||||
"index": 0,
|
||||
"placeholder": "{{{{IMAGE:南方小镇的青石板路}}}}",
|
||||
"url": "https://life-echo-dev-1319381411.cos.ap-shanghai.myqcloud.com/memoirs/u1/c1/0.png",
|
||||
"storage_key": "memoirs/u1/c1/0.png",
|
||||
"status": "completed",
|
||||
}
|
||||
],
|
||||
},
|
||||
)()
|
||||
|
||||
await service.generate_pdf(book, [chapter])
|
||||
|
||||
_, kwargs = reportlab_image_cls.call_args
|
||||
self.assertAlmostEqual(kwargs["width"], 5 * 72)
|
||||
self.assertAlmostEqual(kwargs["height"], 2.5 * 72)
|
||||
|
||||
@patch("api.services.pdf_service.httpx.AsyncClient")
|
||||
@patch("api.services.pdf_service.TencentCosStorageService")
|
||||
async def test_generate_pdf_embeds_completed_images_and_removes_placeholders(
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.huaga.life_echo.network.models.ChapterDto
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_COMPLETED
|
||||
import com.huaga.life_echo.ui.components.common.MarkdownText
|
||||
import com.huaga.life_echo.ui.icons.AppIcons
|
||||
import com.huaga.life_echo.ui.theme.*
|
||||
@@ -173,7 +174,9 @@ private fun FilledChapterCard(
|
||||
) {
|
||||
val processedContent = TextUtils.removeImagePlaceholders(
|
||||
chapter.content,
|
||||
hasImages = chapter.images.any { it.status == "completed" && !it.url.isNullOrBlank() }
|
||||
hasImages = chapter.images.any {
|
||||
it.status == MEMOIR_IMAGE_STATUS_COMPLETED && !it.url.isNullOrBlank()
|
||||
}
|
||||
)
|
||||
|
||||
MarkdownText(
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.huaga.life_echo.network.models.ChapterContentDto
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_COMPLETED
|
||||
import com.huaga.life_echo.network.models.ChapterImageDto
|
||||
import com.huaga.life_echo.ui.components.common.MarkdownText
|
||||
import com.huaga.life_echo.ui.theme.AppTypography
|
||||
@@ -77,7 +78,7 @@ fun ChapterReadingView(
|
||||
MemoirInlineImage(
|
||||
image = block.image,
|
||||
onClick = {
|
||||
if (block.image.status == "completed" && !block.image.url.isNullOrBlank()) {
|
||||
if (block.image.status == MEMOIR_IMAGE_STATUS_COMPLETED && !block.image.url.isNullOrBlank()) {
|
||||
viewerImage = block.image
|
||||
}
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.huaga.life_echo.network.models.ChapterContentDto
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_COMPLETED
|
||||
import com.huaga.life_echo.network.models.ChapterImageDto
|
||||
import com.huaga.life_echo.ui.components.common.MarkdownText
|
||||
import com.huaga.life_echo.ui.icons.AppIcons
|
||||
@@ -91,7 +92,7 @@ fun FullTextReadingView(
|
||||
MemoirInlineImage(
|
||||
image = block.image,
|
||||
onClick = {
|
||||
if (block.image.status == "completed" && !block.image.url.isNullOrBlank()) {
|
||||
if (block.image.status == MEMOIR_IMAGE_STATUS_COMPLETED && !block.image.url.isNullOrBlank()) {
|
||||
viewerImage = block.image
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.huaga.life_echo.ui.components.memoir
|
||||
|
||||
import com.huaga.life_echo.network.models.ChapterImageDto
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_COMPLETED
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_PENDING
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_PROCESSING
|
||||
|
||||
sealed interface MemoirContentBlock {
|
||||
data class Text(val content: String) : MemoirContentBlock
|
||||
@@ -24,9 +27,12 @@ fun splitMemoirContent(content: String, images: List<ChapterImageDto>): List<Mem
|
||||
val parts = remaining.split(placeholder, limit = 2)
|
||||
val before = stripImagePlaceholders(parts.first())
|
||||
if (before.isNotBlank()) blocks += MemoirContentBlock.Text(before)
|
||||
if (image.status == "completed" && !image.url.isNullOrBlank()) {
|
||||
if (image.status == MEMOIR_IMAGE_STATUS_COMPLETED && !image.url.isNullOrBlank()) {
|
||||
blocks += MemoirContentBlock.Image(image)
|
||||
} else if (image.status == "pending" || image.status == "processing") {
|
||||
} else if (
|
||||
image.status == MEMOIR_IMAGE_STATUS_PENDING ||
|
||||
image.status == MEMOIR_IMAGE_STATUS_PROCESSING
|
||||
) {
|
||||
blocks += MemoirContentBlock.Image(image)
|
||||
}
|
||||
remaining = parts.getOrElse(1) { "" }
|
||||
|
||||
@@ -22,6 +22,9 @@ import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import com.huaga.life_echo.network.models.ChapterImageDto
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_COMPLETED
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_PENDING
|
||||
import com.huaga.life_echo.network.models.MEMOIR_IMAGE_STATUS_PROCESSING
|
||||
import com.huaga.life_echo.ui.theme.AppTypography
|
||||
import com.huaga.life_echo.ui.theme.LightPurple
|
||||
import com.huaga.life_echo.ui.theme.SlatePurple
|
||||
@@ -74,7 +77,7 @@ fun MemoirInlineImage(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
when (image.status) {
|
||||
"completed" -> SubcomposeAsyncImage(
|
||||
MEMOIR_IMAGE_STATUS_COMPLETED -> SubcomposeAsyncImage(
|
||||
model = image.url,
|
||||
contentDescription = image.description,
|
||||
contentScale = ContentScale.FillWidth,
|
||||
@@ -99,7 +102,7 @@ fun MemoirInlineImage(
|
||||
.clickable(onClick = onClick)
|
||||
.testTag("memoir-image-${image.index}")
|
||||
)
|
||||
"pending", "processing" -> MemoirImageLoadingPlaceholder(
|
||||
MEMOIR_IMAGE_STATUS_PENDING, MEMOIR_IMAGE_STATUS_PROCESSING -> MemoirImageLoadingPlaceholder(
|
||||
image = image,
|
||||
modifier = modifier,
|
||||
text = "图片生成中…",
|
||||
|
||||
Reference in New Issue
Block a user