diff --git a/src/components/Search.js b/src/components/Search.js
new file mode 100644
index 0000000..df00aa1
--- /dev/null
+++ b/src/components/Search.js
@@ -0,0 +1,39 @@
+import React from "react";
+import { Box, Input } from "theme-ui";
+
+import SearchIcon from "../icons/SearchIcon";
+
+function Search(props) {
+ return (
+
+
+
+
+
+
+ );
+}
+
+export default Search;
diff --git a/src/components/map/MapTile.js b/src/components/map/MapTile.js
index 25fb480..9437ba5 100644
--- a/src/components/map/MapTile.js
+++ b/src/components/map/MapTile.js
@@ -6,7 +6,15 @@ import EditMapIcon from "../../icons/EditMapIcon";
import useDataSource from "../../helpers/useDataSource";
import { mapSources as defaultMapSources, unknownSource } from "../../maps";
-function MapTile({ map, isSelected, onMapSelect, onMapEdit, onDone, large }) {
+function MapTile({
+ map,
+ isSelected,
+ onMapSelect,
+ onMapEdit,
+ onDone,
+ large,
+ canEdit,
+}) {
const [isMapTileMenuOpen, setIsTileMenuOpen] = useState(false);
const isDefault = map.type === "default";
const mapSource = useDataSource(
@@ -39,9 +47,7 @@ function MapTile({ map, isSelected, onMapSelect, onMapEdit, onDone, large }) {
onClick={(e) => {
e.stopPropagation();
setIsTileMenuOpen(false);
- if (!isSelected) {
- onMapSelect(map);
- }
+ onMapSelect(map);
}}
onDoubleClick={(e) => {
if (!isMapTileMenuOpen) {
@@ -97,8 +103,7 @@ function MapTile({ map, isSelected, onMapSelect, onMapEdit, onDone, large }) {
borderRadius: "4px",
}}
/>
- {/* Show expand button only if both reset and remove is available */}
- {isSelected && (
+ {canEdit && (
0 ||
- selectedMapState.mapDrawActions.length > 0 ||
- selectedMapState.fogDrawActions.length > 0);
+ let hasMapState = false;
+ if (selectedMapStates.length > 0) {
+ for (let state of selectedMapStates) {
+ if (
+ Object.values(state.tokens).length > 0 ||
+ state.mapDrawActions.length > 0 ||
+ state.fogDrawActions.length > 0
+ ) {
+ hasMapState = true;
+ break;
+ }
+ }
+ }
return (
+ onMapSelect()}
+ >
+
+
+ onSelectModeChange(selectMode === "single" ? "multiple" : "single")
+ }
+ aria-label={
+ selectMode === "single" ? "Select Multiple" : "Select Single"
+ }
+ title={selectMode === "single" ? "Select Multiple" : "Select Single"}
+ ml={1}
+ >
+ {selectMode === "single" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
onMapSelect(null)}
+ onClick={() => onMapSelect()}
>
-
-
-
-
-
{maps.map((map) => {
- const isSelected = selectedMap && map.id === selectedMap.id;
+ const isSelected = selectedMaps.includes(map);
return (
);
})}
@@ -118,7 +134,7 @@ function MapTiles({
)}
- {selectedMap && (
+ {selectedMaps.length > 0 && (
onMapSelect(null)}
+ onClick={() => onMapSelect()}
/>
onMapReset(selectedMap.id)}
+ onClick={() => onMapsReset()}
disabled={!hasMapState}
>
@@ -146,7 +162,7 @@ function MapTiles({
onMapRemove(selectedMap.id)}
+ onClick={() => onMapsRemove()}
>
diff --git a/src/icons/SearchIcon.js b/src/icons/SearchIcon.js
new file mode 100644
index 0000000..095987b
--- /dev/null
+++ b/src/icons/SearchIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SearchIcon() {
+ return (
+
+ );
+}
+
+export default SearchIcon;
diff --git a/src/icons/SelectDiceIcon.js b/src/icons/SelectDiceIcon.js
index c9433ef..c51c1a2 100644
--- a/src/icons/SelectDiceIcon.js
+++ b/src/icons/SelectDiceIcon.js
@@ -1,6 +1,6 @@
import React from "react";
-function SelectMapIcon() {
+function SelectDiceIcon() {
return (
+ );
+}
+
+export default SelectMultipleIcon;
diff --git a/src/icons/SelectSingleIcon.js b/src/icons/SelectSingleIcon.js
new file mode 100644
index 0000000..47b0bd6
--- /dev/null
+++ b/src/icons/SelectSingleIcon.js
@@ -0,0 +1,18 @@
+import React from "react";
+
+function SelectSingleIcon() {
+ return (
+
+ );
+}
+
+export default SelectSingleIcon;
diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js
index 9169a91..eca6825 100644
--- a/src/modals/SelectMapModal.js
+++ b/src/modals/SelectMapModal.js
@@ -54,11 +54,13 @@ function SelectMapModal({
const [imageLoading, setImageLoading] = useState(false);
// The map selected in the modal
- const [selectedMapId, setSelectedMapId] = useState(null);
+ const [selectedMapIds, setSelectedMapIds] = useState([]);
- const selectedMap = ownedMaps.find((map) => map.id === selectedMapId);
- const selectedMapState = mapStates.find(
- (state) => state.mapId === selectedMapId
+ const selectedMaps = ownedMaps.filter((map) =>
+ selectedMapIds.includes(map.id)
+ );
+ const selectedMapStates = mapStates.filter((state) =>
+ selectedMapIds.includes(state.mapId)
);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
@@ -171,31 +173,83 @@ function SelectMapModal({
async function handleMapAdd(map) {
await addMap(map);
- setSelectedMapId(map.id);
+ setSelectedMapIds([map.id]);
}
- async function handleMapRemove(id) {
- await removeMap(id);
- setSelectedMapId(null);
+ async function handleMapsRemove() {
+ for (let id of selectedMapIds) {
+ await removeMap(id);
+ }
+ setSelectedMapIds([]);
// Removed the map from the map screen if needed
- if (currentMap && currentMap.id === selectedMapId) {
+ if (currentMap && selectedMapIds.includes(currentMap.id)) {
onMapChange(null, null);
}
}
+ // Either single, multiple or range
+ const [selectMode, setSelectMode] = useState("single");
+
async function handleMapSelect(map) {
if (map) {
- setSelectedMapId(map.id);
+ switch (selectMode) {
+ case "single":
+ setSelectedMapIds([map.id]);
+ break;
+ case "multiple":
+ setSelectedMapIds((prev) => {
+ if (prev.includes(map.id)) {
+ return prev.filter((id) => id !== map.id);
+ } else {
+ return [...prev, map.id];
+ }
+ });
+ break;
+ case "range":
+ // Add all items inbetween the previous selected map and the current selected
+ if (selectedMapIds.length > 0) {
+ const mapIndex = ownedMaps.findIndex((m) => m.id === map.id);
+ const lastIndex = ownedMaps.findIndex(
+ (m) => m.id === selectedMapIds[selectedMapIds.length - 1]
+ );
+ let idsToAdd = [];
+ let idsToRemove = [];
+ const direction = mapIndex > lastIndex ? 1 : -1;
+ for (
+ let i = mapIndex;
+ direction > 0 ? i >= lastIndex : i <= lastIndex;
+ i += direction
+ ) {
+ const mapId = ownedMaps[i].id;
+ if (selectedMapIds.includes(mapId)) {
+ idsToRemove.push(mapId);
+ } else {
+ idsToAdd.push(mapId);
+ }
+ }
+ setSelectedMapIds((prev) => {
+ let ids = [...prev, ...idsToAdd];
+ return ids.filter((id) => idsToRemove.includes(id));
+ });
+ } else {
+ setSelectedMapIds([map.id]);
+ }
+ break;
+ default:
+ setSelectedMapIds([]);
+ }
} else {
- setSelectedMapId(null);
+ setSelectedMapIds([]);
}
}
- async function handleMapReset(id) {
- const newState = await resetMap(id);
- // Reset the state of the current map if needed
- if (currentMap && currentMap.id === selectedMapId) {
- onMapStateChange(newState);
+ async function handleMapsReset() {
+ for (let id of selectedMapIds) {
+ const newState = await resetMap(id);
+ // Reset the state of the current map if needed
+ if (currentMap && currentMap.id === id) {
+ onMapStateChange(newState);
+ }
}
}
@@ -207,11 +261,11 @@ function SelectMapModal({
if (imageLoading) {
return;
}
- if (selectedMapId) {
+ if (selectedMapIds.length === 1) {
// Update last used for cache invalidation
const lastUsed = Date.now();
- await updateMap(selectedMapId, { lastUsed });
- onMapChange({ ...selectedMap, lastUsed }, selectedMapState);
+ await updateMap(selectedMapIds[0], { lastUsed });
+ onMapChange({ ...selectedMaps[0], lastUsed }, selectedMapStates[0]);
} else {
onMapChange(null, null);
}
@@ -245,16 +299,18 @@ function SelectMapModal({
maps={ownedMaps}
onMapAdd={openImageDialog}
onMapEdit={() => setIsEditModalOpen(true)}
- onMapReset={handleMapReset}
- onMapRemove={handleMapRemove}
- selectedMap={selectedMap}
- selectedMapState={selectedMapState}
+ onMapsReset={handleMapsReset}
+ onMapsRemove={handleMapsRemove}
+ selectedMaps={selectedMaps}
+ selectedMapStates={selectedMapStates}
onMapSelect={handleMapSelect}
onDone={handleDone}
+ selectMode={selectMode}
+ onSelectModeChange={setSelectMode}
/>