mirror of
https://github.com/denizsafak/abogen.git
synced 2026-07-18 13:40:27 +02:00
feat: Add bottom navigation for OPDS modal and enhance styling for better visibility
This commit is contained in:
@@ -6,8 +6,8 @@ import re
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any, Deque, Dict, Iterable, Iterator, List, Mapping, Optional, Set
|
||||
from urllib.parse import urljoin, urlparse
|
||||
from typing import Any, Deque, Dict, Iterable, Iterator, List, Mapping, Optional, Set, Tuple
|
||||
from urllib.parse import quote, urljoin, urlparse
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
import httpx
|
||||
@@ -202,13 +202,26 @@ class CalibreOPDSClient:
|
||||
cleaned = (query or "").strip()
|
||||
if not cleaned:
|
||||
return self.fetch_feed()
|
||||
candidates = [
|
||||
|
||||
base_feed: Optional[OPDSFeed] = None
|
||||
try:
|
||||
base_feed = self.fetch_feed()
|
||||
except CalibreOPDSError:
|
||||
pass
|
||||
|
||||
candidates: List[Tuple[Optional[str], Optional[Mapping[str, Any]]]] = []
|
||||
if base_feed:
|
||||
search_url = self._resolve_search_url(base_feed, cleaned)
|
||||
if search_url:
|
||||
candidates.append((search_url, None))
|
||||
|
||||
candidates.extend([
|
||||
("search", {"query": cleaned}),
|
||||
("search", {"q": cleaned}),
|
||||
(None, {"search": cleaned}),
|
||||
]
|
||||
])
|
||||
|
||||
last_error: Optional[Exception] = None
|
||||
base_feed: Optional[OPDSFeed] = None
|
||||
base_combined: Optional[OPDSFeed] = None
|
||||
for path, params in candidates:
|
||||
try:
|
||||
@@ -1037,6 +1050,20 @@ class CalibreOPDSClient:
|
||||
return dataclasses.replace(template, entries=collected)
|
||||
return dataclasses.replace(template, entries=[])
|
||||
|
||||
def _resolve_search_url(self, feed: OPDSFeed, query: str) -> Optional[str]:
|
||||
link = self._resolve_link(feed.links, "search")
|
||||
if not link:
|
||||
link = self._resolve_link(feed.links, "http://opds-spec.org/search")
|
||||
|
||||
if not link or not link.href:
|
||||
return None
|
||||
|
||||
href = link.href.strip()
|
||||
if "{searchTerms}" in href:
|
||||
return href.replace("{searchTerms}", quote(query))
|
||||
|
||||
return href
|
||||
|
||||
|
||||
def feed_to_dict(feed: OPDSFeed) -> Dict[str, Any]:
|
||||
"""Helper used by APIs to convert a feed into JSON-serialisable payloads."""
|
||||
|
||||
@@ -5,6 +5,7 @@ if (modal && browser) {
|
||||
const statusEl = browser.querySelector('[data-role="opds-status"]');
|
||||
const resultsEl = browser.querySelector('[data-role="opds-results"]');
|
||||
const navEl = browser.querySelector('[data-role="opds-nav"]');
|
||||
const navBottomEl = browser.querySelector('[data-role="opds-nav-bottom"]');
|
||||
const alphaPickerEl = browser.querySelector('[data-role="opds-alpha-picker"]');
|
||||
const tabsEl = modal.querySelector('[data-role="opds-tabs"]');
|
||||
const searchForm = modal.querySelector('[data-role="opds-search"]');
|
||||
@@ -247,17 +248,10 @@ if (modal && browser) {
|
||||
if (!alphaPickerEl || state.query) {
|
||||
return false;
|
||||
}
|
||||
if (!Array.isArray(entries) || entries.length < 6) {
|
||||
if (!Array.isArray(entries) || entries.length === 0) {
|
||||
return false;
|
||||
}
|
||||
const mode = deriveBrowseMode();
|
||||
const counts = stats || computeEntryStats(entries);
|
||||
const hasNavigation = counts[EntryTypes.NAVIGATION] > 0;
|
||||
if (mode === 'generic' && !hasNavigation) {
|
||||
return false;
|
||||
}
|
||||
const alphabetCounts = collectAlphabetCounts(entries);
|
||||
return alphabetCounts.size > 1;
|
||||
return true;
|
||||
};
|
||||
|
||||
const refreshAlphabetActiveState = () => {
|
||||
@@ -542,10 +536,14 @@ if (modal && browser) {
|
||||
};
|
||||
|
||||
const renderNav = (links) => {
|
||||
if (!navEl) {
|
||||
const targets = [navEl, navBottomEl].filter(Boolean);
|
||||
if (!targets.length) {
|
||||
return;
|
||||
}
|
||||
navEl.innerHTML = '';
|
||||
targets.forEach((el) => {
|
||||
el.innerHTML = '';
|
||||
});
|
||||
|
||||
const descriptors = [
|
||||
{ key: 'up', label: 'Up one level' },
|
||||
{ key: 'previous', label: 'Previous page' },
|
||||
@@ -556,26 +554,35 @@ if (modal && browser) {
|
||||
return;
|
||||
}
|
||||
const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`);
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'button button--ghost';
|
||||
button.textContent = label;
|
||||
const hasLink = Boolean(link && link.href);
|
||||
if (hasLink) {
|
||||
button.addEventListener('click', () => {
|
||||
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 {
|
||||
|
||||
if (!hasLink && key !== 'previous') {
|
||||
return;
|
||||
}
|
||||
navEl.appendChild(button);
|
||||
|
||||
targets.forEach((targetEl) => {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'button button--ghost';
|
||||
button.textContent = label;
|
||||
|
||||
if (hasLink) {
|
||||
button.addEventListener('click', () => {
|
||||
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');
|
||||
}
|
||||
targetEl.appendChild(button);
|
||||
});
|
||||
});
|
||||
|
||||
targets.forEach((el) => {
|
||||
el.hidden = !el.childElementCount;
|
||||
});
|
||||
navEl.hidden = !navEl.childElementCount;
|
||||
};
|
||||
|
||||
const createEntry = (entry) => {
|
||||
|
||||
@@ -654,7 +654,22 @@ body {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
margin: 0.35rem 0 0.65rem;
|
||||
margin: 0;
|
||||
padding: 0.75rem 0 1rem;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: var(--bg);
|
||||
box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.2);
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--panel-border);
|
||||
}
|
||||
|
||||
.opds-modal__nav--bottom {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid var(--panel-border);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.opds-alpha-picker__button {
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
<div class="opds-modal__results-wrap">
|
||||
<ul class="opds-modal__results" data-role="opds-results"></ul>
|
||||
</div>
|
||||
<div class="opds-modal__nav opds-modal__nav--bottom" data-role="opds-nav-bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user