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": {
"@babylonjs/core": "^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",
"@mitchemmc/dexie-export-import": "^1.0.1",
"@msgpack/msgpack": "^2.4.1",

View File

@ -1,10 +1,9 @@
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";
function Tile({
src,
title,
isSelected,
onSelect,
@ -13,6 +12,8 @@ function Tile({
canEdit,
badges,
editTitle,
columns,
children,
}) {
return (
<Flex
@ -34,21 +35,21 @@ function Tile({
onSelect();
}}
onDoubleClick={onDoubleClick}
aria-label={title}
>
{src && (
<UIImage
sx={{
width: "100%",
height: "100%",
objectFit: "contain",
position: "absolute",
top: 0,
left: 0,
}}
src={src}
alt={title}
/>
)}
<Grid
columns={columns}
sx={{
width: "100%",
height: "100%",
position: "absolute",
top: 0,
left: 0,
gridGap: 0,
}}
>
{children}
</Grid>
<Flex
sx={{
position: "absolute",
@ -116,7 +117,6 @@ function Tile({
}
Tile.defaultProps = {
src: "",
title: "",
isSelected: false,
onSelect: () => {},
@ -126,6 +126,7 @@ Tile.defaultProps = {
canEdit: false,
badges: [],
editTitle: "Edit",
columns: "1fr",
};
export default Tile;

View File

@ -1,16 +1,18 @@
import React from "react";
import { Image } from "theme-ui";
import Tile from "../Tile";
function DiceTile({ dice, isSelected, onDiceSelect, onDone }) {
return (
<Tile
src={dice.preview}
title={dice.name}
isSelected={isSelected}
onSelect={() => onDiceSelect(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 }) {
setDragId();
if (active && over && active.id !== over.id) {
const oldIndex = groups.indexOf(active.id);
const newIndex = groups.indexOf(over.id);
const oldIndex = groups.findIndex((group) => group.id === active.id);
const newIndex = groups.findIndex((group) => group.id === over.id);
onGroupChange(arrayMove(groups, oldIndex, newIndex));
}
}
@ -53,7 +53,7 @@ function SortableTiles({ groups, onGroupChange, renderTile, children }) {
<DragOverlay dropAnimation={null}>
{dragId && (
<animated.div style={dragBounce}>
{renderTile(dragId)}
{renderTile(groups.find((group) => group.id === dragId))}
</animated.div>
)}
</DragOverlay>,

View File

@ -1,9 +1,7 @@
import React from "react";
import Tile from "../Tile";
import { useDataURL } from "../../contexts/AssetsContext";
import { mapSources as defaultMapSources } from "../../maps";
import MapTileImage from "./MapTileImage";
function MapTile({
map,
@ -14,16 +12,8 @@ function MapTile({
canEdit,
badges,
}) {
const mapURL = useDataURL(
map,
defaultMapSources,
undefined,
map.type === "file"
);
return (
<Tile
src={mapURL}
title={map.name}
isSelected={isSelected}
onSelect={() => onMapSelect(map)}
@ -32,7 +22,9 @@ function MapTile({
canEdit={canEdit}
badges={badges}
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 MapTile from "./MapTile";
import MapTileGroup from "./MapTileGroup";
import Link from "../Link";
import FilterBar from "../FilterBar";
@ -53,24 +54,35 @@ function MapTiles({
(map) => map.type === "default"
);
function mapToTile(mapId) {
const map = maps.find((map) => map.id === mapId);
const isSelected = selectedMaps.includes(map);
return (
<MapTile
key={map.id}
map={map}
isSelected={isSelected}
onMapSelect={onMapSelect}
onMapEdit={onMapEdit}
onDone={onDone}
size={layout.tileSize}
canEdit={
isSelected && selectMode === "single" && selectedMaps.length === 1
}
badges={[`${map.grid.size.x}x${map.grid.size.y}`]}
/>
);
function groupToMapTile(group) {
if (group.type === "item") {
const map = maps.find((map) => map.id === group.id);
const isSelected = selectedMaps.includes(map);
return (
<MapTile
key={map.id}
map={map}
isSelected={isSelected}
onMapSelect={onMapSelect}
onMapEdit={onMapEdit}
onDone={onDone}
canEdit={
isSelected && selectMode === "single" && selectedMaps.length === 1
}
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;
@ -79,7 +91,7 @@ function MapTiles({
<SortableTiles
groups={groups}
onGroupChange={onMapsGroup}
renderTile={mapToTile}
renderTile={groupToMapTile}
>
<Box sx={{ position: "relative" }}>
<FilterBar
@ -110,9 +122,9 @@ function MapTiles({
columns={layout.gridTemplate}
onClick={() => onMapSelect()}
>
{groups.map((mapId) => (
<Sortable id={mapId} key={mapId}>
{mapToTile(mapId)}
{groups.map((group) => (
<Sortable id={group.id} key={group.id}>
{groupToMapTile(group)}
</Sortable>
))}
</Grid>

View File

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

View File

@ -1,10 +1,7 @@
import React from "react";
import Tile from "../Tile";
import { useDataURL } from "../../contexts/AssetsContext";
import { tokenSources as defaultTokenSources } from "../../tokens";
import TokenTileImage from "./TokenTileImage";
function TokenTile({
token,
@ -14,16 +11,8 @@ function TokenTile({
canEdit,
badges,
}) {
const tokenURL = useDataURL(
token,
defaultTokenSources,
undefined,
token.type === "file"
);
return (
<Tile
src={tokenURL}
title={token.name}
isSelected={isSelected}
onSelect={() => onTokenSelect(token)}
@ -31,7 +20,9 @@ function TokenTile({
canEdit={canEdit}
badges={badges}
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 TokenTile from "./TokenTile";
import TokenTileGroup from "./TokenTileGroup";
import Link from "../Link";
import FilterBar from "../FilterBar";
@ -40,25 +41,37 @@ function TokenTiles({
);
let allTokensVisible = selectedTokens.every((token) => !token.hideInSidebar);
function tokenToTile(tokenId) {
const token = tokens.find((token) => token.id === tokenId);
const isSelected = selectedTokens.includes(token);
return (
<TokenTile
key={token.id}
token={token}
isSelected={isSelected}
onTokenSelect={onTokenSelect}
onTokenEdit={onTokenEdit}
canEdit={
isSelected &&
token.type !== "default" &&
selectMode === "single" &&
selectedTokens.length === 1
}
badges={[`${token.defaultSize}x`]}
/>
);
function groupToTokenTile(group) {
if (group.type === "item") {
const token = tokens.find((token) => token.id === group.id);
const isSelected = selectedTokens.includes(token);
return (
<TokenTile
key={token.id}
token={token}
isSelected={isSelected}
onTokenSelect={onTokenSelect}
onTokenEdit={onTokenEdit}
canEdit={
isSelected &&
token.type !== "default" &&
selectMode === "single" &&
selectedTokens.length === 1
}
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;
@ -82,7 +95,7 @@ function TokenTiles({
<SortableTiles
groups={groups}
onGroupChange={onTokensGroup}
renderTile={tokenToTile}
renderTile={groupToTokenTile}
>
<Box sx={{ position: "relative" }}>
<FilterBar
@ -112,9 +125,9 @@ function TokenTiles({
columns={layout.gridTemplate}
onClick={() => onTokenSelect()}
>
{groups.map((tokenId) => (
<Sortable id={tokenId} key={tokenId}>
{tokenToTile(tokenId)}
{groups.map((group) => (
<Sortable id={group.id} key={group.id}>
{groupToTokenTile(group)}
</Sortable>
))}
</Grid>

View File

@ -47,7 +47,7 @@ export function MapDataProvider({ children }) {
const storedStates = await database.table("states").toArray();
setMapStates(storedStates);
const group = await database.table("groups").get("maps");
const storedGroups = group.data;
const storedGroups = group.items;
setMapGroups(storedGroups);
setMapsLoading(false);
}
@ -82,9 +82,9 @@ export function MapDataProvider({ children }) {
await database.table("maps").add(map);
await database.table("states").add(state);
const group = await database.table("groups").get("maps");
await database
.table("groups")
.update("maps", { data: [map.id, ...group.data] });
await database.table("groups").update("maps", {
items: [{ id: map.id, type: "item" }, ...group.items],
});
},
[database]
);
@ -146,7 +146,7 @@ export function MapDataProvider({ children }) {
async (groups) => {
// Update group state immediately to avoid animation delay
setMapGroups(groups);
await database.table("groups").update("maps", { data: groups });
await database.table("groups").update("maps", { items: groups });
},
[database]
);
@ -207,7 +207,7 @@ export function MapDataProvider({ children }) {
}
if (change.table === "groups") {
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);
const group = await database.table("groups").get("tokens");
const storedGroups = group.data;
const storedGroups = group.items;
setTokenGroups(storedGroups);
setTokensLoading(false);
}
@ -54,9 +54,9 @@ export function TokenDataProvider({ children }) {
async (token) => {
await database.table("tokens").add(token);
const group = await database.table("groups").get("tokens");
await database
.table("groups")
.update("tokens", { data: [token.id, ...group.data] });
await database.table("groups").update("tokens", {
items: [{ id: token.id, type: "item" }, ...group.items],
});
},
[database]
);
@ -99,7 +99,7 @@ export function TokenDataProvider({ children }) {
async (groups) => {
// Update group state immediately to avoid animation delay
setTokenGroups(groups);
await database.table("groups").update("tokens", { data: groups });
await database.table("groups").update("tokens", { items: groups });
},
[database]
);
@ -139,7 +139,7 @@ export function TokenDataProvider({ children }) {
}
if (change.table === "groups") {
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);
db.table("tokens").bulkAdd(tokens);
db.table("groups").bulkAdd([
{ id: "maps", data: maps.map((map) => map.id) },
{ id: "tokens", data: tokens.map((token) => token.id) },
{ id: "maps", items: maps.map((map) => ({ id: map.id, type: "item" })) },
{
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);
});
},
// v1.9.0 -
// v1.9.0 - Add new group table
33(v) {
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());
maps = maps.sort((a, b) => b.created - a.created);
const mapIds = maps.map((map) => map.id);
tx.table("groups").add({ id: "maps", data: mapIds });
const mapGroupItems = groupItems(maps);
tx.table("groups").add({ id: "maps", items: mapGroupItems });
let tokens = await Dexie.waitFor(tx.table("tokens").toArray());
tokens = tokens.sort((a, b) => b.created - a.created);
const tokenIds = tokens.map((token) => token.id);
tx.table("groups").add({ id: "tokens", data: tokenIds });
const tokenGroupItems = groupItems(tokens);
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

View File

@ -1803,7 +1803,7 @@
dependencies:
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"
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-3.0.2.tgz#e46ae11ef667aa5c31fddab21cf36ffd80d3ce5b"
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"
dependencies:
debug "^4.0.1"
err-code "^2.0.3"
get-browser-rtc "^1.0.0"
queue-microtask "^1.1.0"
randombytes "^2.0.3"