Add sub groups to group table

This commit is contained in:
Mitchell McCaffrey 2021-05-14 18:02:50 +10:00
parent ab800150ec
commit 05968c1964
18 changed files with 272 additions and 116 deletions

View File

@ -5,7 +5,7 @@
"dependencies": { "dependencies": {
"@babylonjs/core": "^4.2.0", "@babylonjs/core": "^4.2.0",
"@babylonjs/loaders": "^4.2.0", "@babylonjs/loaders": "^4.2.0",
"@dnd-kit/core": "^3.0.2", "@dnd-kit/core": "3.0.2",
"@dnd-kit/sortable": "^3.0.1", "@dnd-kit/sortable": "^3.0.1",
"@mitchemmc/dexie-export-import": "^1.0.1", "@mitchemmc/dexie-export-import": "^1.0.1",
"@msgpack/msgpack": "^2.4.1", "@msgpack/msgpack": "^2.4.1",

View File

@ -1,10 +1,9 @@
import React from "react"; import React from "react";
import { Flex, Image as UIImage, IconButton, Box, Text, Badge } from "theme-ui"; import { Flex, IconButton, Box, Text, Badge, Grid } from "theme-ui";
import EditTileIcon from "../icons/EditTileIcon"; import EditTileIcon from "../icons/EditTileIcon";
function Tile({ function Tile({
src,
title, title,
isSelected, isSelected,
onSelect, onSelect,
@ -13,6 +12,8 @@ function Tile({
canEdit, canEdit,
badges, badges,
editTitle, editTitle,
columns,
children,
}) { }) {
return ( return (
<Flex <Flex
@ -34,21 +35,21 @@ function Tile({
onSelect(); onSelect();
}} }}
onDoubleClick={onDoubleClick} onDoubleClick={onDoubleClick}
aria-label={title}
> >
{src && ( <Grid
<UIImage columns={columns}
sx={{ sx={{
width: "100%", width: "100%",
height: "100%", height: "100%",
objectFit: "contain",
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 0, left: 0,
gridGap: 0,
}} }}
src={src} >
alt={title} {children}
/> </Grid>
)}
<Flex <Flex
sx={{ sx={{
position: "absolute", position: "absolute",
@ -116,7 +117,6 @@ function Tile({
} }
Tile.defaultProps = { Tile.defaultProps = {
src: "",
title: "", title: "",
isSelected: false, isSelected: false,
onSelect: () => {}, onSelect: () => {},
@ -126,6 +126,7 @@ Tile.defaultProps = {
canEdit: false, canEdit: false,
badges: [], badges: [],
editTitle: "Edit", editTitle: "Edit",
columns: "1fr",
}; };
export default Tile; export default Tile;

View File

@ -1,16 +1,18 @@
import React from "react"; import React from "react";
import { Image } from "theme-ui";
import Tile from "../Tile"; import Tile from "../Tile";
function DiceTile({ dice, isSelected, onDiceSelect, onDone }) { function DiceTile({ dice, isSelected, onDiceSelect, onDone }) {
return ( return (
<Tile <Tile
src={dice.preview}
title={dice.name} title={dice.name}
isSelected={isSelected} isSelected={isSelected}
onSelect={() => onDiceSelect(dice)} onSelect={() => onDiceSelect(dice)}
onDoubleClick={() => onDone(dice)} onDoubleClick={() => onDone(dice)}
/> >
<Image src={dice.preview}></Image>
</Tile>
); );
} }

View File

@ -30,8 +30,8 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) {
function handleDragEnd({ active, over }) { function handleDragEnd({ active, over }) {
setDragId(); setDragId();
if (active && over && active.id !== over.id) { if (active && over && active.id !== over.id) {
const oldIndex = groups.indexOf(active.id); const oldIndex = groups.findIndex((group) => group.id === active.id);
const newIndex = groups.indexOf(over.id); const newIndex = groups.findIndex((group) => group.id === over.id);
onGroupChange(arrayMove(groups, oldIndex, newIndex)); onGroupChange(arrayMove(groups, oldIndex, newIndex));
} }
} }
@ -53,7 +53,7 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) {
<DragOverlay dropAnimation={null}> <DragOverlay dropAnimation={null}>
{dragId && ( {dragId && (
<animated.div style={dragBounce}> <animated.div style={dragBounce}>
{renderTile(dragId)} {renderTile(groups.find((group) => group.id === dragId))}
</animated.div> </animated.div>
)} )}
</DragOverlay>, </DragOverlay>,

View File

@ -1,9 +1,7 @@
import React from "react"; import React from "react";
import Tile from "../Tile"; import Tile from "../Tile";
import MapTileImage from "./MapTileImage";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
function MapTile({ function MapTile({
map, map,
@ -14,16 +12,8 @@ function MapTile({
canEdit, canEdit,
badges, badges,
}) { }) {
const mapURL = useDataURL(
map,
defaultMapSources,
undefined,
map.type === "file"
);
return ( return (
<Tile <Tile
src={mapURL}
title={map.name} title={map.name}
isSelected={isSelected} isSelected={isSelected}
onSelect={() => onMapSelect(map)} onSelect={() => onMapSelect(map)}
@ -32,7 +22,9 @@ function MapTile({
canEdit={canEdit} canEdit={canEdit}
badges={badges} badges={badges}
editTitle="Edit Map" editTitle="Edit Map"
/> >
<MapTileImage map={map} />
</Tile>
); );
} }

View File

@ -0,0 +1,33 @@
import React from "react";
import Tile from "../Tile";
import MapTileImage from "./MapTileImage";
function MapTileGroup({
group,
maps,
isSelected,
onGroupSelect,
onOpen,
canOpen,
}) {
return (
<Tile
title={group.name}
isSelected={isSelected}
// onSelect={() => onGroupSelect(group)}
// onDoubleClick={() => canOpen && onOpen()}
columns="1fr 1fr"
>
{maps.slice(0, 4).map((map) => (
<MapTileImage
sx={{ padding: 1, borderRadius: "8px" }}
map={map}
key={map.id}
/>
))}
</Tile>
);
}
export default MapTileGroup;

View File

@ -0,0 +1,18 @@
import React from "react";
import { Image } from "theme-ui";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
function MapTileImage({ map, sx }) {
const mapURL = useDataURL(
map,
defaultMapSources,
undefined,
map.type === "file"
);
return <Image sx={sx} src={mapURL}></Image>;
}
export default MapTileImage;

View File

@ -6,6 +6,7 @@ import RemoveMapIcon from "../../icons/RemoveMapIcon";
import ResetMapIcon from "../../icons/ResetMapIcon"; import ResetMapIcon from "../../icons/ResetMapIcon";
import MapTile from "./MapTile"; import MapTile from "./MapTile";
import MapTileGroup from "./MapTileGroup";
import Link from "../Link"; import Link from "../Link";
import FilterBar from "../FilterBar"; import FilterBar from "../FilterBar";
@ -53,8 +54,9 @@ function MapTiles({
(map) => map.type === "default" (map) => map.type === "default"
); );
function mapToTile(mapId) { function groupToMapTile(group) {
const map = maps.find((map) => map.id === mapId); if (group.type === "item") {
const map = maps.find((map) => map.id === group.id);
const isSelected = selectedMaps.includes(map); const isSelected = selectedMaps.includes(map);
return ( return (
<MapTile <MapTile
@ -64,13 +66,23 @@ function MapTiles({
onMapSelect={onMapSelect} onMapSelect={onMapSelect}
onMapEdit={onMapEdit} onMapEdit={onMapEdit}
onDone={onDone} onDone={onDone}
size={layout.tileSize}
canEdit={ canEdit={
isSelected && selectMode === "single" && selectedMaps.length === 1 isSelected && selectMode === "single" && selectedMaps.length === 1
} }
badges={[`${map.grid.size.x}x${map.grid.size.y}`]} badges={[`${map.grid.size.x}x${map.grid.size.y}`]}
/> />
); );
} else {
return (
<MapTileGroup
key={group.id}
group={group}
maps={group.items.map((item) =>
maps.find((map) => map.id === item.id)
)}
/>
);
}
} }
const multipleSelected = selectedMaps.length > 1; const multipleSelected = selectedMaps.length > 1;
@ -79,7 +91,7 @@ function MapTiles({
<SortableTiles <SortableTiles
groups={groups} groups={groups}
onGroupChange={onMapsGroup} onGroupChange={onMapsGroup}
renderTile={mapToTile} renderTile={groupToMapTile}
> >
<Box sx={{ position: "relative" }}> <Box sx={{ position: "relative" }}>
<FilterBar <FilterBar
@ -110,9 +122,9 @@ function MapTiles({
columns={layout.gridTemplate} columns={layout.gridTemplate}
onClick={() => onMapSelect()} onClick={() => onMapSelect()}
> >
{groups.map((mapId) => ( {groups.map((group) => (
<Sortable id={mapId} key={mapId}> <Sortable id={group.id} key={group.id}>
{mapToTile(mapId)} {groupToMapTile(group)}
</Sortable> </Sortable>
))} ))}
</Grid> </Grid>

View File

@ -29,6 +29,7 @@ function TokenBar({ onMapTokenStateCreate }) {
function handleDragEnd({ active }) { function handleDragEnd({ active }) {
setDragId(null); setDragId(null);
const token = tokensById[active.id]; const token = tokensById[active.id];
console.log("Drag", active);
if (token) { if (token) {
// TODO: Get drag position // TODO: Get drag position
const tokenState = createTokenState(token, { x: 0, y: 0 }, userId); const tokenState = createTokenState(token, { x: 0, y: 0 }, userId);
@ -36,8 +37,9 @@ function TokenBar({ onMapTokenStateCreate }) {
} }
} }
const tokens = tokenGroups const tokens = tokenGroups
.map((tokenId) => tokensById[tokenId]) .map((group) => tokensById[group.id])
.filter((token) => !token.hideInSidebar) // TODO: Add group support
.filter((token) => token && !token.hideInSidebar)
.map((token) => ( .map((token) => (
<Draggable id={token.id} key={token.id}> <Draggable id={token.id} key={token.id}>
<ListToken token={token} /> <ListToken token={token} />

View File

@ -1,10 +1,7 @@
import React from "react"; import React from "react";
import Tile from "../Tile"; import Tile from "../Tile";
import TokenTileImage from "./TokenTileImage";
import { useDataURL } from "../../contexts/AssetsContext";
import { tokenSources as defaultTokenSources } from "../../tokens";
function TokenTile({ function TokenTile({
token, token,
@ -14,16 +11,8 @@ function TokenTile({
canEdit, canEdit,
badges, badges,
}) { }) {
const tokenURL = useDataURL(
token,
defaultTokenSources,
undefined,
token.type === "file"
);
return ( return (
<Tile <Tile
src={tokenURL}
title={token.name} title={token.name}
isSelected={isSelected} isSelected={isSelected}
onSelect={() => onTokenSelect(token)} onSelect={() => onTokenSelect(token)}
@ -31,7 +20,9 @@ function TokenTile({
canEdit={canEdit} canEdit={canEdit}
badges={badges} badges={badges}
editTitle="Edit Token" editTitle="Edit Token"
/> >
<TokenTileImage token={token} />
</Tile>
); );
} }

View File

@ -0,0 +1,33 @@
import React from "react";
import Tile from "../Tile";
import TokenTileImage from "./TokenTileImage";
function TokenTileGroup({
group,
tokens,
isSelected,
onGroupSelect,
onOpen,
canOpen,
}) {
return (
<Tile
title={group.name}
isSelected={isSelected}
// onSelect={() => onGroupSelect(group)}
// onDoubleClick={() => canOpen && onOpen()}
columns="1fr 1fr"
>
{tokens.slice(0, 4).map((token) => (
<TokenTileImage
sx={{ padding: 1, borderRadius: "8px" }}
token={token}
key={token.id}
/>
))}
</Tile>
);
}
export default TokenTileGroup;

View File

@ -0,0 +1,19 @@
import React from "react";
import { Image } from "theme-ui";
import { useDataURL } from "../../contexts/AssetsContext";
import { tokenSources as defaultTokenSources } from "../../tokens";
function TokenTileImage({ token, sx }) {
const tokenURL = useDataURL(
token,
defaultTokenSources,
undefined,
token.type === "file"
);
return <Image sx={sx} src={tokenURL}></Image>;
}
export default TokenTileImage;

View File

@ -7,6 +7,7 @@ import TokenHideIcon from "../../icons/TokenHideIcon";
import TokenShowIcon from "../../icons/TokenShowIcon"; import TokenShowIcon from "../../icons/TokenShowIcon";
import TokenTile from "./TokenTile"; import TokenTile from "./TokenTile";
import TokenTileGroup from "./TokenTileGroup";
import Link from "../Link"; import Link from "../Link";
import FilterBar from "../FilterBar"; import FilterBar from "../FilterBar";
@ -40,8 +41,9 @@ function TokenTiles({
); );
let allTokensVisible = selectedTokens.every((token) => !token.hideInSidebar); let allTokensVisible = selectedTokens.every((token) => !token.hideInSidebar);
function tokenToTile(tokenId) { function groupToTokenTile(group) {
const token = tokens.find((token) => token.id === tokenId); if (group.type === "item") {
const token = tokens.find((token) => token.id === group.id);
const isSelected = selectedTokens.includes(token); const isSelected = selectedTokens.includes(token);
return ( return (
<TokenTile <TokenTile
@ -59,6 +61,17 @@ function TokenTiles({
badges={[`${token.defaultSize}x`]} badges={[`${token.defaultSize}x`]}
/> />
); );
} else {
return (
<TokenTileGroup
key={group.id}
group={group}
tokens={group.items.map((item) =>
tokens.find((token) => token.id === item.id)
)}
/>
);
}
} }
const multipleSelected = selectedTokens.length > 1; const multipleSelected = selectedTokens.length > 1;
@ -82,7 +95,7 @@ function TokenTiles({
<SortableTiles <SortableTiles
groups={groups} groups={groups}
onGroupChange={onTokensGroup} onGroupChange={onTokensGroup}
renderTile={tokenToTile} renderTile={groupToTokenTile}
> >
<Box sx={{ position: "relative" }}> <Box sx={{ position: "relative" }}>
<FilterBar <FilterBar
@ -112,9 +125,9 @@ function TokenTiles({
columns={layout.gridTemplate} columns={layout.gridTemplate}
onClick={() => onTokenSelect()} onClick={() => onTokenSelect()}
> >
{groups.map((tokenId) => ( {groups.map((group) => (
<Sortable id={tokenId} key={tokenId}> <Sortable id={group.id} key={group.id}>
{tokenToTile(tokenId)} {groupToTokenTile(group)}
</Sortable> </Sortable>
))} ))}
</Grid> </Grid>

View File

@ -47,7 +47,7 @@ export function MapDataProvider({ children }) {
const storedStates = await database.table("states").toArray(); const storedStates = await database.table("states").toArray();
setMapStates(storedStates); setMapStates(storedStates);
const group = await database.table("groups").get("maps"); const group = await database.table("groups").get("maps");
const storedGroups = group.data; const storedGroups = group.items;
setMapGroups(storedGroups); setMapGroups(storedGroups);
setMapsLoading(false); setMapsLoading(false);
} }
@ -82,9 +82,9 @@ export function MapDataProvider({ children }) {
await database.table("maps").add(map); await database.table("maps").add(map);
await database.table("states").add(state); await database.table("states").add(state);
const group = await database.table("groups").get("maps"); const group = await database.table("groups").get("maps");
await database await database.table("groups").update("maps", {
.table("groups") items: [{ id: map.id, type: "item" }, ...group.items],
.update("maps", { data: [map.id, ...group.data] }); });
}, },
[database] [database]
); );
@ -146,7 +146,7 @@ export function MapDataProvider({ children }) {
async (groups) => { async (groups) => {
// Update group state immediately to avoid animation delay // Update group state immediately to avoid animation delay
setMapGroups(groups); setMapGroups(groups);
await database.table("groups").update("maps", { data: groups }); await database.table("groups").update("maps", { items: groups });
}, },
[database] [database]
); );
@ -207,7 +207,7 @@ export function MapDataProvider({ children }) {
} }
if (change.table === "groups") { if (change.table === "groups") {
if (change.type === 2 && change.key === "maps") { if (change.type === 2 && change.key === "maps") {
setMapGroups(change.obj.data); setMapGroups(change.obj.items);
} }
} }
} }

View File

@ -33,7 +33,7 @@ export function TokenDataProvider({ children }) {
} }
setTokens(storedTokens); setTokens(storedTokens);
const group = await database.table("groups").get("tokens"); const group = await database.table("groups").get("tokens");
const storedGroups = group.data; const storedGroups = group.items;
setTokenGroups(storedGroups); setTokenGroups(storedGroups);
setTokensLoading(false); setTokensLoading(false);
} }
@ -54,9 +54,9 @@ export function TokenDataProvider({ children }) {
async (token) => { async (token) => {
await database.table("tokens").add(token); await database.table("tokens").add(token);
const group = await database.table("groups").get("tokens"); const group = await database.table("groups").get("tokens");
await database await database.table("groups").update("tokens", {
.table("groups") items: [{ id: token.id, type: "item" }, ...group.items],
.update("tokens", { data: [token.id, ...group.data] }); });
}, },
[database] [database]
); );
@ -99,7 +99,7 @@ export function TokenDataProvider({ children }) {
async (groups) => { async (groups) => {
// Update group state immediately to avoid animation delay // Update group state immediately to avoid animation delay
setTokenGroups(groups); setTokenGroups(groups);
await database.table("groups").update("tokens", { data: groups }); await database.table("groups").update("tokens", { items: groups });
}, },
[database] [database]
); );
@ -139,7 +139,7 @@ export function TokenDataProvider({ children }) {
} }
if (change.table === "groups") { if (change.table === "groups") {
if (change.type === 2 && change.key === "tokens") { if (change.type === 2 && change.key === "tokens") {
setTokenGroups(change.obj.data); setTokenGroups(change.obj.items);
} }
} }
} }

View File

@ -21,8 +21,11 @@ function populate(db) {
const tokens = getDefaultTokens(userId); const tokens = getDefaultTokens(userId);
db.table("tokens").bulkAdd(tokens); db.table("tokens").bulkAdd(tokens);
db.table("groups").bulkAdd([ db.table("groups").bulkAdd([
{ id: "maps", data: maps.map((map) => map.id) }, { id: "maps", items: maps.map((map) => ({ id: map.id, type: "item" })) },
{ id: "tokens", data: tokens.map((token) => token.id) }, {
id: "tokens",
items: tokens.map((token) => ({ id: token.id, type: "item" })),
},
]); ]);
}); });
} }

View File

@ -639,23 +639,59 @@ export const versions = {
tx.table("tokens").bulkAdd(tokens); tx.table("tokens").bulkAdd(tokens);
}); });
}, },
// v1.9.0 - // v1.9.0 - Add new group table
33(v) { 33(v) {
v.stores({ groups: "id" }).upgrade(async (tx) => { v.stores({ groups: "id" }).upgrade(async (tx) => {
function groupItems(items) {
let groups = [];
let subGroups = {};
for (let item of items) {
if (!item.group) {
groups.push({ id: item.id, type: "item" });
} else if (item.group in subGroups) {
subGroups[item.group].items.push({ id: item.id, type: "item" });
} else {
subGroups[item.group] = {
id: uuid(),
type: "group",
name: item.group,
items: [{ id: item.id, type: "item" }],
};
}
}
groups.push(...Object.values(subGroups));
return groups;
}
let maps = await Dexie.waitFor(tx.table("maps").toArray()); let maps = await Dexie.waitFor(tx.table("maps").toArray());
maps = maps.sort((a, b) => b.created - a.created); maps = maps.sort((a, b) => b.created - a.created);
const mapIds = maps.map((map) => map.id); const mapGroupItems = groupItems(maps);
tx.table("groups").add({ id: "maps", data: mapIds }); tx.table("groups").add({ id: "maps", items: mapGroupItems });
let tokens = await Dexie.waitFor(tx.table("tokens").toArray()); let tokens = await Dexie.waitFor(tx.table("tokens").toArray());
tokens = tokens.sort((a, b) => b.created - a.created); tokens = tokens.sort((a, b) => b.created - a.created);
const tokenIds = tokens.map((token) => token.id); const tokenGroupItems = groupItems(tokens);
tx.table("groups").add({ id: "tokens", data: tokenIds }); tx.table("groups").add({ id: "tokens", items: tokenGroupItems });
});
},
// v1.9.0 - Remove map and token group in respective tables
34(v) {
v.stores({}).upgrade((tx) => {
tx.table("maps")
.toCollection()
.modify((map) => {
delete map.group;
});
tx.table("tokens")
.toCollection()
.modify((token) => {
delete token.group;
});
}); });
}, },
}; };
export const latestVersion = 33; export const latestVersion = 34;
/** /**
* Load versions onto a database up to a specific version number * Load versions onto a database up to a specific version number

View File

@ -1803,7 +1803,7 @@
dependencies: dependencies:
tslib "^2.0.0" tslib "^2.0.0"
"@dnd-kit/core@^3.0.0", "@dnd-kit/core@^3.0.2": "@dnd-kit/core@3.0.2", "@dnd-kit/core@^3.0.0":
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.0.2.tgz#e46ae11ef667aa5c31fddab21cf36ffd80d3ce5b" resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.0.2.tgz#e46ae11ef667aa5c31fddab21cf36ffd80d3ce5b"
integrity sha512-L+rGnDYBb4BfYKDylzIBeODRIlJ+YVvo2iL9pVXsh317Nq7c9irCvi3XK8JnWD5QBw/3WZ5FmbPmTE91EKwKeA== integrity sha512-L+rGnDYBb4BfYKDylzIBeODRIlJ+YVvo2iL9pVXsh317Nq7c9irCvi3XK8JnWD5QBw/3WZ5FmbPmTE91EKwKeA==
@ -11894,6 +11894,7 @@ simple-peer@feross/simple-peer#694/head:
resolved "https://codeload.github.com/feross/simple-peer/tar.gz/0d08d07b83ff3b8c60401688d80642d24dfeffe2" resolved "https://codeload.github.com/feross/simple-peer/tar.gz/0d08d07b83ff3b8c60401688d80642d24dfeffe2"
dependencies: dependencies:
debug "^4.0.1" debug "^4.0.1"
err-code "^2.0.3"
get-browser-rtc "^1.0.0" get-browser-rtc "^1.0.0"
queue-microtask "^1.1.0" queue-microtask "^1.1.0"
randombytes "^2.0.3" randombytes "^2.0.3"