mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-04-30 03:52: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