Skip to content

Commit

Permalink
feat(rolls): Added ability to copy rolls to the clipboard
Browse files Browse the repository at this point in the history
  • Loading branch information
scottbenton committed Nov 28, 2023
1 parent dad0418 commit 9823f3d
Show file tree
Hide file tree
Showing 20 changed files with 775 additions and 495 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### New Features

- Added a new beta tests dialog in the user settings menu - this will allow me to test changes before fully releasing them
- Added the ability to copy rolls to the clipboard

### Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Box, Typography } from "@mui/material";
import { RollSnackbar } from "components/shared/RollSnackbar";
import { useEffect } from "react";
import { useStore } from "stores/store";
import { Roll } from "types/DieRolls.type";
import { RollDisplay } from "../RollDisplay";
import { NormalRollActions } from "../RollDisplay/NormalRollActions";

export interface GameLogEntryProps {
log: Roll;
Expand Down Expand Up @@ -67,11 +68,11 @@ export function GameLogEntry(props: GameLogEntryProps) {
alignItems={isYourEntry ? "flex-end" : "flex-start"}
>
<Typography>{rollerName}</Typography>
<RollSnackbar roll={log} isExpanded />
{/* <Card variant={"outlined"} sx={(theme) => ({ p: 2 })}>
<Typography>{log.rollLabel}</Typography>
<Typography>{log.result}</Typography>
</Card> */}
<RollDisplay
roll={log}
isExpanded
actions={<NormalRollActions roll={log} />}
/>
<Typography color={"textSecondary"} variant={"caption"}>
{getLogTimeString(log.timestamp)}
</Typography>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import {
Box,
ButtonBase,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
} from "@mui/material";
import { useId, useRef, useState } from "react";
import { ROLL_TYPE, Roll } from "types/DieRolls.type";
import MoreIcon from "@mui/icons-material/MoreHoriz";
import CopyIcon from "@mui/icons-material/CopyAll";
import { getRollResultLabel } from "./getRollResultLabel";
import { useSnackbar } from "providers/SnackbarProvider";
import { convertRollToClipboard } from "./clipboardFormatter";

export interface NormalRollActionsProps {
roll: Roll;
}

async function pasteRich(rich: string, plain: string) {
if (typeof ClipboardItem !== "undefined") {
// Shiny new Clipboard API, not fully supported in Firefox.
// https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API#browser_compatibility
const html = new Blob([rich], { type: "text/html" });
const text = new Blob([plain], { type: "text/plain" });
const data = new ClipboardItem({ "text/html": html, "text/plain": text });
await navigator.clipboard.write([data]);
} else {
// Fallback using the deprecated `document.execCommand`.
// https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand#browser_compatibility
const cb = (e: ClipboardEvent) => {
e.clipboardData?.setData("text/html", rich);
e.clipboardData?.setData("text/plain", plain);
e.preventDefault();
};
document.addEventListener("copy", cb);
document.execCommand("copy");
document.removeEventListener("copy", cb);
}
}

export function NormalRollActions(props: NormalRollActionsProps) {
const { roll } = props;
const id = useId();
const blockQuoteId = `roll-action-copy-${id}`;

const [isMenuOpen, setIsMenuOpen] = useState(false);
const menuParent = useRef<HTMLButtonElement>(null);

const { error, success } = useSnackbar();

const handleCopyRoll = () => {
const clipboardData = convertRollToClipboard(roll);

if (clipboardData) {
pasteRich(clipboardData.rich, clipboardData.plain)
.then(() => {
success("Copied roll to clipboard.");
})
.catch(() => {
error("Failed to copy roll");
});
} else {
error("Copying this roll type is not supported");
}
};

let rollTitle = roll.rollLabel;
let actionContents: string | undefined = undefined;
let challengeContents: string | undefined = undefined;
let result: string | undefined = undefined;

if (roll.type === ROLL_TYPE.STAT) {
if (roll.moveName) {
rollTitle = `${roll.moveName} (${roll.rollLabel})`;
}

const rollTotal = roll.action + (roll.modifier ?? 0) + (roll.adds ?? 0);
actionContents = roll.action + "";
if (roll.modifier || roll.adds) {
actionContents +=
(roll.modifier ? ` + ${roll.modifier}` : "") +
(roll.adds ? ` + ${roll.adds}` : "") +
` = ${rollTotal > 10 ? "10 (Max)" : rollTotal}`;
}

challengeContents = roll.challenge1 + ", " + roll.challenge2;

result = getRollResultLabel(roll.result).toLocaleUpperCase();
}

return (
<>
<ButtonBase
aria-label={"Roll Menu"}
ref={menuParent}
onClick={(evt) => {
evt.stopPropagation();
setIsMenuOpen(true);
}}
>
<MoreIcon />
</ButtonBase>
{isMenuOpen && (
<Menu
sx={{
zIndex: 10001,
}}
open={isMenuOpen}
onClose={() => setIsMenuOpen(false)}
anchorEl={menuParent.current}
>
<MenuItem
onClick={(evt) => {
evt.stopPropagation();
handleCopyRoll();
setIsMenuOpen(false);
}}
>
<ListItemIcon>
<CopyIcon />
</ListItemIcon>
<ListItemText>Copy Roll Result</ListItemText>
</MenuItem>
</Menu>
)}
{/* Copy to clipboard component */}
<Box component={"blockquote"} display={"none"} id={blockQuoteId}>
<p>{rollTitle}</p>
{actionContents && (
<p>
<em>Action:</em> {actionContents}
</p>
)}
{challengeContents && (
<p>
<em>Challenge:</em> {challengeContents}
</p>
)}
{result && (
<p>
<b>{result}</b>
</p>
)}
</Box>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Box } from "@mui/material";
import { PropsWithChildren } from "react";

export function RollContainer(props: PropsWithChildren) {
const { children } = props;
return (
<Box display={"flex"} alignItems={"center"}>
{children}
</Box>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Box, Card, CardActionArea } from "@mui/material";
import { ROLL_TYPE, Roll } from "types/DieRolls.type";
import { RollTitle } from "./RollTitle";
import { RollValues } from "./RollValues";
import { RollResult } from "./RollResult";
import { getRollResultLabel } from "./getRollResultLabel";
import { RollContainer } from "./RollContainer";
import { ReactNode } from "react";

export interface RollDisplayProps {
roll: Roll;
onClick?: () => void;
isExpanded: boolean;
actions?: ReactNode;
}

export function RollDisplay(props: RollDisplayProps) {
const { roll, onClick, isExpanded, actions } = props;

return (
<Card
sx={(theme) => ({
backgroundColor: theme.palette.darkGrey.dark,
color: theme.palette.darkGrey.contrastText,
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
})}
>
<Box
component={onClick ? CardActionArea : "div"}
onClick={onClick ? onClick : undefined}
px={2}
py={1}
>
{roll.type === ROLL_TYPE.STAT && (
<>
<RollTitle
title={roll.moveName ? roll.moveName : roll.rollLabel}
overline={roll.moveName ? roll.rollLabel : undefined}
isExpanded={isExpanded}
actions={actions}
/>
<RollContainer>
<RollValues
d6Result={{
action: roll.action,
modifier: roll.modifier,
adds: roll.adds,
rollTotal:
roll.action + (roll.modifier ?? 0) + (roll.adds ?? 0),
}}
d10Results={[roll.challenge1, roll.challenge2]}
isExpanded={isExpanded}
/>
<RollResult
result={getRollResultLabel(roll.result)}
extras={[
...(roll.challenge1 === roll.challenge2 ? ["Doubles"] : []),
...(roll.action === 1 ? ["Natural 1"] : []),
]}
/>
</RollContainer>
</>
)}
{roll.type === ROLL_TYPE.TRACK_PROGRESS && (
<>
<RollTitle
title={roll.rollLabel}
isExpanded={isExpanded}
actions={actions}
/>
<RollContainer>
<RollValues
fixedResult={{
title: "Progress",
value: roll.trackProgress,
}}
d10Results={[roll.challenge1, roll.challenge2]}
isExpanded={isExpanded}
/>
<RollResult
result={getRollResultLabel(roll.result)}
extras={[
...(roll.challenge1 === roll.challenge2 ? ["Doubles"] : []),
]}
/>
</RollContainer>
</>
)}
{roll.type === ROLL_TYPE.ORACLE_TABLE && (
<>
<RollTitle
overline={roll.oracleCategoryName}
title={roll.rollLabel}
isExpanded={isExpanded}
actions={actions}
/>
<RollContainer>
<RollValues d10Results={roll.roll} isExpanded={isExpanded} />
<RollResult markdown={roll.result} />
</RollContainer>
</>
)}
{roll.type === ROLL_TYPE.CLOCK_PROGRESSION && (
<>
<RollTitle
overline={roll.oracleTitle}
title={roll.rollLabel}
isExpanded={isExpanded}
actions={actions}
/>
<RollContainer>
<RollValues d10Results={roll.roll} isExpanded={isExpanded} />
<RollResult markdown={roll.result} />
</RollContainer>
</>
)}
</Box>
</Card>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box, Typography } from "@mui/material";
import { MarkdownRenderer } from "components/shared/MarkdownRenderer";

export interface RollResultProps {
result?: string;
markdown?: string;
extras?: string[];
}

export function RollResult(props: RollResultProps) {
const { result, markdown, extras } = props;

return (
<Box
display={"flex"}
flexDirection={"column"}
alignItems={"flex-start"}
justifyContent={"center"}
>
{result && (
<Typography
color={"white"}
variant={"h5"}
component={"p"}
fontFamily={(theme) => theme.fontFamilyTitle}
>
{result}
</Typography>
)}
{markdown && (
<MarkdownRenderer markdown={markdown} inheritColor disableLinks />
)}
{Array.isArray(extras) &&
extras.map((extra) => (
<Typography
key={extra}
color={(theme) => theme.palette.grey[200]}
variant={"caption"}
component={"p"}
fontFamily={(theme) => theme.fontFamilyTitle}
>
{extra}
</Typography>
))}
</Box>
);
}
Loading

0 comments on commit 9823f3d

Please sign in to comment.