perf: slim entries API + on-demand article loading for iOS Reader
Server: add slim query param (default true) to entries list endpoint, returning EntrySlimOut without content/full_content HTML — cuts payload size dramatically for list views. Single entry endpoint still returns full content. iOS: ArticleView now fetches full entry content on demand when opened instead of relying on list data. Shows loading indicator while fetching. Mark-as-read is fire-and-forget to avoid blocking the view. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -47,13 +47,28 @@ class EntryOut(BaseModel):
|
||||
from_attributes = True
|
||||
|
||||
@classmethod
|
||||
def from_entry(cls, entry: Entry) -> "EntryOut":
|
||||
# Use full_content if available, otherwise RSS content
|
||||
best_content = entry.full_content if entry.full_content else entry.content
|
||||
def from_entry(cls, entry: Entry, slim: bool = False) -> "EntryOut | EntrySlimOut":
|
||||
# Extract thumbnail from stored field, or from content
|
||||
thumb = entry.thumbnail
|
||||
if not thumb:
|
||||
thumb = cls._extract_thumbnail(entry.content or entry.full_content or "")
|
||||
|
||||
if slim:
|
||||
return EntrySlimOut(
|
||||
id=entry.id,
|
||||
title=entry.title,
|
||||
url=entry.url,
|
||||
author=entry.author,
|
||||
published_at=entry.published_at.isoformat() if entry.published_at else None,
|
||||
status=entry.status,
|
||||
starred=entry.starred,
|
||||
reading_time=entry.reading_time,
|
||||
thumbnail=thumb,
|
||||
feed=FeedRef(id=entry.feed.id, title=entry.feed.title) if entry.feed else None,
|
||||
)
|
||||
|
||||
# Use full_content if available, otherwise RSS content
|
||||
best_content = entry.full_content if entry.full_content else entry.content
|
||||
return cls(
|
||||
id=entry.id,
|
||||
title=entry.title,
|
||||
@@ -85,9 +100,26 @@ class EntryOut(BaseModel):
|
||||
return None
|
||||
|
||||
|
||||
class EntrySlimOut(BaseModel):
|
||||
"""Entry without content fields — used for list views."""
|
||||
id: int
|
||||
title: str | None = None
|
||||
url: str | None = None
|
||||
author: str | None = None
|
||||
published_at: str | None = None
|
||||
status: str = "unread"
|
||||
starred: bool = False
|
||||
reading_time: int = 1
|
||||
thumbnail: str | None = None
|
||||
feed: FeedRef | None = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EntryListOut(BaseModel):
|
||||
total: int
|
||||
entries: list[EntryOut]
|
||||
entries: list[EntryOut | EntrySlimOut]
|
||||
|
||||
|
||||
class EntryBulkUpdate(BaseModel):
|
||||
@@ -104,6 +136,7 @@ async def list_entries(
|
||||
starred: Optional[bool] = Query(None),
|
||||
feed_id: Optional[int] = Query(None),
|
||||
category_id: Optional[int] = Query(None),
|
||||
slim: bool = Query(True),
|
||||
limit: int = Query(50, ge=1, le=500),
|
||||
offset: int = Query(0, ge=0),
|
||||
direction: str = Query("desc"),
|
||||
@@ -149,7 +182,7 @@ async def list_entries(
|
||||
|
||||
return EntryListOut(
|
||||
total=total,
|
||||
entries=[EntryOut.from_entry(e) for e in entries],
|
||||
entries=[EntryOut.from_entry(e, slim=slim) for e in entries],
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user