mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Add position handling in OPDSEntry and update navigation button behavior in the UI
This commit is contained in:
@@ -50,6 +50,7 @@ class OPDSLink:
|
|||||||
class OPDSEntry:
|
class OPDSEntry:
|
||||||
id: str
|
id: str
|
||||||
title: str
|
title: str
|
||||||
|
position: Optional[int] = None
|
||||||
authors: List[str] = field(default_factory=list)
|
authors: List[str] = field(default_factory=list)
|
||||||
updated: Optional[str] = None
|
updated: Optional[str] = None
|
||||||
summary: Optional[str] = None
|
summary: Optional[str] = None
|
||||||
@@ -62,6 +63,7 @@ class OPDSEntry:
|
|||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
|
"position": self.position,
|
||||||
"authors": list(self.authors),
|
"authors": list(self.authors),
|
||||||
"updated": self.updated,
|
"updated": self.updated,
|
||||||
"summary": self.summary,
|
"summary": self.summary,
|
||||||
@@ -243,6 +245,7 @@ class CalibreOPDSClient:
|
|||||||
def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry:
|
def _parse_entry(self, node: ET.Element, base_url: str) -> OPDSEntry:
|
||||||
entry_id = node.findtext("atom:id", default="", namespaces=NS).strip()
|
entry_id = node.findtext("atom:id", default="", namespaces=NS).strip()
|
||||||
title = node.findtext("atom:title", default="Untitled", namespaces=NS).strip() or "Untitled"
|
title = node.findtext("atom:title", default="Untitled", namespaces=NS).strip() or "Untitled"
|
||||||
|
position_value = self._extract_position(node)
|
||||||
updated = node.findtext("atom:updated", default=None, namespaces=NS)
|
updated = node.findtext("atom:updated", default=None, namespaces=NS)
|
||||||
summary = node.findtext("atom:summary", default=None, namespaces=NS) or node.findtext(
|
summary = node.findtext("atom:summary", default=None, namespaces=NS) or node.findtext(
|
||||||
"atom:content", default=None, namespaces=NS
|
"atom:content", default=None, namespaces=NS
|
||||||
@@ -272,6 +275,7 @@ class CalibreOPDSClient:
|
|||||||
return OPDSEntry(
|
return OPDSEntry(
|
||||||
id=entry_id or title,
|
id=entry_id or title,
|
||||||
title=title,
|
title=title,
|
||||||
|
position=position_value,
|
||||||
authors=authors,
|
authors=authors,
|
||||||
updated=updated,
|
updated=updated,
|
||||||
summary=cleaned_summary,
|
summary=cleaned_summary,
|
||||||
@@ -281,6 +285,25 @@ class CalibreOPDSClient:
|
|||||||
links=list(parsed_links.values()),
|
links=list(parsed_links.values()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _extract_position(self, node: ET.Element) -> Optional[int]:
|
||||||
|
candidates = [
|
||||||
|
node.findtext("opds:position", default=None, namespaces=NS),
|
||||||
|
node.findtext("opds:groupPosition", default=None, namespaces=NS),
|
||||||
|
node.findtext("opds:order", default=None, namespaces=NS),
|
||||||
|
node.findtext("dc:identifier", default=None, namespaces=NS),
|
||||||
|
]
|
||||||
|
for value in candidates:
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
text = str(value).strip()
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
return int(float(text))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
def _parse_links(self, link_nodes: List[ET.Element], base_url: str) -> Dict[str, OPDSLink]:
|
def _parse_links(self, link_nodes: List[ET.Element], base_url: str) -> Dict[str, OPDSLink]:
|
||||||
results: Dict[str, OPDSLink] = {}
|
results: Dict[str, OPDSLink] = {}
|
||||||
for link in link_nodes:
|
for link in link_nodes:
|
||||||
|
|||||||
@@ -2871,7 +2871,7 @@ def calibre_opds_import() -> ResponseReturnValue:
|
|||||||
|
|
||||||
service.store_pending_job(pending)
|
service.store_pending_job(pending)
|
||||||
|
|
||||||
redirect_url = url_for("web.prepare_job", pending_id=pending.id)
|
redirect_url = url_for("web.prepare_job", pending_id=pending.id, step="book")
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"pending_id": pending.id,
|
"pending_id": pending.id,
|
||||||
"redirect_url": redirect_url,
|
"redirect_url": redirect_url,
|
||||||
|
|||||||
@@ -213,18 +213,23 @@ if (modal && browser) {
|
|||||||
];
|
];
|
||||||
descriptors.forEach(({ key, label }) => {
|
descriptors.forEach(({ key, label }) => {
|
||||||
const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`);
|
const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`);
|
||||||
if (!link || !link.href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const button = document.createElement('button');
|
const button = document.createElement('button');
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
button.className = 'button button--ghost';
|
button.className = 'button button--ghost';
|
||||||
button.textContent = label;
|
button.textContent = label;
|
||||||
button.addEventListener('click', () => {
|
const hasLink = Boolean(link && link.href);
|
||||||
const targetQuery = key === 'up' ? '' : state.query;
|
if (hasLink) {
|
||||||
const tabId = resolveTabIdForHref(link.href);
|
button.addEventListener('click', () => {
|
||||||
loadFeed({ href: link.href, query: targetQuery, activeTab: tabId || (targetQuery ? TabIds.SEARCH : TabIds.CUSTOM) });
|
const targetQuery = key === 'up' ? '' : state.query;
|
||||||
});
|
const tabId = resolveTabIdForHref(link.href);
|
||||||
|
loadFeed({ href: link.href, query: targetQuery, activeTab: tabId || (targetQuery ? TabIds.SEARCH : TabIds.CUSTOM) });
|
||||||
|
});
|
||||||
|
} else if (key === 'previous') {
|
||||||
|
button.disabled = true;
|
||||||
|
button.setAttribute('aria-disabled', 'true');
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
navEl.appendChild(button);
|
navEl.appendChild(button);
|
||||||
});
|
});
|
||||||
navEl.hidden = !navEl.childElementCount;
|
navEl.hidden = !navEl.childElementCount;
|
||||||
@@ -237,9 +242,11 @@ if (modal && browser) {
|
|||||||
const header = document.createElement('div');
|
const header = document.createElement('div');
|
||||||
header.className = 'opds-browser__entry-head';
|
header.className = 'opds-browser__entry-head';
|
||||||
|
|
||||||
const title = document.createElement('h3');
|
const title = document.createElement('h3');
|
||||||
title.className = 'opds-browser__title';
|
title.className = 'opds-browser__title';
|
||||||
title.textContent = entry.title || 'Untitled';
|
const positionLabel = Number.isFinite(entry?.position) ? Number(entry.position) : null;
|
||||||
|
const baseTitle = entry.title || 'Untitled';
|
||||||
|
title.textContent = positionLabel !== null ? `${positionLabel}. ${baseTitle}` : baseTitle;
|
||||||
header.appendChild(title);
|
header.appendChild(title);
|
||||||
|
|
||||||
const authors = formatAuthors(entry.authors);
|
const authors = formatAuthors(entry.authors);
|
||||||
@@ -375,6 +382,9 @@ if (modal && browser) {
|
|||||||
try {
|
try {
|
||||||
const target = new URL(redirectUrl, window.location.origin);
|
const target = new URL(redirectUrl, window.location.origin);
|
||||||
target.searchParams.set('format', 'json');
|
target.searchParams.set('format', 'json');
|
||||||
|
if (!target.searchParams.has('step')) {
|
||||||
|
target.searchParams.set('step', 'book');
|
||||||
|
}
|
||||||
await wizard.requestStep(target.toString(), { method: 'GET' });
|
await wizard.requestStep(target.toString(), { method: 'GET' });
|
||||||
} catch (wizardError) {
|
} catch (wizardError) {
|
||||||
console.error('Unable to open wizard via JSON payload', wizardError);
|
console.error('Unable to open wizard via JSON payload', wizardError);
|
||||||
|
|||||||
Reference in New Issue
Block a user