diff --git a/src/components/dialogs/OptInDialog.tsx b/src/components/dialogs/OptInDialog.tsx new file mode 100644 index 0000000..58490e3 --- /dev/null +++ b/src/components/dialogs/OptInDialog.tsx @@ -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 = ({ visible, onAccept, onDecline }) => { + return ( + {}} size="medium"> + {}} hideCloseButton={true}> +

Help improve CreamLinux

+
+ + +
+ +

+ CreamLinux can collect anonymous compatibility reports to help users know which + games work with CreamLinux and SmokeAPI before they install them. +

+ +
+

What we collect

+
    +
  • + A one-way anonymous hash derived from your machine ID, Steam + install path, and a locally-stored random salt. This cannot be reversed + to identify you, and even we cannot link it to your machine. +
  • +
  • The Steam App ID of the game you rated.
  • +
  • Which unlocker you used (CreamLinux or SmokeAPI).
  • +
  • Whether it worked or not.
  • +
+ +

What we do not collect

+
    +
  • Your username, IP address, or any personally identifiable information.
  • +
+
+ +
+ + + 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. + +
+
+
+ + + + + + + +
+ ) +} + +export default OptInDialog \ No newline at end of file diff --git a/src/components/dialogs/RatingDialog.tsx b/src/components/dialogs/RatingDialog.tsx new file mode 100644 index 0000000..c5774cc --- /dev/null +++ b/src/components/dialogs/RatingDialog.tsx @@ -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 +} + +const UNLOCKER_LABELS: Record = { + creamlinux: 'CreamLinux', + smokeapi: 'SmokeAPI', +} + +/** + * Per-game rating dialog. Submits exactly one report for the installed unlocker. + */ +const RatingDialog: React.FC = ({ + 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(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('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 ( + + +

Submit rating

+
+ + + {submitted ? ( +
+

Thanks for your report! Your vote helps other users.

+
+ ) : ( +
+

+ You have {label} installed for{' '} + {gameTitle}. Did it work? +

+ + {previousVote !== null && ( +

+ You previously voted {previousVote ? 'worked' : "didn't work"}. + You can change your vote below. +

+ )} + + {previousVote === null && ( +

+ Your rating is anonymous and helps other users know if{' '} + {label} works with this game. +

+ )} + +
+ + + +
+ +
+ + Only the result for {label} will be submitted. +
+
+ )} +
+ + + + + + +
+ ) +} + +export default RatingDialog \ No newline at end of file