mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-05-01 20:42:04 -04:00
Manually add DLC dialog #99
This commit is contained in:
93
src/components/dialogs/AddDlcDialog.tsx
Normal file
93
src/components/dialogs/AddDlcDialog.tsx
Normal file
@@ -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<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Dialog visible={visible} onClose={onClose} size="small">
|
||||||
|
<DialogHeader onClose={onClose}>
|
||||||
|
<h3>Add DLC Manually</h3>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogBody>
|
||||||
|
<div className="add-dlc-form">
|
||||||
|
<div className="add-dlc-field">
|
||||||
|
<label className="add-dlc-label">DLC ID</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="add-dlc-input"
|
||||||
|
placeholder="e.g. 1234560"
|
||||||
|
value={id}
|
||||||
|
onChange={(e) => { setId(e.target.value); setError('') }}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="add-dlc-field">
|
||||||
|
<label className="add-dlc-label">DLC Name</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="add-dlc-input"
|
||||||
|
placeholder="e.g. Expansion - My DLC"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => { setName(e.target.value); setError('') }}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleSubmit()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p className="add-dlc-error">{error}</p>}
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="secondary" onClick={onClose}>Cancel</Button>
|
||||||
|
<Button variant="primary" onClick={handleSubmit}>Add DLC</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AddDlcDialog
|
||||||
@@ -4,6 +4,7 @@ import DialogHeader from './DialogHeader'
|
|||||||
import DialogBody from './DialogBody'
|
import DialogBody from './DialogBody'
|
||||||
import DialogFooter from './DialogFooter'
|
import DialogFooter from './DialogFooter'
|
||||||
import DialogActions from './DialogActions'
|
import DialogActions from './DialogActions'
|
||||||
|
import AddDlcDialog from './AddDlcDialog'
|
||||||
import { Button, AnimatedCheckbox } from '@/components/buttons'
|
import { Button, AnimatedCheckbox } from '@/components/buttons'
|
||||||
import { DlcInfo } from '@/types'
|
import { DlcInfo } from '@/types'
|
||||||
import { Icon, check, info } from '@/components/icons'
|
import { Icon, check, info } from '@/components/icons'
|
||||||
@@ -51,6 +52,7 @@ const DlcSelectionDialog = ({
|
|||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [selectAll, setSelectAll] = useState(true)
|
const [selectAll, setSelectAll] = useState(true)
|
||||||
const [initialized, setInitialized] = useState(false)
|
const [initialized, setInitialized] = useState(false)
|
||||||
|
const [showAddDlc, setShowAddDlc] = useState(false)
|
||||||
|
|
||||||
// Reset dialog state when it opens or closes
|
// Reset dialog state when it opens or closes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -126,6 +128,11 @@ const DlcSelectionDialog = ({
|
|||||||
)
|
)
|
||||||
}, [selectAll])
|
}, [selectAll])
|
||||||
|
|
||||||
|
// Add a manually-entered DLC to the list
|
||||||
|
const handleAddDlc = useCallback((dlc: DlcInfo) => {
|
||||||
|
setSelectedDlcs((prev) => [...prev, dlc])
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Submit selected DLCs to parent component
|
// Submit selected DLCs to parent component
|
||||||
const handleConfirm = useCallback(() => {
|
const handleConfirm = useCallback(() => {
|
||||||
// Create a deep copy to prevent reference issues
|
// Create a deep copy to prevent reference issues
|
||||||
@@ -151,6 +158,7 @@ const DlcSelectionDialog = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
|
<Dialog visible={visible} onClose={onClose} size="large" preventBackdropClose={isLoading}>
|
||||||
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
<DialogHeader onClose={onClose} hideCloseButton={true}>
|
||||||
<h3>{dialogTitle}</h3>
|
<h3>{dialogTitle}</h3>
|
||||||
@@ -251,6 +259,14 @@ const DlcSelectionDialog = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setShowAddDlc(true)}
|
||||||
|
disabled={isLoading || isUpdating}
|
||||||
|
>
|
||||||
|
Add DLC Manually
|
||||||
|
</Button>
|
||||||
|
|
||||||
{/* Update button - only show in edit mode */}
|
{/* Update button - only show in edit mode */}
|
||||||
{isEditMode && onUpdate && (
|
{isEditMode && onUpdate && (
|
||||||
<Button
|
<Button
|
||||||
@@ -268,6 +284,14 @@ const DlcSelectionDialog = ({
|
|||||||
</DialogActions>
|
</DialogActions>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<AddDlcDialog
|
||||||
|
visible={showAddDlc}
|
||||||
|
onClose={() => setShowAddDlc(false)}
|
||||||
|
onAdd={handleAddDlc}
|
||||||
|
existingIds={new Set(selectedDlcs.map((d) => d.appid))}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export { default as DialogFooter } from './DialogFooter'
|
|||||||
export { default as DialogActions } from './DialogActions'
|
export { default as DialogActions } from './DialogActions'
|
||||||
export { default as ProgressDialog } from './ProgressDialog'
|
export { default as ProgressDialog } from './ProgressDialog'
|
||||||
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
export { default as DlcSelectionDialog } from './DlcSelectionDialog'
|
||||||
|
export { default as AddDlcDialog } from './AddDlcDialog'
|
||||||
export { default as SettingsDialog } from './SettingsDialog'
|
export { default as SettingsDialog } from './SettingsDialog'
|
||||||
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
export { default as SmokeAPISettingsDialog } from './SmokeAPISettingsDialog'
|
||||||
export { default as ConflictDialog } from './ConflictDialog'
|
export { default as ConflictDialog } from './ConflictDialog'
|
||||||
@@ -20,5 +21,6 @@ export type { DialogFooterProps } from './DialogFooter'
|
|||||||
export type { DialogActionsProps } from './DialogActions'
|
export type { DialogActionsProps } from './DialogActions'
|
||||||
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
export type { ProgressDialogProps, InstallationInstructions } from './ProgressDialog'
|
||||||
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
|
||||||
|
export type { AddDlcDialogProps } from './AddDlcDialog'
|
||||||
export type { ConflictDialogProps, Conflict } from './ConflictDialog'
|
export type { ConflictDialogProps, Conflict } from './ConflictDialog'
|
||||||
export type { UnlockerSelectionDialogProps } from './UnlockerSelectionDialog'
|
export type { UnlockerSelectionDialogProps } from './UnlockerSelectionDialog'
|
||||||
@@ -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
|
// Loading animations
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
|
|||||||
Reference in New Issue
Block a user