feat: set version dialog

This commit is contained in:
trafficlunar 2024-12-28 21:07:12 +00:00
parent cf83d2fc7c
commit a7e5dfd3d6
10 changed files with 908 additions and 27 deletions

View file

@ -0,0 +1,80 @@
import { useEffect, useState } from "react";
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { numberToVersion, versionToNumber } from "@/utils/version";
const versions = [
"1.21.4",
"1.21",
"1.20",
"1.18",
"1.19",
"1.17",
"1.16",
"1.15",
"1.14",
"1.13",
"1.12",
"1.11",
"1.10",
"1.9",
"1.8",
"1.7.2",
"1.6.1",
];
interface Props {
version: number;
setVersion: React.Dispatch<React.SetStateAction<number>>;
}
function VersionCombobox({ version, setVersion }: Props) {
const [comboboxOpen, setComboboxOpen] = useState(false);
const [comboboxValue, setComboboxValue] = useState(numberToVersion(version));
useEffect(() => {
setVersion(versionToNumber(comboboxValue));
}, [comboboxValue]);
return (
<Popover open={comboboxOpen} onOpenChange={setComboboxOpen}>
<PopoverTrigger asChild>
<Button variant="outline" role="combobox" aria-expanded={comboboxOpen} className="w-[200px] justify-between">
{comboboxValue ? versions.find((version) => version === comboboxValue) : "Select version..."}
<ChevronsUpDownIcon className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0 pointer-events-auto">
<Command>
<CommandInput placeholder="Search version..." />
<CommandList>
<CommandEmpty>No version found.</CommandEmpty>
<CommandGroup>
{versions.map((version) => (
<CommandItem
key={version}
value={version}
onSelect={(currentValue) => {
setComboboxValue(currentValue === comboboxValue ? versions[0] : currentValue);
setComboboxOpen(false);
}}
>
{version}
<CheckIcon className={cn("ml-auto", comboboxValue === version ? "opacity-100" : "opacity-0")} />
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}
export default VersionCombobox;

View file

@ -0,0 +1,40 @@
import { useContext, useState } from "react";
import { CanvasContext } from "@/context/Canvas";
import { DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import VersionCombobox from "../VersionCombobox";
function SetVersion({ close }: DialogProps) {
const { version: contextVersion, setVersion: setContextVersion } = useContext(CanvasContext);
const [version, setVersion] = useState(contextVersion);
const onSubmit = () => {
setContextVersion(version);
close();
};
return (
<DialogContent>
<DialogHeader>
<DialogTitle>Set Version</DialogTitle>
<DialogDescription>Updates your block palette to the specified Minecraft version</DialogDescription>
</DialogHeader>
<VersionCombobox version={version} setVersion={setVersion} />
<DialogFooter>
<Button variant="outline" onClick={close}>
Cancel
</Button>
<Button type="submit" onClick={onSubmit}>
Set
</Button>
</DialogFooter>
</DialogContent>
);
}
export default SetVersion;

View file

@ -34,6 +34,7 @@ function FileMenu() {
</MenubarSub>
<MenubarSeparator />
<MenubarItem onClick={() => openDialog("SetVersion")}>Set Version</MenubarItem>
<MenubarItem onClick={() => openDialog("ClearBlocks")}>Clear All Blocks</MenubarItem>
</MenubarContent>
</MenubarMenu>

View file

@ -0,0 +1,151 @@
import * as React from "react"
import { type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-md bg-white text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50",
className
)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
const CommandDialog = ({ children, ...props }: DialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 dark:[&_[cmdk-group-heading]]:text-zinc-400">
{children}
</Command>
</DialogContent>
</Dialog>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-zinc-500 disabled:cursor-not-allowed disabled:opacity-50 dark:placeholder:text-zinc-400",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty
ref={ref}
className="py-6 text-center text-sm"
{...props}
/>
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-zinc-950 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-zinc-500 dark:text-zinc-50 dark:[&_[cmdk-group-heading]]:text-zinc-400",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-zinc-200 dark:bg-zinc-800", className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected='true']:bg-zinc-100 data-[selected=true]:text-zinc-900 data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 dark:data-[selected='true']:bg-zinc-800 dark:data-[selected=true]:text-zinc-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-zinc-500 dark:text-zinc-400",
className
)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View file

@ -0,0 +1,29 @@
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-zinc-200 bg-white p-4 text-zinc-950 shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-zinc-800 dark:bg-zinc-950 dark:text-zinc-50",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent }

View file

@ -25,7 +25,7 @@ export const CanvasProvider = ({ children }: Props) => {
const [blocks, setBlocks] = useState<Block[]>([]);
const [coords, setCoords] = useState<Position>({ x: 0, y: 0 });
const [scale, setScale] = useState(1);
const [version, setVersion] = useState(1200);
const [version, setVersion] = useState(1214);
const canvasSize = useMemo(() => {
let minX = Infinity,

View file

@ -503,13 +503,13 @@
},
"chiseled_nether_bricks": {
"name": "Chiseled Nether Bricks",
"version": 1000,
"version": 1160,
"id": ["chiseled_nether_bricks", 887],
"color": [48, 24, 29, 255]
},
"chiseled_polished_blackstone": {
"name": "Chiseled Polished Blackstone",
"version": 1000,
"version": 1160,
"id": ["chiseled_polished_blackstone", 877],
"color": [57, 53, 61, 255]
},
@ -539,7 +539,7 @@
},
"chiseled_stone_bricks": {
"name": "Chiseled Stone Bricks",
"version": 1000,
"version": 1020,
"id": ["chiseled_stone_bricks", 310],
"color": [122, 121, 122, 255]
},
@ -1505,7 +1505,7 @@
},
"lodestone_side": {
"name": "Lodestone",
"version": 1000,
"version": 1160,
"id": ["lodestone", 869],
"color": [124, 125, 128, 255]
},
@ -1669,7 +1669,7 @@
},
"nether_gold_ore": {
"name": "Nether Gold Ore",
"version": 1000,
"version": 1160,
"id": ["nether_gold_ore", 48],
"color": [124, 74, 47, 255]
},
@ -1857,7 +1857,7 @@
},
"packed_ice": {
"name": "Packed Ice",
"version": 1000,
"version": 1072,
"id": ["packed_ice", 513],
"color": [142, 180, 251, 255]
},
@ -2177,7 +2177,7 @@
},
"red_nether_bricks": {
"name": "Red Nether Bricks",
"version": 1000,
"version": 1100,
"id": ["red_nether_bricks", 630],
"color": [73, 10, 12, 255]
},
@ -2259,7 +2259,7 @@
},
"resin_bricks": {
"name": "Resin Bricks",
"version": 1000,
"version": 1214,
"id": ["resin_bricks", -1],
"color": [207, 94, 29, 255]
},
@ -2271,7 +2271,7 @@
},
"rooted_dirt": {
"name": "Rooted Dirt",
"version": 1000,
"version": 1170,
"id": ["rooted_dirt", 1042],
"color": [147, 107, 81, 255]
},
@ -2357,7 +2357,7 @@
},
"smooth_stone_slab_side": {
"name": "Smooth Stone Slab",
"version": 1000,
"version": 1140,
"id": ["smooth_stone_slab", 568],
"color": [169, 169, 169, 255],
"properties": {
@ -2453,13 +2453,13 @@
},
"stripped_bamboo_block": {
"name": "Block of Stripped Bamboo",
"version": 1130,
"version": 1200,
"id": ["stripped_bamboo_block", 70],
"color": [195, 175, 82, 255]
},
"stripped_bamboo_block_top": {
"name": "Block of Stripped Bamboo (Top)",
"version": 1130,
"version": 1200,
"id": ["stripped_bamboo_block", 70],
"color": [183, 163, 77, 255],
"properties": {
@ -2483,13 +2483,13 @@
},
"stripped_cherry_log": {
"name": "Stripped Cherry Log",
"version": 1130,
"version": 1200,
"id": ["stripped_cherry_log", 65],
"color": [215, 145, 149, 255]
},
"stripped_cherry_log_top": {
"name": "Stripped Cherry Log (Top)",
"version": 1130,
"version": 1200,
"id": ["stripped_cherry_log", 65],
"color": [221, 166, 159, 255],
"properties": {
@ -2498,13 +2498,13 @@
},
"stripped_crimson_stem": {
"name": "Stripped Crimson Stem",
"version": 1130,
"version": 1160,
"id": ["stripped_crimson_stem", 820],
"color": [138, 58, 90, 255]
},
"stripped_crimson_stem_top": {
"name": "Stripped Crimson Stem (Top)",
"version": 1130,
"version": 1160,
"id": ["stripped_crimson_stem", 820],
"color": [123, 57, 83, 255],
"properties": {
@ -2543,13 +2543,13 @@
},
"stripped_mangrove_log": {
"name": "Stripped Mangrove Log",
"version": 1130,
"version": 1190,
"id": ["stripped_mangrove_log", 69],
"color": [120, 55, 48, 255]
},
"stripped_mangrove_log_top": {
"name": "Stripped Mangrove Log (Top)",
"version": 1130,
"version": 1190,
"id": ["stripped_mangrove_log", 69],
"color": [109, 45, 44, 255],
"properties": {
@ -2573,13 +2573,13 @@
},
"stripped_pale_oak_log": {
"name": "Stripped Pale Oak Log",
"version": 1130,
"version": 1214,
"id": ["stripped_pale_oak_log", 67],
"color": [246, 238, 237, 255]
},
"stripped_pale_oak_log_top": {
"name": "Stripped Pale Oak Log (Top)",
"version": 1130,
"version": 1214,
"id": ["stripped_pale_oak_log", 67],
"color": [236, 228, 226, 255],
"properties": {
@ -2603,13 +2603,13 @@
},
"stripped_warped_stem": {
"name": "Stripped Warped Stem",
"version": 1130,
"version": 1160,
"id": ["stripped_warped_stem", 811],
"color": [58, 151, 148, 255]
},
"stripped_warped_stem_top": {
"name": "Stripped Warped Stem (Top)",
"version": 1130,
"version": 1160,
"id": ["stripped_warped_stem", 811],
"color": [53, 130, 126, 255],
"properties": {
@ -2632,7 +2632,7 @@
},
"suspicious_sand_0": {
"name": "Suspicious Sand",
"version": 1000,
"version": 1200,
"id": ["suspicious_sand", -1],
"color": [218, 205, 160, 255],
"fallable": true
@ -2651,7 +2651,7 @@
},
"tinted_glass": {
"name": "Tinted Glass",
"version": 1000,
"version": 1170,
"id": ["tinted_glass", 945],
"color": [45, 39, 48, 131]
},
@ -2663,7 +2663,7 @@
},
"trial_spawner_side_inactive": {
"name": "Trial Spawner",
"version": 1000,
"version": 1210,
"id": ["trial_spawner", -1],
"color": [56, 76, 90, 193],
"creative": true
@ -2683,7 +2683,7 @@
},
"tuff_bricks": {
"name": "Tuff Bricks",
"version": 1000,
"version": 1210,
"id": ["tuff_bricks", 939],
"color": [101, 105, 98, 255]
},

18
src/utils/version.ts Normal file
View file

@ -0,0 +1,18 @@
export function versionToNumber(version: string) {
const parts = version.split(".").map(Number);
return parts[0] * 1000 + (parts[1] || 0) * 10 + (parts[2] || 0);
}
export function numberToVersion(version: number) {
const major = Math.floor(version / 1000);
const minor = version % 1000; // Remainder becomes the minor and patch version
// If minor is a multiple of 10, it's just minor with no patch
if (minor % 10 === 0) {
return `${major}.${Math.floor(minor / 10)}`;
}
// Otherwise, split into minor and patch
const patch = minor % 10;
return `${major}.${Math.floor(minor / 10)}.${patch}`;
}