Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/editor/EditorContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useSettings } from "../settings";
import ZoomControls from "../workbench/ZoomControls";
import Editor from "./Editor";
import NonMainFileNotice from "./NonMainFileNotice";
import EditorToolbar from "./EditorToolbar";

interface EditorContainerProps {
filename: string;
Expand Down
31 changes: 31 additions & 0 deletions src/editor/EditorToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { HStack } from "@chakra-ui/react";
import React from "react";
import DownloadButton from "../workbench/DownloadButton";
import ProjectNameEditable from "../workbench/ProjectNameEditable";
import ZoomControls from "../workbench/ZoomControls";

interface EditorToolbarProps {}

const EditorToolbar = ({}: EditorToolbarProps) => {
return (
<HStack
justifyContent="space-between"
pt={1}
pb={1}
pl={2}
pr={2}
backgroundColor="var(--code-background)"
borderBottom="1px solid #dddddd" // Hack to match the CodeMirror gutter
>
<HStack>
<ProjectNameEditable />
</HStack>
<HStack>
<DownloadButton mode="button" variant="outline" />
<ZoomControls />
</HStack>
</HStack>
);
};

export default EditorToolbar;
42 changes: 42 additions & 0 deletions src/workbench/CollapsableButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Button,
HTMLChakraProps,
IconButton,
ThemingProps,
} from "@chakra-ui/react";
import React from "react";

export interface CollapsableButtonProps
extends HTMLChakraProps<"button">,
ThemingProps<"Button"> {
mode: "icon" | "button";
text: string;
icon: React.ReactElement;
/**
* Width used only in button mode.
*/
buttonWidth?: number | string;
}

/**
* Button that can be a regular or icon button.
*
* We'd like to do this at a lower-level so we can animate a transition.
*/
const CollapsableButton = ({
mode,
text,
icon,
buttonWidth,
...props
}: CollapsableButtonProps) => {
return mode === "icon" ? (
<IconButton icon={icon} aria-label={text} {...props} />
) : (
<Button leftIcon={icon} minWidth={buttonWidth} {...props}>
{text}
</Button>
);
};

export default CollapsableButton;
71 changes: 17 additions & 54 deletions src/workbench/DeviceConnection.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, HStack, Switch, Text, VStack } from "@chakra-ui/react";
import { Button, Flex, HStack, Switch, Text, VStack } from "@chakra-ui/react";
import React, { useCallback, useState } from "react";
import { RiFlashlightFill } from "react-icons/ri";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
Expand All @@ -10,6 +10,7 @@ import useActionFeedback, {
} from "../common/use-action-feedback";
import { BoardId } from "../device/board-id";
import Separate from "../common/Separate";
import FlashButton from "./FlashButton";

class HexGenerationError extends Error {}

Expand All @@ -23,7 +24,6 @@ const DeviceConnection = () => {
const connectionStatus = useConnectionStatus();
const connected = connectionStatus === ConnectionStatus.CONNECTED;
const supported = connectionStatus !== ConnectionStatus.NOT_SUPPORTED;
const [progress, setProgress] = useState<undefined | number>(undefined);
const actionFeedback = useActionFeedback();
const device = useDevice();
const fs = useFileSystem();
Expand All @@ -38,67 +38,30 @@ const DeviceConnection = () => {
}
}
}, [device, connected]);

const handleFlash = useCallback(async () => {
const dataSource = async (boardId: BoardId) => {
try {
return await fs.toHexForFlash(boardId);
} catch (e) {
throw new HexGenerationError(e.message);
}
};

try {
await device.flash(dataSource, { partial: true, progress: setProgress });
} catch (e) {
if (e instanceof HexGenerationError) {
actionFeedback.expectedError({
title: "Failed to build the hex file",
description: e.message,
});
} else {
handleWebUSBError(actionFeedback, e);
}
}
}, [fs, device, actionFeedback]);

const buttonWidth = "10rem";
return (
<VStack
backgroundColor="var(--sidebar)"
padding={5}
spacing={2}
align="flex-start"
>
<HStack>
{supported ? (
<HStack as="label" spacing={3}>
<Switch
size="lg"
isChecked={connected}
onChange={handleToggleConnected}
/>
<HStack as="label" spacing={3} width="14rem">
<Switch isChecked={connected} onChange={handleToggleConnected} />
<Text as="span" size="lg" fontWeight="semibold">
{connected ? "micro:bit connected" : "micro:bit disconnected"}
</Text>
</HStack>
) : null}

<HStack justifyContent="space-between" width="100%">
{connected && (
<Button
leftIcon={<RiFlashlightFill />}
size="lg"
width="100%"
disabled={!fs || !connected || typeof progress !== "undefined"}
onClick={handleFlash}
>
{typeof progress === "undefined"
? "Flash"
: `Flashing… (${(progress * 100).toFixed(0)}%)`}
</Button>
<HStack>
{supported && (
<FlashButton
mode={connected ? "button" : "icon"}
buttonWidth={buttonWidth}
/>
)}
<DownloadButton size="lg" width="100%" />
<DownloadButton
mode={connected ? "icon" : "button"}
buttonWidth={buttonWidth}
/>
</HStack>
</VStack>
</HStack>
);
};

Expand Down
23 changes: 18 additions & 5 deletions src/workbench/DownloadButton.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import {
BoxProps,
Button,
ButtonProps,
Icon,
IconButton,
} from "@chakra-ui/react";
import React, { useCallback } from "react";
import { RiDownload2Line } from "react-icons/ri";
import useActionFeedback from "../common/use-action-feedback";
import { useFileSystem } from "../fs/fs-hooks";
import CollapsableButton, { CollapsableButtonProps } from "./CollapsableButton";

interface DownloadButtonProps
extends Omit<CollapsableButtonProps, "onClick" | "text" | "icon"> {}

/**
* Download HEX button.
Expand All @@ -12,7 +22,7 @@ import { useFileSystem } from "../fs/fs-hooks";
*
* Otherwise it's a more minor action.
*/
const DownloadButton = (props: ButtonProps) => {
const DownloadButton = (props: DownloadButtonProps) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const handleDownload = useCallback(async () => {
Expand All @@ -33,9 +43,12 @@ const DownloadButton = (props: ButtonProps) => {
}, []);

return (
<Button leftIcon={<RiDownload2Line />} onClick={handleDownload} {...props}>
Download
</Button>
<CollapsableButton
{...props}
icon={<RiDownload2Line />}
onClick={handleDownload}
text="Download"
/>
);
};

Expand Down
26 changes: 17 additions & 9 deletions src/workbench/Files.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
Button,
Center,
HStack,
IconButton,
List,
ListItem,
Text,
VStack,
} from "@chakra-ui/react";
import React, { useCallback } from "react";
Expand All @@ -13,6 +15,7 @@ import { useFileSystem, useFileSystemState } from "../fs/fs-hooks";
import { saveAs } from "file-saver";
import useActionFeedback from "../common/use-action-feedback";
import ProjectNameEditable from "./ProjectNameEditable";
import OpenButton from "./OpenButton";

interface FilesProps {
onSelectedFileChanged: (name: string) => void;
Expand All @@ -27,15 +30,20 @@ const Files = ({ onSelectedFileChanged }: FilesProps) => {
return null;
}
return (
<VStack alignItems="stretch" padding={2} spacing={5}>
<ProjectNameEditable />
<List>
{fs.files.map((f) => (
<ListItem key={f.name}>
<FileRow value={f} onClick={() => onSelectedFileChanged(f.name)} />
</ListItem>
))}
</List>
<VStack alignItems="stretch" pl={2} pr={2} spacing={6}>
<VStack alignItems="stretch">
<List>
{fs.files.map((f) => (
<ListItem key={f.name}>
<FileRow
value={f}
onClick={() => onSelectedFileChanged(f.name)}
/>
</ListItem>
))}
</List>
</VStack>
<OpenButton text="Open a project" mt={2} variant="outline" />
</VStack>
);
};
Expand Down
80 changes: 80 additions & 0 deletions src/workbench/FlashButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import React, { useCallback, useState } from "react";
import { RiFlashlightFill } from "react-icons/ri";
import Separate from "../common/Separate";
import useActionFeedback, {
ActionFeedback,
} from "../common/use-action-feedback";
import { ConnectionStatus, WebUSBError } from "../device";
import { BoardId } from "../device/board-id";
import { useConnectionStatus, useDevice } from "../device/device-hooks";
import { useFileSystem } from "../fs/fs-hooks";
import CollapsableButton, { CollapsableButtonProps } from "./CollapsableButton";

class HexGenerationError extends Error {}

/**
* Flash button.
*/
const FlashButton = (
props: Omit<CollapsableButtonProps, "onClick" | "text" | "icon">
) => {
const fs = useFileSystem();
const actionFeedback = useActionFeedback();
const device = useDevice();
const status = useConnectionStatus();
const connected = status === ConnectionStatus.CONNECTED;
const [progress, setProgress] = useState<number | undefined>();

const handleFlash = useCallback(async () => {
const dataSource = async (boardId: BoardId) => {
try {
return await fs.toHexForFlash(boardId);
} catch (e) {
throw new HexGenerationError(e.message);
}
};

try {
await device.flash(dataSource, { partial: true, progress: setProgress });
} catch (e) {
if (e instanceof HexGenerationError) {
actionFeedback.expectedError({
title: "Failed to build the hex file",
description: e.message,
});
} else {
handleWebUSBError(actionFeedback, e);
}
}
}, [fs, device, actionFeedback]);
const text =
typeof progress === "undefined"
? "Flash"
: `Flashing… (${(progress * 100).toFixed(0)}%)`;
return (
<CollapsableButton
{...props}
disabled={!fs || !connected || typeof progress !== "undefined"}
icon={<RiFlashlightFill />}
onClick={handleFlash}
text={text}
/>
);
};

const handleWebUSBError = (actionFeedback: ActionFeedback, e: any) => {
if (e instanceof WebUSBError) {
actionFeedback.expectedError({
title: e.title,
description: (
<Separate separator={(k) => <br key={k} />}>
{[e.message, e.description].filter(Boolean)}
</Separate>
),
});
} else {
actionFeedback.unexpectedError(e);
}
};

export default FlashButton;
Loading