diff --git a/src/components/dialogs/AddDlcDialog.tsx b/src/components/dialogs/AddDlcDialog.tsx new file mode 100644 index 0000000..5e075cc --- /dev/null +++ b/src/components/dialogs/AddDlcDialog.tsx @@ -0,0 +1,93 @@ +import { useState, useEffect } from 'react' +import Dialog from './Dialog' +import DialogHeader from './DialogHeader' +import DialogBody from './DialogBody' +import DialogFooter from './DialogFooter' +import DialogActions from './DialogActions' +import { Button } from '@/components/buttons' +import { DlcInfo } from '@/types' + +export interface AddDlcDialogProps { + visible: boolean + onClose: () => void + onAdd: (dlc: DlcInfo) => void + existingIds: Set +} + +/** + * Add DLC Manually dialog + * Allows users to manually enter a DLC ID and name when it is + * missing from the Steam API and cannot be fetched automatically + */ +const AddDlcDialog = ({ visible, onClose, onAdd, existingIds }: AddDlcDialogProps) => { + const [id, setId] = useState('') + const [name, setName] = useState('') + const [error, setError] = useState('') + + // Reset form state when dialog closes + useEffect(() => { + if (!visible) { + setId('') + setName('') + setError('') + } + }, [visible]) + + // Validate inputs and add the DLC to the list + const handleSubmit = () => { + const trimmedId = id.trim() + const trimmedName = name.trim() + + if (!trimmedId) return setError('DLC ID is required.') + if (!/^\d+$/.test(trimmedId)) return setError('DLC ID must be a number.') + if (existingIds.has(trimmedId)) return setError('A DLC with this ID already exists.') + if (!trimmedName) return setError('DLC name is required.') + + onAdd({ appid: trimmedId, name: trimmedName, enabled: true }) + onClose() + } + + return ( + + +

Add DLC Manually

+
+ +
+
+ + { setId(e.target.value); setError('') }} + onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} + autoFocus + /> +
+
+ + { setName(e.target.value); setError('') }} + onKeyDown={(e) => e.key === 'Enter' && handleSubmit()} + /> +
+ {error &&

{error}

} +
+
+ + + + + + +
+ ) +} + +export default AddDlcDialog diff --git a/src/components/dialogs/DlcSelectionDialog.tsx b/src/components/dialogs/DlcSelectionDialog.tsx index 67606ec..1afc12c 100644 --- a/src/components/dialogs/DlcSelectionDialog.tsx +++ b/src/components/dialogs/DlcSelectionDialog.tsx @@ -4,6 +4,7 @@ import DialogHeader from './DialogHeader' import DialogBody from './DialogBody' import DialogFooter from './DialogFooter' import DialogActions from './DialogActions' +import AddDlcDialog from './AddDlcDialog' import { Button, AnimatedCheckbox } from '@/components/buttons' import { DlcInfo } from '@/types' import { Icon, check, info } from '@/components/icons' @@ -51,6 +52,7 @@ const DlcSelectionDialog = ({ const [searchQuery, setSearchQuery] = useState('') const [selectAll, setSelectAll] = useState(true) const [initialized, setInitialized] = useState(false) + const [showAddDlc, setShowAddDlc] = useState(false) // Reset dialog state when it opens or closes useEffect(() => { @@ -126,6 +128,11 @@ const DlcSelectionDialog = ({ ) }, [selectAll]) + // Add a manually-entered DLC to the list + const handleAddDlc = useCallback((dlc: DlcInfo) => { + setSelectedDlcs((prev) => [...prev, dlc]) + }, []) + // Submit selected DLCs to parent component const handleConfirm = useCallback(() => { // Create a deep copy to prevent reference issues @@ -151,124 +158,141 @@ const DlcSelectionDialog = ({ } return ( - - -

{dialogTitle}

-
- {gameTitle} - - {selectedCount} of {selectedDlcs.length} DLCs selected - {getLoadingInfoText()} - -
-
+ <> + + +

{dialogTitle}

+
+ {gameTitle} + + {selectedCount} of {selectedDlcs.length} DLCs selected + {getLoadingInfoText()} + +
+
-
- setSearchQuery(e.target.value)} - className="dlc-search-input" - /> -
- + setSearchQuery(e.target.value)} + className="dlc-search-input" /> -
-
- - {(isLoading || isUpdating) && loadingProgress > 0 && ( -
-
-
-
-
- {isUpdating ? 'Updating DLC list' : 'Loading DLCs'}: {loadingProgress}% - {estimatedTimeLeft && ( - Est. time left: {estimatedTimeLeft} - )} +
+
- )} - - {selectedDlcs.length > 0 ? ( -
    - {filteredDlcs.map((dlc) => ( -
  • - handleToggleDlc(dlc.appid)} - label={dlc.name} - sublabel={`ID: ${dlc.appid}`} - /> -
  • - ))} - {isLoading && ( -
  • -
    -
  • - )} -
- ) : ( -
-
-

Loading DLC information...

+ {(isLoading || isUpdating) && loadingProgress > 0 && ( +
+
+
+
+
+ {isUpdating ? 'Updating DLC list' : 'Loading DLCs'}: {loadingProgress}% + {estimatedTimeLeft && ( + Est. time left: {estimatedTimeLeft} + )} +
)} - - - {/* Show update results */} - {!isUpdating && !isLoading && isEditMode && updateAttempted && ( - <> - {newDlcsCount > 0 && ( -
- - Found {newDlcsCount} new DLC{newDlcsCount > 1 ? 's' : ''}! - -
- )} - {newDlcsCount === 0 && ( -
- - No new DLCs found. Your list is up to date! - -
- )} - - )} - - - - - {/* Update button - only show in edit mode */} - {isEditMode && onUpdate && ( + + {selectedDlcs.length > 0 ? ( +
    + {filteredDlcs.map((dlc) => ( +
  • + handleToggleDlc(dlc.appid)} + label={dlc.name} + sublabel={`ID: ${dlc.appid}`} + /> +
  • + ))} + {isLoading && ( +
  • +
    +
  • + )} +
+ ) : ( +
+
+

Loading DLC information...

+
+ )} +
+ + + {/* Show update results */} + {!isUpdating && !isLoading && isEditMode && updateAttempted && ( + <> + {newDlcsCount > 0 && ( +
+ + Found {newDlcsCount} new DLC{newDlcsCount > 1 ? 's' : ''}! + +
+ )} + {newDlcsCount === 0 && ( +
+ + No new DLCs found. Your list is up to date! + +
+ )} + + )} + + + + - )} - - - -
-
+ + {/* Update button - only show in edit mode */} + {isEditMode && onUpdate && ( + + )} + + + + +
+ + setShowAddDlc(false)} + onAdd={handleAddDlc} + existingIds={new Set(selectedDlcs.map((d) => d.appid))} + /> + ) } -export default DlcSelectionDialog \ No newline at end of file +export default DlcSelectionDialog diff --git a/src/components/dialogs/index.ts b/src/components/dialogs/index.ts index adf8246..152f7c3 100644 --- a/src/components/dialogs/index.ts +++ b/src/components/dialogs/index.ts @@ -6,6 +6,7 @@ export { default as DialogFooter } from './DialogFooter' export { default as DialogActions } from './DialogActions' export { default as ProgressDialog } from './ProgressDialog' export { default as DlcSelectionDialog } from './DlcSelectionDialog' +export { default as AddDlcDialog } from './AddDlcDialog' export { default as SettingsDialog } from './SettingsDialog' export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog' export { default as ConflictDialog } from './ConflictDialog' @@ -20,5 +21,6 @@ export type { DialogFooterProps } from './DialogFooter' export type { DialogActionsProps } from './DialogActions' export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog' export type { DlcSelectionDialogProps } from './DlcSelectionDialog' +export type { AddDlcDialogProps } from './AddDlcDialog' export type { ConflictDialogProps, Conflict } from './ConflictDialog' export type { UnlockerSelectionDialogProps } from './UnlockerSelectionDialog' \ No newline at end of file diff --git a/src/styles/components/dialogs/_dlc_dialog.scss b/src/styles/components/dialogs/_dlc_dialog.scss index 3c62b87..e4ff838 100644 --- a/src/styles/components/dialogs/_dlc_dialog.scss +++ b/src/styles/components/dialogs/_dlc_dialog.scss @@ -209,6 +209,51 @@ } } +// Add DLC manually form +.add-dlc-form { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.add-dlc-field { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.add-dlc-label { + font-size: 0.85rem; + color: var(--text-secondary); + font-weight: 500; +} + +.add-dlc-input { + background-color: var(--border-dark); + border: 1px solid var(--border-soft); + border-radius: 4px; + color: var(--text-primary); + padding: 0.6rem 1rem; + font-size: 0.9rem; + transition: all var(--duration-normal) var(--easing-ease-out); + + &:focus { + border-color: var(--primary-color); + outline: none; + box-shadow: 0px 0px 6px rgba(245, 150, 130, 0.2); + } + + &::placeholder { + color: var(--text-muted); + } +} + +.add-dlc-error { + font-size: 0.82rem; + color: var(--error); + margin: 0; +} + // Loading animations @keyframes spin { 0% {