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 collections import deque
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import PurePosixPath
|
from pathlib import PurePosixPath
|
||||||
from typing import Any, Deque, Dict, Iterable, Iterator, List, Mapping, Optional, Set
|
from typing import Any, Deque, Dict, Iterable, Iterator, List, Mapping, Optional, Set, Tuple
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import quote, urljoin, urlparse
|
||||||
from xml.etree import ElementTree as ET
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -202,13 +202,26 @@ class CalibreOPDSClient:
|
|||||||
cleaned = (query or "").strip()
|
cleaned = (query or "").strip()
|
||||||
if not cleaned:
|
if not cleaned:
|
||||||
return self.fetch_feed()
|
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", {"query": cleaned}),
|
||||||
("search", {"q": cleaned}),
|
("search", {"q": cleaned}),
|
||||||
(None, {"search": cleaned}),
|
(None, {"search": cleaned}),
|
||||||
]
|
])
|
||||||
|
|
||||||
last_error: Optional[Exception] = None
|
last_error: Optional[Exception] = None
|
||||||
base_feed: Optional[OPDSFeed] = None
|
|
||||||
base_combined: Optional[OPDSFeed] = None
|
base_combined: Optional[OPDSFeed] = None
|
||||||
for path, params in candidates:
|
for path, params in candidates:
|
||||||
try:
|
try:
|
||||||
@@ -1037,6 +1050,20 @@ class CalibreOPDSClient:
|
|||||||
return dataclasses.replace(template, entries=collected)
|
return dataclasses.replace(template, entries=collected)
|
||||||
return dataclasses.replace(template, entries=[])
|
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]:
|
def feed_to_dict(feed: OPDSFeed) -> Dict[str, Any]:
|
||||||
"""Helper used by APIs to convert a feed into JSON-serialisable payloads."""
|
"""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 statusEl = browser.querySelector('[data-role="opds-status"]');
|
||||||
const resultsEl = browser.querySelector('[data-role="opds-results"]');
|
const resultsEl = browser.querySelector('[data-role="opds-results"]');
|
||||||
const navEl = browser.querySelector('[data-role="opds-nav"]');
|
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 alphaPickerEl = browser.querySelector('[data-role="opds-alpha-picker"]');
|
||||||
const tabsEl = modal.querySelector('[data-role="opds-tabs"]');
|
const tabsEl = modal.querySelector('[data-role="opds-tabs"]');
|
||||||
const searchForm = modal.querySelector('[data-role="opds-search"]');
|
const searchForm = modal.querySelector('[data-role="opds-search"]');
|
||||||
@@ -247,17 +248,10 @@ if (modal && browser) {
|
|||||||
if (!alphaPickerEl || state.query) {
|
if (!alphaPickerEl || state.query) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!Array.isArray(entries) || entries.length < 6) {
|
if (!Array.isArray(entries) || entries.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const mode = deriveBrowseMode();
|
return true;
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refreshAlphabetActiveState = () => {
|
const refreshAlphabetActiveState = () => {
|
||||||
@@ -542,10 +536,14 @@ if (modal && browser) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderNav = (links) => {
|
const renderNav = (links) => {
|
||||||
if (!navEl) {
|
const targets = [navEl, navBottomEl].filter(Boolean);
|
||||||
|
if (!targets.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
navEl.innerHTML = '';
|
targets.forEach((el) => {
|
||||||
|
el.innerHTML = '';
|
||||||
|
});
|
||||||
|
|
||||||
const descriptors = [
|
const descriptors = [
|
||||||
{ key: 'up', label: 'Up one level' },
|
{ key: 'up', label: 'Up one level' },
|
||||||
{ key: 'previous', label: 'Previous page' },
|
{ key: 'previous', label: 'Previous page' },
|
||||||
@@ -556,11 +554,18 @@ if (modal && browser) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`);
|
const link = resolveRelLink(links, key) || resolveRelLink(links, `/${key}`);
|
||||||
|
const hasLink = Boolean(link && link.href);
|
||||||
|
|
||||||
|
if (!hasLink && key !== 'previous') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
targets.forEach((targetEl) => {
|
||||||
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;
|
||||||
const hasLink = Boolean(link && link.href);
|
|
||||||
if (hasLink) {
|
if (hasLink) {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
const targetQuery = key === 'up' ? '' : state.query;
|
const targetQuery = key === 'up' ? '' : state.query;
|
||||||
@@ -570,12 +575,14 @@ if (modal && browser) {
|
|||||||
} else if (key === 'previous') {
|
} else if (key === 'previous') {
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
button.setAttribute('aria-disabled', 'true');
|
button.setAttribute('aria-disabled', 'true');
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
navEl.appendChild(button);
|
targetEl.appendChild(button);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
targets.forEach((el) => {
|
||||||
|
el.hidden = !el.childElementCount;
|
||||||
});
|
});
|
||||||
navEl.hidden = !navEl.childElementCount;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const createEntry = (entry) => {
|
const createEntry = (entry) => {
|
||||||
|
|||||||
@@ -654,7 +654,22 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.35rem;
|
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 {
|
.opds-alpha-picker__button {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@
|
|||||||
<div class="opds-modal__results-wrap">
|
<div class="opds-modal__results-wrap">
|
||||||
<ul class="opds-modal__results" data-role="opds-results"></ul>
|
<ul class="opds-modal__results" data-role="opds-results"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="opds-modal__nav opds-modal__nav--bottom" data-role="opds-nav-bottom"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user