diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index a5901430..27af9ffc 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -2538,16 +2538,17 @@ function App() { isRequestMode={isBrowseFulfilMode || activeReleaseBook?.provider === 'manual'} showReleaseSourceLinks={config?.show_release_source_links !== false} onShowToast={showToast} - combinedPhase={combinedState?.phase ?? null} - combinedCurrentStep={combinedCurrentStep} - combinedTotalSteps={combinedSelectionPhases.length} - combinedEbookMode={combinedState?.ebookMode ?? null} - combinedAudiobookMode={combinedState?.audiobookMode ?? null} - onCombinedNext={combinedState && !combinedIsFinalStep ? handleCombinedNext : undefined} - onCombinedBack={combinedState && combinedHasPreviousStep ? handleCombinedBack : undefined} - onCombinedDownload={combinedState && combinedIsFinalStep ? handleCombinedDownload : undefined} - stagedEbookRelease={combinedState?.stagedEbook?.release ?? null} - stagedAudiobookRelease={combinedState?.stagedAudiobook ?? null} + combinedMode={combinedState ? { + phase: combinedState.phase, + stepLabel: `Step ${combinedCurrentStep} of ${combinedSelectionPhases.length} — Select ${combinedState.phase === 'ebook' ? 'book' : 'audiobook'}`, + ebookMode: combinedState.ebookMode, + audiobookMode: combinedState.audiobookMode, + stagedEbookRelease: combinedState.stagedEbook?.release ?? null, + stagedAudiobookRelease: combinedState.stagedAudiobook ?? null, + onNext: !combinedIsFinalStep ? handleCombinedNext : undefined, + onBack: combinedHasPreviousStep ? handleCombinedBack : undefined, + onDownload: combinedIsFinalStep ? handleCombinedDownload : undefined, + } : null} /> )} diff --git a/src/frontend/src/components/ReleaseModal.tsx b/src/frontend/src/components/ReleaseModal.tsx index c3ea32b2..35adb1a7 100644 --- a/src/frontend/src/components/ReleaseModal.tsx +++ b/src/frontend/src/components/ReleaseModal.tsx @@ -40,6 +40,31 @@ import { getCachedReleases, setCachedReleases, invalidateCachedReleases } from ' import { SortState, getSavedSort, saveSort, clearSort, inferDefaultDirection, sortReleases, FORMAT_SORT_KEY, sortReleasesByFormat } from '../utils/releaseSort'; +// Combined mode configuration for the ReleaseModal +export interface CombinedModeConfig { + phase: 'ebook' | 'audiobook'; + stepLabel: string; + ebookMode: RequestPolicyMode; + audiobookMode: RequestPolicyMode; + stagedEbookRelease: Release | null; + stagedAudiobookRelease: Release | null; + onNext?: (release: Release) => void; + onBack?: (audiobookRelease: Release | null) => void; + onDownload?: (release: Release) => void; +} + +// Determine the combined download button label based on action modes +function getCombinedDownloadLabel( + ebookMode: RequestPolicyMode | null | undefined, + audiobookMode: RequestPolicyMode | null | undefined, +): string { + const ebookIsRequest = ebookMode === 'request_release' || ebookMode === 'request_book'; + const audiobookIsRequest = audiobookMode === 'request_release' || audiobookMode === 'request_book'; + if (ebookIsRequest && audiobookIsRequest) return 'Request Both'; + if (ebookIsRequest || audiobookIsRequest) return 'Download & Request'; + return 'Download Both'; +} + // Default column configuration (fallback when backend doesn't provide one) const DEFAULT_COLUMN_CONFIG: ReleaseColumnConfig = { columns: [ @@ -101,17 +126,8 @@ interface ReleaseModalProps { isRequestMode?: boolean; showReleaseSourceLinks?: boolean; onShowToast?: (message: string, type: 'success' | 'error' | 'info') => void; - // Combined mode props - combinedPhase?: 'ebook' | 'audiobook' | null; - combinedCurrentStep?: number; - combinedTotalSteps?: number; - combinedEbookMode?: RequestPolicyMode | null; - combinedAudiobookMode?: RequestPolicyMode | null; - onCombinedNext?: (release: Release) => void; - onCombinedBack?: (audiobookRelease: Release | null) => void; - onCombinedDownload?: (release: Release) => void; - stagedEbookRelease?: Release | null; - stagedAudiobookRelease?: Release | null; + // Combined mode (ebook + audiobook in one transaction) + combinedMode?: CombinedModeConfig | null; } @@ -247,6 +263,32 @@ const RadioIndicator = ({ selected }: { selected: boolean }) => ( ); +// Phase indicator chip for combined mode footer +const PhaseChip = ({ release, isActive, label }: { + release: Release | null; + isActive: boolean; + label: string; +}) => ( + + {release ? ( + <> + + + + {release.format?.toUpperCase() || label} · {release.size || '?'} + > + ) : ( + <>{isActive ? '\u25CF' : '\u25CB'} {label}> + )} + +); + // Release row component with dynamic columns const ReleaseRow = ({ release, @@ -591,16 +633,7 @@ export const ReleaseModal = ({ isRequestMode = false, showReleaseSourceLinks = true, onShowToast, - combinedPhase = null, - combinedCurrentStep = 1, - combinedTotalSteps = 2, - combinedEbookMode = null, - combinedAudiobookMode = null, - onCombinedNext, - onCombinedBack, - onCombinedDownload, - stagedEbookRelease = null, - stagedAudiobookRelease = null, + combinedMode = null, }: ReleaseModalProps) => { // Use audiobook formats when in audiobook mode const effectiveFormats = contentType === 'audiobook' && supportedAudiobookFormats.length > 0 @@ -612,7 +645,16 @@ export const ReleaseModal = ({ const [isClosing, setIsClosing] = useState(false); const [isRequestingBook, setIsRequestingBook] = useState(false); const [selectedRelease, setSelectedRelease] = useState(null); - const isCombinedMode = combinedPhase !== null; + const isCombinedMode = combinedMode != null; + const combinedPhase = combinedMode?.phase ?? null; + const combinedStepLabel = combinedMode?.stepLabel ?? ''; + const combinedEbookMode = combinedMode?.ebookMode ?? null; + const combinedAudiobookMode = combinedMode?.audiobookMode ?? null; + const stagedEbookRelease = combinedMode?.stagedEbookRelease ?? null; + const stagedAudiobookRelease = combinedMode?.stagedAudiobookRelease ?? null; + const onCombinedNext = combinedMode?.onNext; + const onCombinedBack = combinedMode?.onBack; + const onCombinedDownload = combinedMode?.onDownload; // Available sources from plugin registry const [availableSources, setAvailableSources] = useState([]); @@ -1456,9 +1498,7 @@ export const ReleaseModal = ({ )} - {isCombinedMode - ? `Step ${combinedCurrentStep} of ${combinedTotalSteps} — Select ${combinedPhase === 'ebook' ? 'book' : 'audiobook'}` - : 'Find Releases'} + {isCombinedMode ? combinedStepLabel : 'Find Releases'} {book.provider === 'manual' ? 'Manual Query' : (book.title || 'Untitled')} @@ -2182,50 +2222,16 @@ export const ReleaseModal = ({ {/* Phase indicators with live selection chips */} - {(() => { - const ebookRelease = combinedPhase === 'ebook' ? selectedRelease : stagedEbookRelease; - const audiobookRelease = combinedPhase === 'audiobook' ? selectedRelease : stagedAudiobookRelease; - return ( - <> - - {ebookRelease ? ( - <> - - - - {ebookRelease.format?.toUpperCase() || 'Book'} · {ebookRelease.size || '?'} - > - ) : ( - <>{combinedPhase === 'ebook' ? '\u25CF' : '\u25CB'} Book> - )} - - - {audiobookRelease ? ( - <> - - - - {audiobookRelease.format?.toUpperCase() || 'Audiobook'} · {audiobookRelease.size || '?'} - > - ) : ( - <>{combinedPhase === 'audiobook' ? '\u25CF' : '\u25CB'} Audiobook> - )} - - > - ); - })()} + + {/* Action buttons */} @@ -2251,31 +2257,23 @@ export const ReleaseModal = ({ )} - {onCombinedDownload && (() => { - const ebookActionMode = combinedPhase === 'ebook' - ? (selectedRelease ? getReleaseActionMode(selectedRelease) : combinedEbookMode) - : (stagedEbookRelease ? getReleaseActionMode(stagedEbookRelease) : combinedEbookMode); - const audiobookActionMode = combinedPhase === 'audiobook' - ? (selectedRelease ? getReleaseActionMode(selectedRelease) : combinedAudiobookMode) - : (stagedAudiobookRelease ? getReleaseActionMode(stagedAudiobookRelease) : combinedAudiobookMode); - const ebookIsRequest = ebookActionMode === 'request_release' || ebookActionMode === 'request_book'; - const audiobookIsRequest = audiobookActionMode === 'request_release' || audiobookActionMode === 'request_book'; - const label = ebookIsRequest && audiobookIsRequest - ? 'Request Both' - : ebookIsRequest || audiobookIsRequest - ? 'Download & Request' - : 'Download Both'; - return ( - selectedRelease && onCombinedDownload(selectedRelease)} - disabled={!selectedRelease} - className="px-4 py-1.5 text-sm font-medium text-white rounded-lg transition-colors bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed" - > - {label} - - ); - })()} + {onCombinedDownload && ( + selectedRelease && onCombinedDownload(selectedRelease)} + disabled={!selectedRelease} + className="px-4 py-1.5 text-sm font-medium text-white rounded-lg transition-colors bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 disabled:cursor-not-allowed" + > + {getCombinedDownloadLabel( + combinedPhase === 'ebook' + ? (selectedRelease ? getReleaseActionMode(selectedRelease) : combinedEbookMode) + : (stagedEbookRelease ? getReleaseActionMode(stagedEbookRelease) : combinedEbookMode), + combinedPhase === 'audiobook' + ? (selectedRelease ? getReleaseActionMode(selectedRelease) : combinedAudiobookMode) + : (stagedAudiobookRelease ? getReleaseActionMode(stagedAudiobookRelease) : combinedAudiobookMode), + )} + + )} diff --git a/src/frontend/src/components/SearchBar.tsx b/src/frontend/src/components/SearchBar.tsx index ab466163..1199b1f2 100644 --- a/src/frontend/src/components/SearchBar.tsx +++ b/src/frontend/src/components/SearchBar.tsx @@ -123,7 +123,7 @@ const getDefaultPlaceholder = ( if (fallback) return fallback; if (!activeQueryTarget || activeQueryTarget.source === 'general') { - if (isCombinedMode) return 'Search Books & Audiobooks (combined)'; + if (isCombinedMode) return 'Search Books & Audiobooks'; return contentType === 'ebook' ? 'Search Books' : 'Search Audiobooks'; }
- {isCombinedMode - ? `Step ${combinedCurrentStep} of ${combinedTotalSteps} — Select ${combinedPhase === 'ebook' ? 'book' : 'audiobook'}` - : 'Find Releases'} + {isCombinedMode ? combinedStepLabel : 'Find Releases'}