mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-05-01 20:42:04 -04:00
Rate & opt-in dialog #22
This commit is contained in:
82
src/components/dialogs/OptInDialog.tsx
Normal file
82
src/components/dialogs/OptInDialog.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogHeader,
|
||||||
|
DialogBody,
|
||||||
|
DialogFooter,
|
||||||
|
DialogActions,
|
||||||
|
} from '@/components/dialogs'
|
||||||
|
import { Button } from '@/components/buttons'
|
||||||
|
import { Icon, info } from '@/components/icons'
|
||||||
|
|
||||||
|
interface OptInDialogProps {
|
||||||
|
visible: boolean
|
||||||
|
onAccept: () => void
|
||||||
|
onDecline: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* First-launch opt-in dialog for the compatibility reporting system.
|
||||||
|
* Shown once when the app fully starts. Does not close until the user makes
|
||||||
|
* an explicit choice.
|
||||||
|
*/
|
||||||
|
const OptInDialog: React.FC<OptInDialogProps> = ({ visible, onAccept, onDecline }) => {
|
||||||
|
return (
|
||||||
|
<Dialog visible={visible} onClose={() => {}} size="medium">
|
||||||
|
<DialogHeader onClose={() => {}} hideCloseButton={true}>
|
||||||
|
<h3>Help improve CreamLinux</h3>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogBody>
|
||||||
|
<div className="optin-content">
|
||||||
|
|
||||||
|
<p className="optin-intro">
|
||||||
|
CreamLinux can collect anonymous compatibility reports to help users know which
|
||||||
|
games work with CreamLinux and SmokeAPI before they install them.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="optin-details">
|
||||||
|
<h4>What we collect</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>A one-way anonymous hash</strong> derived from your machine ID, Steam
|
||||||
|
install path, and a locally-stored random salt. <em>This cannot be reversed
|
||||||
|
to identify you</em>, and even we cannot link it to your machine.
|
||||||
|
</li>
|
||||||
|
<li>The Steam App ID of the game you rated.</li>
|
||||||
|
<li>Which unlocker you used (CreamLinux or SmokeAPI).</li>
|
||||||
|
<li>Whether it worked or not.</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h4>What we do not collect</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Your username, IP address, or any personally identifiable information.</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="optin-notice">
|
||||||
|
<Icon name={info} variant="solid" size="md" />
|
||||||
|
<span>
|
||||||
|
If you opt out, the local salt will be deleted and no data will ever be sent.
|
||||||
|
You will not be able to submit compatibility votes, but the app works fully
|
||||||
|
without this feature.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogBody>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="secondary" onClick={onDecline}>
|
||||||
|
No thanks
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" onClick={onAccept}>
|
||||||
|
Enable reporting
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OptInDialog
|
||||||
164
src/components/dialogs/RatingDialog.tsx
Normal file
164
src/components/dialogs/RatingDialog.tsx
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogHeader,
|
||||||
|
DialogBody,
|
||||||
|
DialogFooter,
|
||||||
|
DialogActions,
|
||||||
|
} from '@/components/dialogs'
|
||||||
|
import { Button } from '@/components/buttons'
|
||||||
|
import { Icon, info } from '@/components/icons'
|
||||||
|
|
||||||
|
interface LocalReport {
|
||||||
|
game_id: string
|
||||||
|
unlocker: string
|
||||||
|
worked: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RatingDialogProps {
|
||||||
|
visible: boolean
|
||||||
|
gameTitle: string
|
||||||
|
gameId: string
|
||||||
|
/** 'creamlinux' | 'smokeapi' – whichever is currently installed */
|
||||||
|
unlocker: 'creamlinux' | 'smokeapi'
|
||||||
|
onClose: () => void
|
||||||
|
onSubmit: (worked: boolean) => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
const UNLOCKER_LABELS: Record<string, string> = {
|
||||||
|
creamlinux: 'CreamLinux',
|
||||||
|
smokeapi: 'SmokeAPI',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-game rating dialog. Submits exactly one report for the installed unlocker.
|
||||||
|
*/
|
||||||
|
const RatingDialog: React.FC<RatingDialogProps> = ({
|
||||||
|
visible,
|
||||||
|
gameTitle,
|
||||||
|
gameId,
|
||||||
|
unlocker,
|
||||||
|
onClose,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [submitted, setSubmitted] = useState(false)
|
||||||
|
// Which vote the user has already cast for this game+unlocker, if any
|
||||||
|
const [previousVote, setPreviousVote] = useState<boolean | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!visible) return
|
||||||
|
|
||||||
|
// Reset submit state each time the dialog opens
|
||||||
|
setSubmitted(false)
|
||||||
|
|
||||||
|
// Load the local reports to see if this game+unlocker has already been started
|
||||||
|
invoke<LocalReport[]>('get_local_reports')
|
||||||
|
.then((reports) => {
|
||||||
|
const existing = reports.find(
|
||||||
|
(r) => r.game_id === gameId && r.unlocker === unlocker
|
||||||
|
)
|
||||||
|
setPreviousVote(existing ? existing.worked : null)
|
||||||
|
})
|
||||||
|
.catch(() => setPreviousVote(null))
|
||||||
|
}, [visible, gameId, unlocker])
|
||||||
|
|
||||||
|
const handleSubmit = async (worked: boolean) => {
|
||||||
|
if (submitting || submitted) return
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
await onSubmit(worked)
|
||||||
|
setSubmitted(true)
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSubmitted(false)
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = UNLOCKER_LABELS[unlocker] ?? unlocker
|
||||||
|
|
||||||
|
// A button is "already chosen" if it matches the previous vote
|
||||||
|
const workedAlreadyChosen = previousVote === true
|
||||||
|
const brokenAlreadyChosen = previousVote === false
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog visible={visible} onClose={handleClose} size="small">
|
||||||
|
<DialogHeader onClose={handleClose} hideCloseButton={true}>
|
||||||
|
<h3>Submit rating</h3>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<DialogBody>
|
||||||
|
{submitted ? (
|
||||||
|
<div className="rating-submitted">
|
||||||
|
<p>Thanks for your report! Your vote helps other users.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rating-content">
|
||||||
|
<p>
|
||||||
|
You have <strong>{label}</strong> installed for{' '}
|
||||||
|
<strong>{gameTitle}</strong>. Did it work?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{previousVote !== null && (
|
||||||
|
<p className="rating-subtext">
|
||||||
|
You previously voted <strong>{previousVote ? 'worked' : "didn't work"}</strong>.
|
||||||
|
You can change your vote below.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{previousVote === null && (
|
||||||
|
<p className="rating-subtext">
|
||||||
|
Your rating is anonymous and helps other users know if{' '}
|
||||||
|
{label} works with this game.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="rating-buttons">
|
||||||
|
<Button
|
||||||
|
variant="success"
|
||||||
|
className={`rating-btn rating-btn--worked${workedAlreadyChosen ? ' rating-btn--active' : ''}`}
|
||||||
|
onClick={() => handleSubmit(true)}
|
||||||
|
disabled={submitting || workedAlreadyChosen}
|
||||||
|
title={workedAlreadyChosen ? 'Already voted' : undefined}
|
||||||
|
leftIcon={<Icon name="Check" variant="solid" size="sm" />}
|
||||||
|
>
|
||||||
|
It worked
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
className={`rating-btn rating-btn--broken${brokenAlreadyChosen ? ' rating-btn--active' : ''}`}
|
||||||
|
onClick={() => handleSubmit(false)}
|
||||||
|
disabled={submitting || brokenAlreadyChosen}
|
||||||
|
title={brokenAlreadyChosen ? 'Already voted' : undefined}
|
||||||
|
leftIcon={<Icon name="Close" variant="solid" size="sm" />}
|
||||||
|
>
|
||||||
|
Didn't work
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rating-notice">
|
||||||
|
<Icon name={info} variant="solid" size="md" />
|
||||||
|
<span>Only the result for {label} will be submitted.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogBody>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="secondary" onClick={handleClose}>
|
||||||
|
{submitted ? 'Close' : 'Cancel'}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</DialogFooter>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RatingDialog
|
||||||
Reference in New Issue
Block a user