This commit is contained in:
Novattz
2026-03-28 15:07:50 +01:00
parent 1571e9d87d
commit 0480d523e3
8 changed files with 225 additions and 9 deletions

View File

@@ -64,6 +64,8 @@ function App() {
handleSettingsOpen, handleSettingsOpen,
handleSettingsClose, handleSettingsClose,
handleSmokeAPISettingsOpen, handleSmokeAPISettingsOpen,
handleOpenRating,
reportingEnabled,
showToast, showToast,
unlockerSelectionDialog, unlockerSelectionDialog,
handleSelectCreamLinux, handleSelectCreamLinux,
@@ -143,6 +145,8 @@ function App() {
onAction={handleGameAction} onAction={handleGameAction}
onEdit={handleGameEdit} onEdit={handleGameEdit}
onSmokeAPISettings={handleSmokeAPISettingsOpen} onSmokeAPISettings={handleSmokeAPISettingsOpen}
onRate={handleOpenRating}
reportingEnabled={reportingEnabled}
/> />
)} )}
</div> </div>
@@ -190,6 +194,7 @@ function App() {
{/* Unlocker Selection Dialog */} {/* Unlocker Selection Dialog */}
<UnlockerSelectionDialog <UnlockerSelectionDialog
visible={unlockerSelectionDialog.visible} visible={unlockerSelectionDialog.visible}
gameId={unlockerSelectionDialog.gameId}
gameTitle={unlockerSelectionDialog.gameTitle || ''} gameTitle={unlockerSelectionDialog.gameTitle || ''}
onClose={closeUnlockerDialog} onClose={closeUnlockerDialog}
onSelectCreamLinux={handleSelectCreamLinux} onSelectCreamLinux={handleSelectCreamLinux}

View File

@@ -1,6 +1,8 @@
export { default as LoadingIndicator } from './LoadingIndicator' export { default as LoadingIndicator } from './LoadingIndicator'
export { default as ProgressBar } from './ProgressBar' export { default as ProgressBar } from './ProgressBar'
export { default as Dropdown } from './Dropdown' export { default as Dropdown } from './Dropdown'
export { default as VotesDisplay } from './VotesDisplay'
export type { LoadingSize, LoadingType } from './LoadingIndicator' export type { LoadingSize, LoadingType } from './LoadingIndicator'
export type { DropdownOption } from './Dropdown' export type { DropdownOption } from './Dropdown'
export type { GameVotes } from './VotesDisplay'

View File

@@ -11,7 +11,10 @@ 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'
export { default as DisclaimerDialog } from './DisclaimerDialog' export { default as DisclaimerDialog } from './DisclaimerDialog'
export { default as UnlockerSelectionDialog} from './UnlockerSelectionDialog' export { default as UnlockerSelectionDialog } from './UnlockerSelectionDialog'
export { default as OptInDialog } from './OptInDialog'
export { default as RatingDialog } from './RatingDialog'
export { default as SmokeAPIVotesDialog } from './SmokeAPIVotesDialog'
// Export types // Export types
export type { DialogProps } from './Dialog' export type { DialogProps } from './Dialog'
@@ -24,3 +27,5 @@ export type { DlcSelectionDialogProps } from './DlcSelectionDialog'
export type { AddDlcDialogProps } from './AddDlcDialog' 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'
export type { RatingDialogProps } from './RatingDialog'
export type { SmokeAPIVotesDialogProps } from './SmokeAPIVotesDialog'

View File

@@ -9,13 +9,15 @@ interface GameItemProps {
onAction: (gameId: string, action: ActionType) => Promise<void> onAction: (gameId: string, action: ActionType) => Promise<void>
onEdit?: (gameId: string) => void onEdit?: (gameId: string) => void
onSmokeAPISettings?: (gameId: string) => void onSmokeAPISettings?: (gameId: string) => void
onRate?: (gameId: string) => void
reportingEnabled?: boolean // When false/undefined, rate button is not rendered at all.
} }
/** /**
* Individual game card component * Individual game card component
* Displays game information and action buttons * Displays game information and action buttons
*/ */
const GameItem = ({ game, onAction, onEdit, onSmokeAPISettings }: GameItemProps) => { const GameItem = ({ game, onAction, onEdit, onSmokeAPISettings, onRate, reportingEnabled }: GameItemProps) => {
const [imageUrl, setImageUrl] = useState<string | null>(null) const [imageUrl, setImageUrl] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [hasError, setHasError] = useState(false) const [hasError, setHasError] = useState(false)
@@ -93,6 +95,13 @@ const GameItem = ({ game, onAction, onEdit, onSmokeAPISettings }: GameItemProps)
} }
} }
// Rating handler
const handleRate = () => {
if (onRate && (game.cream_installed || game.smoke_installed)) {
onRate(game.id)
}
}
// Determine background image // Determine background image
const backgroundImage = const backgroundImage =
!isLoading && imageUrl !isLoading && imageUrl
@@ -179,6 +188,20 @@ const GameItem = ({ game, onAction, onEdit, onSmokeAPISettings }: GameItemProps)
</div> </div>
)} )}
{/* Rate button */}
{(game.cream_installed || game.smoke_installed) && onRate && reportingEnabled && (
<Button
variant="primary"
size="small"
onClick={handleRate}
disabled={!!game.installing}
title="Rate compatibility"
className="edit-button rate-button"
leftIcon={<Icon name="Star" variant="solid" size="md" />}
iconOnly
/>
)}
{/* Edit button - only enabled if CreamLinux is installed */} {/* Edit button - only enabled if CreamLinux is installed */}
{game.cream_installed && ( {game.cream_installed && (
<Button <Button

View File

@@ -10,13 +10,15 @@ interface GameListProps {
onAction: (gameId: string, action: ActionType) => Promise<void> onAction: (gameId: string, action: ActionType) => Promise<void>
onEdit?: (gameId: string) => void onEdit?: (gameId: string) => void
onSmokeAPISettings?: (gameId: string) => void onSmokeAPISettings?: (gameId: string) => void
onRate?: (gameId: string) => void
reportingEnabled?: boolean
} }
/** /**
* Main game list component * Main game list component
* Displays games in a grid with search and filtering applied * Displays games in a grid with search and filtering applied
*/ */
const GameList = ({ games, isLoading, onAction, onEdit, onSmokeAPISettings }: GameListProps) => { const GameList = ({ games, isLoading, onAction, onEdit, onSmokeAPISettings, onRate, reportingEnabled }: GameListProps) => {
const [imagesPreloaded, setImagesPreloaded] = useState(false) const [imagesPreloaded, setImagesPreloaded] = useState(false)
// Sort games alphabetically by title // Sort games alphabetically by title
@@ -57,7 +59,7 @@ const GameList = ({ games, isLoading, onAction, onEdit, onSmokeAPISettings }: Ga
) : ( ) : (
<div className="game-grid"> <div className="game-grid">
{sortedGames.map((game) => ( {sortedGames.map((game) => (
<GameItem key={game.id} game={game} onAction={onAction} onEdit={onEdit} onSmokeAPISettings={onSmokeAPISettings} /> <GameItem key={game.id} game={game} onAction={onAction} onEdit={onEdit} onSmokeAPISettings={onSmokeAPISettings} onRate={onRate} reportingEnabled={reportingEnabled} />
))} ))}
</div> </div>
)} )}

View File

@@ -26,6 +26,14 @@ export interface SmokeAPISettingsDialogState {
gameTitle: string gameTitle: string
} }
export interface RatingDialogState {
visible: boolean
gameId: string
gameTitle: string
unlocker: 'creamlinux' | 'smokeapi'
steamPath: string
}
// Define the context type // Define the context type
export interface AppContextType { export interface AppContextType {
// Game state // Game state
@@ -56,6 +64,22 @@ export interface AppContextType {
handleSmokeAPISettingsOpen: (gameId: string) => void handleSmokeAPISettingsOpen: (gameId: string) => void
handleSmokeAPISettingsClose: () => void handleSmokeAPISettingsClose: () => void
// SmokeAPI votes dialog
smokeAPIVotesDialog: {
visible: boolean
gameId: string | null
gameTitle: string | null
}
handleSmokeAPIVotesClose: () => void
handleSmokeAPIVotesConfirm: () => void
// Rating dialog
ratingDialog: RatingDialogState
handleOpenRating: (gameId: string) => void
handleCloseRating: () => void
handleSubmitRating: (worked: boolean) => Promise<void>
reportingEnabled: boolean
// Toast notifications // Toast notifications
showToast: ( showToast: (
message: string, message: string,

View File

@@ -1,10 +1,11 @@
import { ReactNode, useState } from 'react' import { ReactNode, useState, useEffect } from 'react'
import { AppContext, AppContextType } from './AppContext' import { AppContext, AppContextType } from './AppContext'
import { useGames, useDlcManager, useGameActions, useToasts } from '@/hooks' import { useGames, useDlcManager, useGameActions, useToasts } from '@/hooks'
import { DlcInfo } from '@/types' import { DlcInfo, Config } from '@/types'
import { ActionType } from '@/components/buttons/ActionButton' import { ActionType } from '@/components/buttons/ActionButton'
import { ToastContainer } from '@/components/notifications' import { ToastContainer } from '@/components/notifications'
import { SmokeAPISettingsDialog } from '@/components/dialogs' import { SmokeAPISettingsDialog, OptInDialog, RatingDialog, SmokeAPIVotesDialog } from '@/components/dialogs'
import { invoke } from '@tauri-apps/api/core'
// Context provider component // Context provider component
interface AppProviderProps { interface AppProviderProps {
@@ -53,6 +54,47 @@ export const AppProvider = ({ children }: AppProviderProps) => {
gameTitle: '', gameTitle: '',
}) })
// SmokeAPI votes dialog state
const [smokeAPIVotesDialog, setSmokeAPIVotesDialog] = useState<{
visible: boolean
gameId: string | null
gameTitle: string | null
}>({
visible: false,
gameId: null,
gameTitle: null,
})
// Opt-in dialog state
const [optInDialog, setOptInDialog] = useState(false)
const [reportingEnabled, setReportingEnabled] = useState(false)
// Rating dialog state
const [ratingDialog, setRatingDialog] = useState<{
visible: boolean
gameId: string
gameTitle: string
unlocker: 'creamlinux' | 'smokeapi'
steamPath: string
}>({
visible: false,
gameId: '',
gameTitle: '',
unlocker: 'creamlinux',
steamPath: '',
})
useEffect(() => {
invoke<Config>('load_config')
.then((cfg) => {
setReportingEnabled(cfg.reporting_opted_in)
if (!cfg.reporting_has_seen_prompt) {
setOptInDialog(true)
}
})
.catch((err) => console.error('Failed to load config for reporting check:', err))
}, [])
// Settings handlers // Settings handlers
const handleSettingsOpen = () => { const handleSettingsOpen = () => {
setSettingsDialog({ visible: true }) setSettingsDialog({ visible: true })
@@ -85,6 +127,69 @@ export const AppProvider = ({ children }: AppProviderProps) => {
}) })
} }
const handleSmokeAPIVotesClose = () => {
setSmokeAPIVotesDialog({ visible: false, gameId: null, gameTitle: null })
}
const handleSmokeAPIVotesConfirm = () => {
const gameId = smokeAPIVotesDialog.gameId
setSmokeAPIVotesDialog({ visible: false, gameId: null, gameTitle: null })
if (gameId) {
// Now actually run the install
executeGameAction(gameId, 'install_smoke', games)
}
}
const handleOptInAccept = async () => {
try {
await invoke('set_reporting_opt_in', { optedIn: true })
setReportingEnabled(true)
} catch (err) {
console.error('Failed to save reporting opt-in:', err)
}
setOptInDialog(false)
}
const handleOptInDecline = async () => {
try {
await invoke('set_reporting_opt_in', { optedIn: false })
setReportingEnabled(false)
} catch (err) {
console.error('Failed to save reporting opt-out:', err)
}
setOptInDialog(false)
}
const handleOpenRating = (gameId: string) => {
const game = games.find((g) => g.id === gameId)
if (!game) return
setRatingDialog({
visible: true,
gameId,
gameTitle: game.title,
unlocker: game.cream_installed ? 'creamlinux' : 'smokeapi',
steamPath: game.path,
})
}
const handleCloseRating = () => {
setRatingDialog((prev) => ({ ...prev, visible: false }))
}
const handleSubmitRating = async (worked: boolean) => {
try {
await invoke('submit_report', {
gameId: ratingDialog.gameId,
unlocker: ratingDialog.unlocker,
worked,
steamPath: ratingDialog.steamPath,
})
} catch (err) {
console.error('Failed to submit rating:', err)
}
}
// Game action handler with proper error reporting // Game action handler with proper error reporting
const handleGameAction = async (gameId: string, action: ActionType) => { const handleGameAction = async (gameId: string, action: ActionType) => {
const game = games.find((g) => g.id === gameId) const game = games.find((g) => g.id === gameId)
@@ -117,6 +222,16 @@ export const AppProvider = ({ children }: AppProviderProps) => {
} }
} }
// intercept install_smoke for votes dialog
if (action === 'install_smoke' && !game.native) {
setSmokeAPIVotesDialog({
visible: true,
gameId: game.id,
gameTitle: game.title,
})
return
}
// For install_unlocker action, executeGameAction will handle showing the dialog // For install_unlocker action, executeGameAction will handle showing the dialog
// We should NOT show any notifications here - they'll be shown after actual installation // We should NOT show any notifications here - they'll be shown after actual installation
if (action === 'install_unlocker') { if (action === 'install_unlocker') {
@@ -267,6 +382,18 @@ export const AppProvider = ({ children }: AppProviderProps) => {
handleSmokeAPISettingsOpen, handleSmokeAPISettingsOpen,
handleSmokeAPISettingsClose, handleSmokeAPISettingsClose,
// SmokeAPI Votes
smokeAPIVotesDialog,
handleSmokeAPIVotesClose,
handleSmokeAPIVotesConfirm,
// Rating
ratingDialog,
handleOpenRating,
handleCloseRating,
handleSubmitRating,
reportingEnabled,
// Toast notifications // Toast notifications
showToast, showToast,
@@ -330,6 +457,32 @@ export const AppProvider = ({ children }: AppProviderProps) => {
gamePath={smokeAPISettingsDialog.gamePath} gamePath={smokeAPISettingsDialog.gamePath}
gameTitle={smokeAPISettingsDialog.gameTitle} gameTitle={smokeAPISettingsDialog.gameTitle}
/> />
{/* SmokeAPI Votes Dialog */}
<SmokeAPIVotesDialog
visible={smokeAPIVotesDialog.visible}
gameId={smokeAPIVotesDialog.gameId}
gameTitle={smokeAPIVotesDialog.gameTitle}
onClose={handleSmokeAPIVotesClose}
onConfirm={handleSmokeAPIVotesConfirm}
/>
{/* Opt-in Dialog */}
<OptInDialog
visible={optInDialog}
onAccept={handleOptInAccept}
onDecline={handleOptInDecline}
/>
{/* Rating Dialog */}
<RatingDialog
visible={ratingDialog.visible}
gameId={ratingDialog.gameId}
gameTitle={ratingDialog.gameTitle}
unlocker={ratingDialog.unlocker}
onClose={handleCloseRating}
onSubmit={handleSubmitRating}
/>
</AppContext.Provider> </AppContext.Provider>
) )
} }

View File

@@ -5,4 +5,6 @@
export interface Config { export interface Config {
/** Whether to show the disclaimer on startup */ /** Whether to show the disclaimer on startup */
show_disclaimer: boolean show_disclaimer: boolean
reporting_opted_in: boolean
reporting_has_seen_prompt: boolean
} }