Broken animation changes but ill fix it

This commit is contained in:
Tickbase
2025-05-18 02:34:29 +02:00
parent 985f804a16
commit e29f44bbd5
5 changed files with 213 additions and 121 deletions

View File

@@ -126,7 +126,7 @@ function App() {
// Listen for progress updates from the backend // Listen for progress updates from the backend
const unlistenProgress = await listen('installation-progress', (event) => { const unlistenProgress = await listen('installation-progress', (event) => {
console.log('Received installation-progress event:', event) console.log('Received installation-progress event:', event)
const { title, message, progress, complete, show_instructions, instructions } = const { title, message, progress, complete, show_instructions, instructions } =
event.payload as { event.payload as {
title: string title: string
@@ -136,32 +136,30 @@ function App() {
show_instructions?: boolean show_instructions?: boolean
instructions?: InstructionInfo instructions?: InstructionInfo
} }
// Always update progress dialog
setProgressDialog({
visible: true, // Always set to visible - our ProgressDialog component handles exit animation
title,
message,
progress,
showInstructions: show_instructions || false,
instructions,
})
// If complete and no instructions, we need to schedule a game list refresh
// The dialog will auto-close with animation, but we still need to refresh the games
if (complete && !show_instructions) { if (complete && !show_instructions) {
// Hide dialog when complete if no instructions // Schedule a refresh for after the dialog closes
setTimeout(() => { if (!refreshInProgress.current) {
setProgressDialog((prev) => ({ ...prev, visible: false })) refreshInProgress.current = true
// Wait for dialog animation + close delay (about 1.2s total)
// Only refresh games list if dialog is closing without instructions setTimeout(() => {
if (!refreshInProgress.current) { loadGames().then(() => {
refreshInProgress.current = true refreshInProgress.current = false
setTimeout(() => { })
loadGames().then(() => { }, 1300)
refreshInProgress.current = false }
})
}, 100)
}
}, 1000)
} else {
// Update progress dialog
setProgressDialog({
visible: true,
title,
message,
progress,
showInstructions: show_instructions || false,
instructions,
})
} }
}) })
@@ -379,9 +377,9 @@ function App() {
}, []) }, [])
const handleCloseProgressDialog = () => { const handleCloseProgressDialog = () => {
// Just hide the dialog without refreshing game list // Set dialog to not visible - animation is handled by the component
setProgressDialog((prev) => ({ ...prev, visible: false })) setProgressDialog((prev) => ({ ...prev, visible: false }))
// Only refresh if we need to (instructions didn't trigger update) // Only refresh if we need to (instructions didn't trigger update)
if (progressDialog.showInstructions === false && !refreshInProgress.current) { if (progressDialog.showInstructions === false && !refreshInProgress.current) {
refreshInProgress.current = true refreshInProgress.current = true
@@ -650,41 +648,42 @@ function App() {
// Cancel any in-progress DLC fetching // Cancel any in-progress DLC fetching
if (isFetchingDlcs && activeDlcFetchId.current) { if (isFetchingDlcs && activeDlcFetchId.current) {
console.log(`Aborting DLC fetch for game ${activeDlcFetchId.current}`) console.log(`Aborting DLC fetch for game ${activeDlcFetchId.current}`)
// This will signal to the Rust backend that we want to stop the process // This will signal to the Rust backend that we want to stop the process
invoke('abort_dlc_fetch', { gameId: activeDlcFetchId.current }).catch((err) => invoke('abort_dlc_fetch', { gameId: activeDlcFetchId.current }).catch((err) =>
console.error('Error aborting DLC fetch:', err) console.error('Error aborting DLC fetch:', err)
) )
// Reset state // Reset state
activeDlcFetchId.current = null activeDlcFetchId.current = null
setIsFetchingDlcs(false) setIsFetchingDlcs(false)
} }
// Clear controller // Clear controller
if (dlcFetchController.current) { if (dlcFetchController.current) {
dlcFetchController.current.abort() dlcFetchController.current.abort()
dlcFetchController.current = null dlcFetchController.current = null
} }
// Close dialog // Close dialog - animation is handled in DlcSelectionDialog
setDlcDialog((prev) => ({ ...prev, visible: false })) setDlcDialog((prev) => ({ ...prev, visible: false }))
} }
// Handle DLC selection confirmation // Handle DLC selection confirmation
const handleDlcConfirm = async (selectedDlcs: DlcInfo[]) => { const handleDlcConfirm = async (selectedDlcs: DlcInfo[]) => {
// Close the dialog first // The dialog has already started its exit animation
// Just make sure it's marked as invisible
setDlcDialog((prev) => ({ ...prev, visible: false })) setDlcDialog((prev) => ({ ...prev, visible: false }))
const gameId = dlcDialog.gameId const gameId = dlcDialog.gameId
const game = games.find((g) => g.id === gameId) const game = games.find((g) => g.id === gameId)
if (!game) return if (!game) return
// Update local state to show installation in progress // Update local state to show installation in progress
setGames((prevGames) => setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g)) prevGames.map((g) => (g.id === gameId ? { ...g, installing: true } : g))
) )
try { try {
if (dlcDialog.isEditMode) { if (dlcDialog.isEditMode) {
// If in edit mode, we're updating existing cream_api.ini // If in edit mode, we're updating existing cream_api.ini
@@ -697,13 +696,13 @@ function App() {
showInstructions: false, showInstructions: false,
instructions: undefined, instructions: undefined,
}) })
// Call the backend to update the DLC configuration // Call the backend to update the DLC configuration
await invoke('update_dlc_configuration_command', { await invoke('update_dlc_configuration_command', {
gamePath: game.path, gamePath: game.path,
dlcs: selectedDlcs, dlcs: selectedDlcs,
}) })
// Update progress dialog for completion // Update progress dialog for completion
setProgressDialog((prev) => ({ setProgressDialog((prev) => ({
...prev, ...prev,
@@ -711,10 +710,12 @@ function App() {
message: 'DLC configuration updated successfully!', message: 'DLC configuration updated successfully!',
progress: 100, progress: 100,
})) }))
// Hide dialog after a delay // The ProgressDialog component will now handle the exit animation
// when progress reaches 100% after a delay
// But we still need to reset installing state with a delay
setTimeout(() => { setTimeout(() => {
setProgressDialog((prev) => ({ ...prev, visible: false }))
// Reset installing state // Reset installing state
setGames((prevGames) => setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g)) prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
@@ -731,7 +732,7 @@ function App() {
showInstructions: false, showInstructions: false,
instructions: undefined, instructions: undefined,
}) })
// Invoke the installation with the selected DLCs // Invoke the installation with the selected DLCs
await invoke('install_cream_with_dlcs_command', { await invoke('install_cream_with_dlcs_command', {
gameId, gameId,
@@ -740,29 +741,26 @@ function App() {
console.error(`Error installing CreamLinux with selected DLCs:`, err) console.error(`Error installing CreamLinux with selected DLCs:`, err)
throw err throw err
}) })
// We don't need to manually close the dialog or update the game state // We don't need to manually close the dialog or update the game state
// because the backend will emit progress events that handle this // because the backend will emit progress events that handle this
} }
} catch (error) { } catch (error) {
console.error('Error processing DLC selection:', error) console.error('Error processing DLC selection:', error)
// Show error in progress dialog // Show error in progress dialog
setProgressDialog((prev) => ({ setProgressDialog((prev) => ({
...prev, ...prev,
message: `Error: ${error}`, message: `Error: ${error}`,
progress: 100, progress: 100,
})) }))
// Reset installing state // Reset installing state
setGames((prevGames) => setGames((prevGames) =>
prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g)) prevGames.map((g) => (g.id === gameId ? { ...g, installing: false } : g))
) )
// Hide dialog after a delay // ProgressDialog will handle exit animation automatically
setTimeout(() => {
setProgressDialog((prev) => ({ ...prev, visible: false }))
}, 3000)
} }
} }

View File

@@ -31,10 +31,13 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
estimatedTimeLeft = '', estimatedTimeLeft = '',
}) => { }) => {
const [selectedDlcs, setSelectedDlcs] = useState<DlcInfo[]>([]) const [selectedDlcs, setSelectedDlcs] = useState<DlcInfo[]>([])
const [showContent, setShowContent] = useState(false) // Use a simple string for animation state - keep it simple
const [animationState, setAnimationState] = useState('closed')
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)
// Track previous visibility to detect changes
const [prevVisible, setPrevVisible] = useState(false)
// Initialize selected DLCs when DLC list changes // Initialize selected DLCs when DLC list changes
useEffect(() => { useEffect(() => {
@@ -50,19 +53,33 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
} }
}, [visible, dlcs, initialized]) }, [visible, dlcs, initialized])
// Handle visibility changes // Handle animations on visibility changes
useEffect(() => { useEffect(() => {
if (visible) { // Only respond to actual changes in visibility
// Show content immediately for better UX if (visible !== prevVisible) {
const timer = setTimeout(() => { if (visible) {
setShowContent(true) // Show animation
}, 50) setAnimationState('visible')
return () => clearTimeout(timer) } else {
} else { // Hide animation - but only if we're currently visible
setShowContent(false) if (animationState === 'visible') {
setInitialized(false) // Reset initialized state when dialog closes setAnimationState('hiding')
// After animation completes, set to closed
const timer = setTimeout(() => {
setAnimationState('closed')
// Also reset initialization when fully closed
if (!visible) {
setInitialized(false)
}
}, 200) // Match animation duration
return () => clearTimeout(timer)
}
}
// Update previous visibility
setPrevVisible(visible)
} }
}, [visible]) }, [visible, prevVisible, animationState])
// Memoize filtered DLCs to avoid unnecessary recalculations // Memoize filtered DLCs to avoid unnecessary recalculations
const filteredDlcs = useMemo(() => { const filteredDlcs = useMemo(() => {
@@ -115,10 +132,11 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
} }
const handleConfirm = () => { const handleConfirm = () => {
// Just call onConfirm directly
onConfirm(selectedDlcs) onConfirm(selectedDlcs)
} }
// Modified to prevent closing when loading // Handle overlay click
const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => { const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
// Prevent clicks from propagating through the overlay // Prevent clicks from propagating through the overlay
e.stopPropagation() e.stopPropagation()
@@ -129,6 +147,11 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
} }
} }
// Handle the close button click
const handleClose = () => {
onClose()
}
// Count selected DLCs // Count selected DLCs
const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length const selectedCount = selectedDlcs.filter((dlc) => dlc.enabled).length
@@ -142,14 +165,19 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
return '' return ''
} }
if (!visible) return null // Don't render anything if we're in closed state
if (animationState === 'closed') return null
// Generate appropriate classes based on animation state
const dialogClasses = `dlc-dialog-overlay ${animationState === 'hiding' ? 'exiting' : 'visible'}`
const contentClasses = `dlc-selection-dialog dialog-${animationState === 'hiding' ? 'exiting' : 'visible'}`
return ( return (
<div <div
className={`dlc-dialog-overlay ${showContent ? 'visible' : ''}`} className={dialogClasses}
onClick={handleOverlayClick} onClick={handleOverlayClick}
> >
<div className={`dlc-selection-dialog ${showContent ? 'dialog-visible' : ''}`}> <div className={contentClasses}>
<div className="dlc-dialog-header"> <div className="dlc-dialog-header">
<h3>{isEditMode ? 'Edit DLCs' : 'Select DLCs to Enable'}</h3> <h3>{isEditMode ? 'Edit DLCs' : 'Select DLCs to Enable'}</h3>
<div className="dlc-game-info"> <div className="dlc-game-info">
@@ -222,7 +250,7 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
<div className="dlc-dialog-actions"> <div className="dlc-dialog-actions">
<button <button
className="cancel-button" className="cancel-button"
onClick={onClose} onClick={handleClose}
disabled={isLoading && loadingProgress < 10} // Briefly disable to prevent accidental closing at start disabled={isLoading && loadingProgress < 10} // Briefly disable to prevent accidental closing at start
> >
Cancel Cancel
@@ -236,4 +264,4 @@ const DlcSelectionDialog: React.FC<DlcSelectionDialogProps> = ({
) )
} }
export default DlcSelectionDialog export default DlcSelectionDialog

View File

@@ -27,24 +27,56 @@ const ProgressDialog: React.FC<ProgressDialogProps> = ({
onClose, onClose,
}) => { }) => {
const [copySuccess, setCopySuccess] = useState(false) const [copySuccess, setCopySuccess] = useState(false)
const [showContent, setShowContent] = useState(false) // Use a simple string for animation state - keep it simple
const [animationState, setAnimationState] = useState('closed')
// Reset copy state when dialog visibility changes // Track previous visibility to detect changes
const [prevVisible, setPrevVisible] = useState(false)
// Handle animations on visibility changes
useEffect(() => { useEffect(() => {
if (!visible) { // Only respond to actual changes in visibility
setCopySuccess(false) if (visible !== prevVisible) {
setShowContent(false) if (visible) {
} else { // Show animation
// Add a small delay to trigger the entrance animation setAnimationState('visible')
} else {
// Hide animation - but only if we're currently visible
if (animationState === 'visible') {
setAnimationState('hiding')
// After animation completes, set to closed
const timer = setTimeout(() => {
setAnimationState('closed')
}, 200) // Match animation duration
return () => clearTimeout(timer)
}
}
// Update previous visibility
setPrevVisible(visible)
}
}, [visible, prevVisible, animationState])
// Auto-close on progress completion
useEffect(() => {
// Only auto-close if showing and progress reaches 100% and not showing instructions
if (visible && progress >= 100 && !showInstructions && animationState === 'visible') {
// Wait a moment before closing
const timer = setTimeout(() => { const timer = setTimeout(() => {
setShowContent(true) // Only proceed if we're still in visible state
}, 50) if (animationState === 'visible' && onClose) {
onClose() // Call the onClose function directly
}
}, 1000) // Wait 1 second after completion
return () => clearTimeout(timer) return () => clearTimeout(timer)
} }
}, [visible]) }, [progress, showInstructions, animationState, visible, onClose])
if (!visible) return null // Don't render if state is closed
if (animationState === 'closed') {
return null
}
const handleCopyCommand = () => { const handleCopyCommand = () => {
if (instructions?.command) { if (instructions?.command) {
navigator.clipboard.writeText(instructions.command) navigator.clipboard.writeText(instructions.command)
@@ -58,13 +90,10 @@ const ProgressDialog: React.FC<ProgressDialogProps> = ({
} }
const handleClose = () => { const handleClose = () => {
setShowContent(false) // If we can close, just call onClose directly
// Delay closing to allow exit animation if (onClose) {
setTimeout(() => { onClose()
if (onClose) { }
onClose()
}
}, 300)
} }
// Prevent closing when in progress // Prevent closing when in progress
@@ -146,13 +175,17 @@ const ProgressDialog: React.FC<ProgressDialogProps> = ({
// Determine if close button should be enabled // Determine if close button should be enabled
const isCloseButtonEnabled = showInstructions || progress >= 100 const isCloseButtonEnabled = showInstructions || progress >= 100
// Generate appropriate classes based on animation state
const dialogClasses = `progress-dialog-overlay ${animationState === 'hiding' ? 'exiting' : 'visible'}`
const contentClasses = `progress-dialog ${showInstructions ? 'with-instructions' : ''} dialog-${animationState === 'hiding' ? 'exiting' : 'visible'}`
return ( return (
<div <div
className={`progress-dialog-overlay ${showContent ? 'visible' : ''}`} className={dialogClasses}
onClick={handleOverlayClick} onClick={handleOverlayClick}
> >
<div <div
className={`progress-dialog ${showInstructions ? 'with-instructions' : ''} ${showContent ? 'dialog-visible' : ''}`} className={contentClasses}
> >
<h3>{title}</h3> <h3>{title}</h3>
<p>{message}</p> <p>{message}</p>
@@ -206,4 +239,4 @@ const ProgressDialog: React.FC<ProgressDialogProps> = ({
) )
} }
export default ProgressDialog export default ProgressDialog

View File

@@ -1,6 +1,29 @@
@use '../variables' as *; @use '../variables' as *;
@use '../mixins' as *; @use '../mixins' as *;
// Shared keyframes for both enter and exit animations
@keyframes modal-appear {
0% {
opacity: 0;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
}
}
@keyframes modal-disappear {
0% {
opacity: 1;
transform: scale(1);
}
100% {
opacity: 0;
transform: scale(0.95);
}
}
// Progress Dialog // Progress Dialog
.progress-dialog-overlay { .progress-dialog-overlay {
position: fixed; position: fixed;
@@ -13,22 +36,19 @@
@include flex-center; @include flex-center;
z-index: var(--z-modal); z-index: var(--z-modal);
opacity: 0; opacity: 0;
animation: modal-appear 0.2s ease-out;
cursor: pointer; cursor: pointer;
pointer-events: auto; // Ensure clicks work
// When visible, fade in
&.visible { &.visible {
opacity: 1; opacity: 1;
animation: modal-appear 0.2s ease-out forwards;
} }
@keyframes modal-appear { // When exiting, fade out
0% { &.exiting {
opacity: 0; animation: modal-disappear 0.2s ease-out forwards;
transform: scale(0.95); pointer-events: none; // Prevent clicks during exit animation
}
100% {
opacity: 1;
transform: scale(1);
}
} }
} }
@@ -36,32 +56,31 @@
background-color: var(--elevated-bg); background-color: var(--elevated-bg);
border-radius: 8px; border-radius: 8px;
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.3); // shadow-glow box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.3);
width: 450px; width: 450px;
max-width: 90vw; max-width: 90vw;
border: 1px solid var(--border-soft); border: 1px solid var(--border-soft);
opacity: 0; opacity: 0;
transform: scale(0.95);
cursor: default; cursor: default;
// When visible, show the dialog
&.dialog-visible { &.dialog-visible {
transform: scale(1);
opacity: 1; opacity: 1;
transform: scale(1);
transition: transform 0.2s var(--easing-bounce), opacity 0.2s ease-out;
}
// When exiting, hide the dialog
&.dialog-exiting {
opacity: 0;
transform: scale(0.95);
transition: transform 0.2s ease-in, opacity 0.2s ease-in;
} }
&.with-instructions { &.with-instructions {
width: 500px; width: 500px;
} }
h3 {
font-weight: 700;
margin-bottom: 1rem;
color: var(--text-primary);
}
p {
margin-bottom: 1rem;
color: var(--text-secondary);
}
} }
// Progress bar // Progress bar

View File

@@ -13,10 +13,18 @@
z-index: var(--z-modal); z-index: var(--z-modal);
opacity: 0; opacity: 0;
cursor: pointer; cursor: pointer;
pointer-events: auto; // Ensure clicks work
// When visible, fade in
&.visible { &.visible {
opacity: 1; opacity: 1;
animation: modal-appear 0.2s ease-out; animation: modal-appear 0.2s ease-out forwards;
}
// When exiting, fade out
&.exiting {
animation: modal-disappear 0.2s ease-out forwards;
pointer-events: none; // Prevent clicks during exit animation
} }
} }
@@ -34,12 +42,18 @@
opacity: 0; opacity: 0;
transform: scale(0.95); transform: scale(0.95);
// When visible, show the dialog
&.dialog-visible { &.dialog-visible {
transform: scale(1);
opacity: 1; opacity: 1;
transition: transform: scale(1);
transform 0.2s var(--easing-bounce), transition: transform 0.2s var(--easing-bounce), opacity 0.2s ease-out;
opacity 0.2s ease-out; }
// When exiting, hide the dialog
&.dialog-exiting {
opacity: 0;
transform: scale(0.95);
transition: transform 0.2s ease-in, opacity 0.2s ease-in;
} }
} }