mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
Add ROOTLESS_CONTAINER and request UI updates
Introduce ROOTLESS_CONTAINER env to opt out of gosu (replace /proc uid_map detection) and update entrypoint messaging; adjust app-start.sh and redis-start.sh to skip gosu when ROOTLESS_CONTAINER=true and warn on UID/GID mismatch only when applicable. Backend: include audiobook audibleAsin in admin requests response (mapped to asin) and pass baseUrl through test-flaresolverr endpoint to the FlareSolverr tester. Frontend: RecentRequestsTable and RequestActionsDropdown now surface asin, accept/passthrough annasArchiveBaseUrl, and add a "View Details" flow using AudiobookDetailsModal; admin page passes ebook baseUrl from settings. InteractiveTorrentSearchModal refactor: improved UX/UI, keyboard handling, portal/modal mounting, skeleton/loading states, formatting helpers, and richer result display. Tests updated to match changes.
This commit is contained in:
@@ -13,11 +13,13 @@ import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { mutate } from 'swr';
|
||||
import { authenticatedFetcher, fetchWithAuth } from '@/lib/utils/api';
|
||||
import { useToast } from '@/components/ui/Toast';
|
||||
import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal';
|
||||
|
||||
interface RecentRequest {
|
||||
requestId: string;
|
||||
title: string;
|
||||
author: string;
|
||||
asin?: string | null;
|
||||
status: string;
|
||||
type?: 'audiobook' | 'ebook';
|
||||
userId: string;
|
||||
@@ -43,6 +45,7 @@ interface RequestsResponse {
|
||||
|
||||
interface RecentRequestsTableProps {
|
||||
ebookSidecarEnabled?: boolean;
|
||||
annasArchiveBaseUrl?: string;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
@@ -158,7 +161,7 @@ function getInitialParams(): {
|
||||
};
|
||||
}
|
||||
|
||||
export function RecentRequestsTable({ ebookSidecarEnabled = false }: RecentRequestsTableProps) {
|
||||
export function RecentRequestsTable({ ebookSidecarEnabled = false, annasArchiveBaseUrl = 'https://annas-archive.li' }: RecentRequestsTableProps) {
|
||||
const toast = useToast();
|
||||
|
||||
// Get initial filter state from URL (only evaluated once due to lazy init)
|
||||
@@ -185,6 +188,10 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false }: RecentReque
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
const [isFetchingEbook, setIsFetchingEbook] = useState(false);
|
||||
|
||||
// View Details modal state
|
||||
const [viewDetailsAsin, setViewDetailsAsin] = useState<string | null>(null);
|
||||
const [viewDetailsStatus, setViewDetailsStatus] = useState<string | null>(null);
|
||||
|
||||
// Build API URL with current local filters
|
||||
const apiUrl = `/api/admin/requests?page=${page}&pageSize=${pageSize}&search=${encodeURIComponent(debouncedSearch)}&status=${status}&userId=${userId}&sortBy=${sortBy}&sortOrder=${sortOrder}`;
|
||||
|
||||
@@ -314,6 +321,11 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false }: RecentReque
|
||||
const hasActiveFilters = debouncedSearch || status !== 'all' || userId;
|
||||
|
||||
// Action handlers
|
||||
const handleViewDetails = (asin: string, requestStatus?: string) => {
|
||||
setViewDetailsAsin(asin);
|
||||
setViewDetailsStatus(requestStatus || null);
|
||||
};
|
||||
|
||||
const handleDeleteClick = (requestId: string, title: string) => {
|
||||
setSelectedRequest({ id: requestId, title });
|
||||
setShowDeleteConfirm(true);
|
||||
@@ -659,13 +671,16 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false }: RecentReque
|
||||
author: request.author,
|
||||
status: request.status,
|
||||
type: request.type,
|
||||
asin: request.asin,
|
||||
torrentUrl: request.torrentUrl,
|
||||
}}
|
||||
onDelete={handleDeleteClick}
|
||||
onManualSearch={handleManualSearch}
|
||||
onCancel={handleCancel}
|
||||
onViewDetails={(asin) => handleViewDetails(asin, request.status)}
|
||||
onFetchEbook={handleFetchEbook}
|
||||
ebookSidecarEnabled={ebookSidecarEnabled}
|
||||
annasArchiveBaseUrl={annasArchiveBaseUrl}
|
||||
isLoading={isDeleting || isFetchingEbook}
|
||||
/>
|
||||
</td>
|
||||
@@ -808,6 +823,21 @@ export function RecentRequestsTable({ ebookSidecarEnabled = false }: RecentReque
|
||||
onConfirm={handleDeleteConfirm}
|
||||
onCancel={handleDeleteCancel}
|
||||
/>
|
||||
|
||||
{/* Audiobook Details Modal */}
|
||||
{viewDetailsAsin && (
|
||||
<AudiobookDetailsModal
|
||||
asin={viewDetailsAsin}
|
||||
isOpen={!!viewDetailsAsin}
|
||||
onClose={() => {
|
||||
setViewDetailsAsin(null);
|
||||
setViewDetailsStatus(null);
|
||||
}}
|
||||
isAvailable={viewDetailsStatus === 'available' || viewDetailsStatus === 'completed'}
|
||||
requestStatus={viewDetailsStatus}
|
||||
hideRequestActions
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,13 +19,16 @@ export interface RequestActionsDropdownProps {
|
||||
author: string;
|
||||
status: string;
|
||||
type?: 'audiobook' | 'ebook';
|
||||
asin?: string | null;
|
||||
torrentUrl?: string | null;
|
||||
};
|
||||
onDelete: (requestId: string, title: string) => void;
|
||||
onManualSearch: (requestId: string) => Promise<void>;
|
||||
onCancel: (requestId: string) => Promise<void>;
|
||||
onViewDetails?: (asin: string) => void;
|
||||
onFetchEbook?: (requestId: string) => Promise<void>;
|
||||
ebookSidecarEnabled?: boolean;
|
||||
annasArchiveBaseUrl?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
@@ -34,8 +37,10 @@ export function RequestActionsDropdown({
|
||||
onDelete,
|
||||
onManualSearch,
|
||||
onCancel,
|
||||
onViewDetails,
|
||||
onFetchEbook,
|
||||
ebookSidecarEnabled = false,
|
||||
annasArchiveBaseUrl = 'https://annas-archive.li',
|
||||
isLoading = false,
|
||||
}: RequestActionsDropdownProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -46,6 +51,9 @@ export function RequestActionsDropdown({
|
||||
// Determine request type
|
||||
const isEbook = request.type === 'ebook';
|
||||
|
||||
// View Details: available when ASIN exists (audiobook requests only)
|
||||
const canViewDetails = !isEbook && !!request.asin && !!onViewDetails;
|
||||
|
||||
// Determine available actions based on status and type
|
||||
// Ebooks don't support manual/interactive search (Anna's Archive only)
|
||||
const canSearch = !isEbook && ['pending', 'failed', 'awaiting_search'].includes(request.status);
|
||||
@@ -64,7 +72,7 @@ export function RequestActionsDropdown({
|
||||
if (Array.isArray(urls) && urls.length > 0) {
|
||||
const md5Match = urls[0].match(/\/slow_download\/([a-f0-9]{32})\//i);
|
||||
if (md5Match) {
|
||||
viewSourceUrl = `https://annas-archive.li/md5/${md5Match[1]}`;
|
||||
viewSourceUrl = `${annasArchiveBaseUrl.replace(/\/+$/, '')}/md5/${md5Match[1]}`;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -147,6 +155,13 @@ export function RequestActionsDropdown({
|
||||
}
|
||||
};
|
||||
|
||||
const handleViewDetails = () => {
|
||||
setIsOpen(false);
|
||||
if (request.asin && onViewDetails) {
|
||||
onViewDetails(request.asin);
|
||||
}
|
||||
};
|
||||
|
||||
// Dropdown menu content (rendered via portal)
|
||||
const dropdownMenu = isOpen && style && (
|
||||
<div
|
||||
@@ -155,6 +170,41 @@ export function RequestActionsDropdown({
|
||||
className="w-56 rounded-lg shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 z-50 max-h-[calc(100vh-2rem)] overflow-y-auto"
|
||||
>
|
||||
<div className="py-1" role="menu">
|
||||
{/* View Details */}
|
||||
{canViewDetails && (
|
||||
<button
|
||||
onClick={handleViewDetails}
|
||||
className="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 flex items-center gap-2 transition-colors"
|
||||
role="menuitem"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"
|
||||
/>
|
||||
</svg>
|
||||
View Details
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Divider after View Details */}
|
||||
{canViewDetails && (canSearch || canViewSource || canFetchEbook || canCancel || canDelete) && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700 my-1" />
|
||||
)}
|
||||
|
||||
{/* Manual Search */}
|
||||
{canSearch && (
|
||||
<button
|
||||
|
||||
+26
-24
@@ -485,31 +485,8 @@ function AdminDashboardContent() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Requests Awaiting Approval */}
|
||||
{pendingApprovalData?.requests && pendingApprovalData.requests.length > 0 && (
|
||||
<PendingApprovalSection requests={pendingApprovalData.requests} />
|
||||
)}
|
||||
|
||||
{/* Active Downloads */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Active Downloads
|
||||
</h2>
|
||||
<ActiveDownloadsTable downloads={downloadsData.downloads} />
|
||||
</div>
|
||||
|
||||
{/* Request Management */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Request Management
|
||||
</h2>
|
||||
<RecentRequestsTable
|
||||
ebookSidecarEnabled={settingsData?.ebook?.annasArchiveEnabled || settingsData?.ebook?.indexerSearchEnabled || false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<Link
|
||||
href="/admin/settings"
|
||||
className="block p-6 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg hover:shadow-md transition-all"
|
||||
@@ -595,6 +572,31 @@ function AdminDashboardContent() {
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Requests Awaiting Approval */}
|
||||
{pendingApprovalData?.requests && pendingApprovalData.requests.length > 0 && (
|
||||
<PendingApprovalSection requests={pendingApprovalData.requests} />
|
||||
)}
|
||||
|
||||
{/* Active Downloads */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Active Downloads
|
||||
</h2>
|
||||
<ActiveDownloadsTable downloads={downloadsData.downloads} />
|
||||
</div>
|
||||
|
||||
{/* Request Management */}
|
||||
<div className="mb-8">
|
||||
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100 mb-4">
|
||||
Request Management
|
||||
</h2>
|
||||
<RecentRequestsTable
|
||||
ebookSidecarEnabled={settingsData?.ebook?.annasArchiveEnabled || settingsData?.ebook?.indexerSearchEnabled || false}
|
||||
annasArchiveBaseUrl={settingsData?.ebook?.baseUrl}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -51,7 +51,10 @@ export function useEbookSettings({ ebook, onChange, onSuccess, onError, markAsSa
|
||||
const response = await fetchWithAuth('/api/admin/settings/ebook/test-flaresolverr', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ url: ebook.flaresolverrUrl }),
|
||||
body: JSON.stringify({
|
||||
url: ebook.flaresolverrUrl,
|
||||
baseUrl: ebook.baseUrl || 'https://annas-archive.li',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user