mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-04-20 13:59:46 -04:00
Hardcover tweaks (#720)
This commit is contained in:
@@ -2188,6 +2188,8 @@ def api_metadata_search() -> Union[Response, Tuple[Response, int]]:
|
||||
}
|
||||
if search_result.source_url:
|
||||
response_data["source_url"] = search_result.source_url
|
||||
if search_result.source_title:
|
||||
response_data["source_title"] = search_result.source_title
|
||||
return jsonify(response_data)
|
||||
except Exception as e:
|
||||
logger.error_trace(f"Metadata search error: {e}")
|
||||
|
||||
@@ -311,6 +311,7 @@ class SearchResult:
|
||||
total_found: int = 0 # Total matching results (if known)
|
||||
has_more: bool = False # True if more results available
|
||||
source_url: Optional[str] = None # External URL for the result set (e.g. Hardcover list page)
|
||||
source_title: Optional[str] = None # Display title for the result set (e.g. list name)
|
||||
|
||||
|
||||
class MetadataProvider(ABC):
|
||||
|
||||
@@ -59,6 +59,7 @@ query LookupListsBySlug($slug: String!) {
|
||||
LIST_BOOKS_BY_ID_QUERY = """
|
||||
query GetListBooksById($id: Int!, $limit: Int!, $offset: Int!) {
|
||||
lists(where: {id: {_eq: $id}}, limit: 1) {
|
||||
name
|
||||
slug
|
||||
user {
|
||||
username
|
||||
@@ -909,8 +910,9 @@ class HardcoverProvider(MetadataProvider):
|
||||
list_books = list_data.get("list_books", []) if isinstance(list_data, dict) else []
|
||||
books_count_raw = list_data.get("books_count", 0) if isinstance(list_data, dict) else 0
|
||||
|
||||
# Build source URL from slug and owner username
|
||||
# Build source URL and title from list metadata
|
||||
source_url = None
|
||||
source_title = str(list_data.get("name") or "").strip() or None
|
||||
list_slug = str(list_data.get("slug") or "").strip()
|
||||
user_data = list_data.get("user", {})
|
||||
owner_username = str(user_data.get("username") or "").strip() if isinstance(user_data, dict) else ""
|
||||
@@ -937,7 +939,7 @@ class HardcoverProvider(MetadataProvider):
|
||||
logger.debug(f"Failed to parse Hardcover list book for list_id={list_id}: {exc}")
|
||||
|
||||
has_more = offset + len(list_books) < books_count
|
||||
return SearchResult(books=books, page=page, total_found=books_count, has_more=has_more, source_url=source_url)
|
||||
return SearchResult(books=books, page=page, total_found=books_count, has_more=has_more, source_url=source_url, source_title=source_title)
|
||||
|
||||
@cacheable(ttl_key="METADATA_CACHE_SEARCH_TTL", ttl_default=300, key_prefix="hardcover:list:slug")
|
||||
def _fetch_list_books(self, slug: str, owner_username: Optional[str], page: int, limit: int) -> SearchResult:
|
||||
|
||||
@@ -79,6 +79,7 @@ import {
|
||||
import { bookFromRequestData } from './utils/requestFulfil';
|
||||
import { emitBookTargetChange, onBookTargetChange } from './utils/bookTargetEvents';
|
||||
import { bookSupportsTargets } from './utils/bookTargetLoader';
|
||||
import { getDynamicOptionGroup } from './components/shared/DynamicDropdown';
|
||||
import { policyTrace } from './utils/policyTrace';
|
||||
import { SearchModeProvider } from './contexts/SearchModeContext';
|
||||
import { useSocket } from './contexts/SocketContext';
|
||||
@@ -1089,16 +1090,28 @@ function App() {
|
||||
[]
|
||||
);
|
||||
|
||||
// When downloading a book while browsing a Hardcover list, automatically
|
||||
// remove it from that list (fire-and-forget).
|
||||
// When downloading a book while browsing a Hardcover list the user owns,
|
||||
// automatically remove it from that list (fire-and-forget).
|
||||
const searchFieldLabelsRef = useRef(searchFieldLabels);
|
||||
searchFieldLabelsRef.current = searchFieldLabels;
|
||||
const metadataConfigRef = useRef(activeMetadataConfig);
|
||||
metadataConfigRef.current = activeMetadataConfig;
|
||||
|
||||
const removeBookFromActiveList = useCallback((book: Book) => {
|
||||
if (!bookSupportsTargets(book)) return;
|
||||
const activeList = searchFieldValuesRef.current.hardcover_list;
|
||||
if (!activeList) return;
|
||||
const target = String(activeList);
|
||||
|
||||
// Only auto-remove from lists the user owns (My Books / My Lists)
|
||||
const listField = metadataConfigRef.current?.search_fields.find(
|
||||
(f) => f.key === 'hardcover_list' && f.type === 'DynamicSelectSearchField',
|
||||
);
|
||||
if (listField && listField.type === 'DynamicSelectSearchField') {
|
||||
const group = getDynamicOptionGroup(listField.options_endpoint, target);
|
||||
if (group && group !== 'My Books' && group !== 'My Lists') return;
|
||||
}
|
||||
|
||||
void setBookTargetState(book.provider!, book.provider_id!, target, false).then((result) => {
|
||||
if (result.changed) {
|
||||
emitBookTargetChange({
|
||||
@@ -2135,7 +2148,7 @@ function App() {
|
||||
getButtonState={getDirectActionButtonState}
|
||||
getUniversalButtonState={getUniversalActionButtonState}
|
||||
sortValue={visibleResultsSort}
|
||||
showSortControl={!activeQueryUsesSeriesBrowse && !activeQueryUsesListBrowse}
|
||||
showSortControl={!activeQueryUsesSeriesBrowse && !activeQueryUsesListBrowse && !resultsSourceUrl}
|
||||
onSortChange={(value) => {
|
||||
const request = buildCurrentSearchRequest(value);
|
||||
const shouldPersistAppliedSort = !(
|
||||
|
||||
@@ -217,7 +217,7 @@ export const BookTargetDropdown = ({
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => { e.stopPropagation(); toggle(); }}
|
||||
className={`flex items-center justify-center rounded-full transition-all duration-200 focus:outline-none ${className ?? 'p-1.5 sm:p-2 text-gray-600 dark:text-gray-200 hover-action'}`}
|
||||
className={`flex items-center justify-center rounded-full transition-colors duration-200 focus:outline-none ${className ?? 'p-1.5 sm:p-2 text-gray-600 dark:text-gray-200 hover-action'}`}
|
||||
aria-label="Hardcover Lists"
|
||||
title={count > 0 ? `On ${count} Hardcover list${count > 1 ? 's' : ''}` : 'Hardcover Lists'}
|
||||
>
|
||||
|
||||
@@ -25,6 +25,13 @@ const buildOptions = (
|
||||
}));
|
||||
};
|
||||
|
||||
/** Look up the group of a cached option by endpoint and value. */
|
||||
export const getDynamicOptionGroup = (endpoint: string, value: string): string | undefined => {
|
||||
const cached = optionsCache.get(endpoint);
|
||||
if (!cached) return undefined;
|
||||
return cached.find((o) => o.value === value)?.group;
|
||||
};
|
||||
|
||||
export const DynamicDropdown = ({
|
||||
endpoint,
|
||||
value,
|
||||
|
||||
@@ -49,8 +49,9 @@ interface UseSearchReturn {
|
||||
isLoadingMore: boolean;
|
||||
loadMore: (config: AppConfig | null, searchMode?: SearchMode) => Promise<void>;
|
||||
totalFound: number;
|
||||
// Source URL for the current result set (e.g. Hardcover list page)
|
||||
// Source URL and title for the current result set (e.g. Hardcover list page)
|
||||
resultsSourceUrl: string | undefined;
|
||||
resultsSourceTitle: string | undefined;
|
||||
}
|
||||
|
||||
export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
@@ -82,6 +83,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const [totalFound, setTotalFound] = useState(0);
|
||||
const [resultsSourceUrl, setResultsSourceUrl] = useState<string | undefined>();
|
||||
const [resultsSourceTitle, setResultsSourceTitle] = useState<string | undefined>();
|
||||
|
||||
// Store last search params for loadMore
|
||||
const lastSearchParamsRef = useRef<{
|
||||
@@ -167,6 +169,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
setTotalFound(0);
|
||||
setCurrentPage(1);
|
||||
setResultsSourceUrl(undefined);
|
||||
setResultsSourceTitle(undefined);
|
||||
lastSearchParamsRef.current = null;
|
||||
return;
|
||||
}
|
||||
@@ -193,6 +196,11 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
setHasMore(result.hasMore);
|
||||
setTotalFound(result.totalFound);
|
||||
setResultsSourceUrl(result.sourceUrl);
|
||||
setResultsSourceTitle(result.sourceTitle);
|
||||
// Replace URL in search input with list title for display
|
||||
if (result.sourceTitle && searchQuery) {
|
||||
setSearchInput(result.sourceTitle);
|
||||
}
|
||||
// Store params for loadMore
|
||||
lastSearchParamsRef.current = {
|
||||
query: searchQuery,
|
||||
@@ -206,6 +214,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
setHasMore(false);
|
||||
setTotalFound(0);
|
||||
setResultsSourceUrl(undefined);
|
||||
setResultsSourceTitle(undefined);
|
||||
showToast('No results found', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -276,6 +285,7 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
setHasMore(false);
|
||||
setTotalFound(0);
|
||||
setResultsSourceUrl(undefined);
|
||||
setResultsSourceTitle(undefined);
|
||||
lastSearchParamsRef.current = null;
|
||||
}, [onSearchReset]);
|
||||
|
||||
@@ -340,5 +350,6 @@ export function useSearch(options: UseSearchOptions): UseSearchReturn {
|
||||
loadMore,
|
||||
totalFound,
|
||||
resultsSourceUrl,
|
||||
resultsSourceTitle,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,6 +225,7 @@ interface MetadataSearchResponse {
|
||||
total_found?: number;
|
||||
has_more?: boolean;
|
||||
source_url?: string;
|
||||
source_title?: string;
|
||||
}
|
||||
|
||||
// Metadata search result with pagination info
|
||||
@@ -234,6 +235,7 @@ export interface MetadataSearchResult {
|
||||
totalFound: number;
|
||||
hasMore: boolean;
|
||||
sourceUrl?: string;
|
||||
sourceTitle?: string;
|
||||
}
|
||||
|
||||
export interface DynamicFieldOption {
|
||||
@@ -300,6 +302,7 @@ export const searchMetadata = async (
|
||||
totalFound: response.total_found || 0,
|
||||
hasMore: response.has_more || false,
|
||||
sourceUrl: response.source_url,
|
||||
sourceTitle: response.source_title,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user