Add upload and download functionality
Most seems to work as intended, very sparse detection of languages for now, a few questions are open
This commit is contained in:
parent
66f1f9d05a
commit
db1b67b4e9
4 changed files with 272 additions and 106 deletions
161
src/App.tsx
161
src/App.tsx
|
@ -1,4 +1,4 @@
|
|||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -9,6 +9,7 @@ import {
|
|||
Icon,
|
||||
Input,
|
||||
InputGroup,
|
||||
ButtonGroup,
|
||||
InputRightElement,
|
||||
Link,
|
||||
Select,
|
||||
|
@ -19,6 +20,8 @@ import {
|
|||
} from "@chakra-ui/react";
|
||||
import {
|
||||
VscChevronRight,
|
||||
VscCloudDownload,
|
||||
VscCloudUpload,
|
||||
VscFolderOpened,
|
||||
VscGist,
|
||||
VscRepoPull,
|
||||
|
@ -27,7 +30,7 @@ import useStorage from "use-local-storage-state";
|
|||
import Editor from "@monaco-editor/react";
|
||||
import { editor } from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import rustpadRaw from "../rustpad-server/src/rustpad.rs?raw";
|
||||
import languages from "./languages.json";
|
||||
import { getFileExtension, getLanguage, Language, languages } from "./languages";
|
||||
import animals from "./animals.json";
|
||||
import Rustpad, { UserInfo } from "./rustpad";
|
||||
import useHash from "./useHash";
|
||||
|
@ -43,6 +46,20 @@ function getWsUri(id: string) {
|
|||
);
|
||||
}
|
||||
|
||||
function useKeyboardCtrlIntercept(key: string, reaction: (evt: KeyboardEvent) => unknown) {
|
||||
useEffect(() => {
|
||||
const wrappedReaction: typeof reaction = (evt) => {
|
||||
if (!(evt.ctrlKey && evt.key.toLowerCase() === key.toLowerCase())) return;
|
||||
evt.preventDefault();
|
||||
reaction(evt);
|
||||
}
|
||||
const controller = new AbortController();
|
||||
window.addEventListener("keydown", wrappedReaction, {signal: controller.signal});
|
||||
|
||||
return () => controller.abort()
|
||||
}, [key, reaction])
|
||||
}
|
||||
|
||||
function generateName() {
|
||||
return "Anonymous " + animals[Math.floor(Math.random() * animals.length)];
|
||||
}
|
||||
|
@ -51,9 +68,23 @@ function generateHue() {
|
|||
return Math.floor(Math.random() * 360);
|
||||
}
|
||||
|
||||
/**
|
||||
* This appears to still be the best way to download a file while suggesting a filename.
|
||||
*
|
||||
* According to [mdn](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-download)
|
||||
* the download attribute gets ignored on URIs that are not either `same-origin` or use the `blob` or `data` schemes.
|
||||
*/
|
||||
function downloadUri(uri: string, filename: string) {
|
||||
const downloadAnchor = document.createElement("a");
|
||||
console.log(filename);
|
||||
downloadAnchor.download = filename;
|
||||
downloadAnchor.href = uri;
|
||||
downloadAnchor.click();
|
||||
}
|
||||
|
||||
function App() {
|
||||
const toast = useToast();
|
||||
const [language, setLanguage] = useState("plaintext");
|
||||
const [language, setLanguage] = useState<Language>("plaintext");
|
||||
const [connection, setConnection] = useState<
|
||||
"connected" | "disconnected" | "desynchronized"
|
||||
>("disconnected");
|
||||
|
@ -104,7 +135,7 @@ function App() {
|
|||
}
|
||||
}, [connection, name, hue]);
|
||||
|
||||
function handleChangeLanguage(language: string) {
|
||||
function handleChangeLanguage(language: Language) {
|
||||
setLanguage(language);
|
||||
if (rustpad.current?.setLanguage(language)) {
|
||||
toast({
|
||||
|
@ -136,26 +167,50 @@ function App() {
|
|||
});
|
||||
}
|
||||
|
||||
const setText = useCallback((newText: string) => {
|
||||
const model = editor?.getModel();
|
||||
if (!model || !editor) return;
|
||||
model.pushEditOperations(
|
||||
editor.getSelections(),
|
||||
[
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text: newText,
|
||||
}
|
||||
],
|
||||
() => null
|
||||
);
|
||||
editor.setPosition({ column: 0, lineNumber: 0});
|
||||
}, [editor]);
|
||||
|
||||
function handleLoadSample() {
|
||||
if (editor?.getModel()) {
|
||||
const model = editor.getModel()!;
|
||||
model.pushEditOperations(
|
||||
editor.getSelections(),
|
||||
[
|
||||
{
|
||||
range: model.getFullModelRange(),
|
||||
text: rustpadRaw,
|
||||
},
|
||||
],
|
||||
() => null
|
||||
);
|
||||
editor.setPosition({ column: 0, lineNumber: 0 });
|
||||
if (language !== "rust") {
|
||||
handleChangeLanguage("rust");
|
||||
}
|
||||
setText(rustpadRaw);
|
||||
if (language !== "rust") {
|
||||
handleChangeLanguage("rust");
|
||||
}
|
||||
}
|
||||
|
||||
const handleUploadFile = useCallback(async (file: File) => {
|
||||
const text = await file.text();
|
||||
setText(text);
|
||||
const newLanguage = getLanguage(file.name);
|
||||
if (newLanguage !== language) handleChangeLanguage(newLanguage);
|
||||
}, [language]);
|
||||
|
||||
const handleDownloadFile = useCallback(() => {
|
||||
const model = editor?.getModel();
|
||||
if (!model || !editor) return;
|
||||
const text = model.getValue();
|
||||
const languageExtension = getFileExtension(language);
|
||||
const fileName = "unknown." + languageExtension;
|
||||
const file = new File([text], fileName);
|
||||
const url = URL.createObjectURL(file);
|
||||
downloadUri(url, fileName);
|
||||
URL.revokeObjectURL(url);
|
||||
}, [editor, language]);
|
||||
|
||||
useKeyboardCtrlIntercept('s', handleDownloadFile);
|
||||
|
||||
function handleDarkMode() {
|
||||
setDarkMode(!darkMode);
|
||||
}
|
||||
|
@ -167,6 +222,15 @@ function App() {
|
|||
overflow="hidden"
|
||||
bgColor={darkMode ? "#1e1e1e" : "white"}
|
||||
color={darkMode ? "#cbcaca" : "inherit"}
|
||||
onDragOver={(evt) => evt.preventDefault()}
|
||||
onDrop={(evt) => {
|
||||
evt.preventDefault();
|
||||
const dragItems = evt.dataTransfer.items;
|
||||
if (dragItems.length !== 1) return;
|
||||
const file = dragItems[0].getAsFile();
|
||||
if (file === null) return;
|
||||
handleUploadFile(file);
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
flexShrink={0}
|
||||
|
@ -202,7 +266,7 @@ function App() {
|
|||
bgColor={darkMode ? "#3c3c3c" : "white"}
|
||||
borderColor={darkMode ? "#3c3c3c" : "white"}
|
||||
value={language}
|
||||
onChange={(event) => handleChangeLanguage(event.target.value)}
|
||||
onChange={(event) => handleChangeLanguage(event.target.value as Language)}
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<option key={lang} value={lang} style={{ color: "black" }}>
|
||||
|
@ -236,6 +300,61 @@ function App() {
|
|||
</InputRightElement>
|
||||
</InputGroup>
|
||||
|
||||
{/* TODO:
|
||||
* * Test
|
||||
* */}
|
||||
<Heading mt={4} mb={1.5} size="sm">
|
||||
Upload & Download
|
||||
</Heading>
|
||||
<Text>
|
||||
You can also upload with drag&drop and downlod with Ctrl + S
|
||||
</Text>
|
||||
<ButtonGroup
|
||||
size="sm"
|
||||
display="flex"
|
||||
>
|
||||
<label htmlFor="file-upload" style={{flex: "auto"}}>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme={darkMode ? "whiteAlpha" : "blackAlpha"}
|
||||
borderColor={darkMode ? "purple.400" : "purple.600"}
|
||||
color={darkMode ? "purple.400" : "purple.600"}
|
||||
variant="outline"
|
||||
leftIcon={<VscCloudUpload />}
|
||||
mt={1}
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
style={{opacity: 0, maxWidth: 0}}
|
||||
onClick={async (evt) => {
|
||||
const target = evt.currentTarget;
|
||||
const files = target.files ?? [];
|
||||
if (files?.length !== 1) return;
|
||||
handleUploadFile(files[1]);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme={darkMode ? "whiteAlpha" : "blackAlpha"}
|
||||
borderColor={darkMode ? "purple.400" : "purple.600"}
|
||||
color={darkMode ? "purple.400" : "purple.600"}
|
||||
variant="outline"
|
||||
leftIcon={<VscCloudDownload />}
|
||||
mt={1}
|
||||
flex="auto"
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
handleDownloadFile();
|
||||
}}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<Heading mt={4} mb={1.5} size="sm">
|
||||
Active Users
|
||||
</Heading>
|
||||
|
|
|
@ -1,82 +0,0 @@
|
|||
[
|
||||
"abap",
|
||||
"aes",
|
||||
"apex",
|
||||
"azcli",
|
||||
"bat",
|
||||
"bicep",
|
||||
"c",
|
||||
"cameligo",
|
||||
"clojure",
|
||||
"coffeescript",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"csp",
|
||||
"css",
|
||||
"dart",
|
||||
"dockerfile",
|
||||
"ecl",
|
||||
"elixir",
|
||||
"flow9",
|
||||
"fsharp",
|
||||
"go",
|
||||
"graphql",
|
||||
"handlebars",
|
||||
"hcl",
|
||||
"html",
|
||||
"ini",
|
||||
"java",
|
||||
"javascript",
|
||||
"json",
|
||||
"julia",
|
||||
"kotlin",
|
||||
"less",
|
||||
"lexon",
|
||||
"liquid",
|
||||
"lua",
|
||||
"m3",
|
||||
"markdown",
|
||||
"mips",
|
||||
"msdax",
|
||||
"mysql",
|
||||
"objective-c",
|
||||
"pascal",
|
||||
"pascaligo",
|
||||
"perl",
|
||||
"pgsql",
|
||||
"php",
|
||||
"pla",
|
||||
"plaintext",
|
||||
"postiats",
|
||||
"powerquery",
|
||||
"powershell",
|
||||
"proto",
|
||||
"pug",
|
||||
"python",
|
||||
"qsharp",
|
||||
"r",
|
||||
"razor",
|
||||
"redis",
|
||||
"redshift",
|
||||
"restructuredtext",
|
||||
"ruby",
|
||||
"rust",
|
||||
"sb",
|
||||
"scala",
|
||||
"scheme",
|
||||
"scss",
|
||||
"shell",
|
||||
"sol",
|
||||
"sparql",
|
||||
"sql",
|
||||
"st",
|
||||
"swift",
|
||||
"systemverilog",
|
||||
"tcl",
|
||||
"twig",
|
||||
"typescript",
|
||||
"vb",
|
||||
"verilog",
|
||||
"xml",
|
||||
"yaml"
|
||||
]
|
128
src/languages.ts
Normal file
128
src/languages.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
export const languages = [
|
||||
"abap",
|
||||
"aes",
|
||||
"apex",
|
||||
"azcli",
|
||||
"bat",
|
||||
"bicep",
|
||||
"c",
|
||||
"cameligo",
|
||||
"clojure",
|
||||
"coffeescript",
|
||||
"cpp",
|
||||
"csharp",
|
||||
"csp",
|
||||
"css",
|
||||
"dart",
|
||||
"dockerfile",
|
||||
"ecl",
|
||||
"elixir",
|
||||
"flow9",
|
||||
"fsharp",
|
||||
"go",
|
||||
"graphql",
|
||||
"handlebars",
|
||||
"hcl",
|
||||
"html",
|
||||
"ini",
|
||||
"java",
|
||||
"javascript",
|
||||
"json",
|
||||
"julia",
|
||||
"kotlin",
|
||||
"less",
|
||||
"lexon",
|
||||
"liquid",
|
||||
"lua",
|
||||
"m3",
|
||||
"markdown",
|
||||
"mips",
|
||||
"msdax",
|
||||
"mysql",
|
||||
"objective-c",
|
||||
"pascal",
|
||||
"pascaligo",
|
||||
"perl",
|
||||
"pgsql",
|
||||
"php",
|
||||
"pla",
|
||||
"plaintext",
|
||||
"postiats",
|
||||
"powerquery",
|
||||
"powershell",
|
||||
"proto",
|
||||
"pug",
|
||||
"python",
|
||||
"qsharp",
|
||||
"r",
|
||||
"razor",
|
||||
"redis",
|
||||
"redshift",
|
||||
"restructuredtext",
|
||||
"ruby",
|
||||
"rust",
|
||||
"sb",
|
||||
"scala",
|
||||
"scheme",
|
||||
"scss",
|
||||
"shell",
|
||||
"sol",
|
||||
"sparql",
|
||||
"sql",
|
||||
"st",
|
||||
"swift",
|
||||
"systemverilog",
|
||||
"tcl",
|
||||
"twig",
|
||||
"typescript",
|
||||
"vb",
|
||||
"verilog",
|
||||
"xml",
|
||||
"yaml"
|
||||
] as const;
|
||||
|
||||
export type Language = typeof languages[number];
|
||||
|
||||
/** The default extension of the known programming languages (to be used on download) */
|
||||
const defaultExtensions: {[key in Language]?: string} = {
|
||||
typescript: "ts",
|
||||
rust: "rs",
|
||||
cpp: "cpp",
|
||||
yaml: "yaml",
|
||||
html: "html",
|
||||
};
|
||||
|
||||
type LanguageExtensions = {[key in string]: Language};
|
||||
|
||||
/**
|
||||
* Additional extensions that refer to known languages, to detect filetype on upload.
|
||||
* NO NEED to add the default file extensions.
|
||||
*/
|
||||
const additionalExtensionsToLanguage: LanguageExtensions = {
|
||||
"yml": "yaml",
|
||||
"cc": "cpp",
|
||||
};
|
||||
|
||||
/** Used on download to determine file extension. */
|
||||
export function getFileExtension(language: Language) {
|
||||
return defaultExtensions[language] ?? "txt";
|
||||
};
|
||||
|
||||
/** invert key value */
|
||||
const defaultExtensionsToLanguage = Object.entries(defaultExtensions)
|
||||
.reduce((defaultExtensionsToLanguage, [key, value]) => {
|
||||
return {
|
||||
...defaultExtensionsToLanguage,
|
||||
[value]: key as Language
|
||||
}
|
||||
}, {} as LanguageExtensions);
|
||||
|
||||
const extensionsToLanguage: LanguageExtensions = {
|
||||
...defaultExtensionsToLanguage,
|
||||
...additionalExtensionsToLanguage,
|
||||
};
|
||||
|
||||
/** Used to detect programming language on upload based on file name */
|
||||
export function getLanguage(fileName: string): Language {
|
||||
return extensionsToLanguage[fileName.split(".")[1]] ?? "plaintext";
|
||||
};
|
|
@ -5,6 +5,7 @@ import type {
|
|||
IPosition,
|
||||
} from "monaco-editor/esm/vs/editor/editor.api";
|
||||
import debounce from "lodash.debounce";
|
||||
import { Language } from "./languages";
|
||||
|
||||
/** Options passed in to the Rustpad constructor. */
|
||||
export type RustpadOptions = {
|
||||
|
@ -13,7 +14,7 @@ export type RustpadOptions = {
|
|||
readonly onConnected?: () => unknown;
|
||||
readonly onDisconnected?: () => unknown;
|
||||
readonly onDesynchronized?: () => unknown;
|
||||
readonly onChangeLanguage?: (language: string) => unknown;
|
||||
readonly onChangeLanguage?: (language: Language) => unknown;
|
||||
readonly onChangeUsers?: (users: Record<number, UserInfo>) => unknown;
|
||||
readonly reconnectInterval?: number;
|
||||
};
|
||||
|
@ -97,7 +98,7 @@ class Rustpad {
|
|||
}
|
||||
|
||||
/** Try to set the language of the editor, if connected. */
|
||||
setLanguage(language: string): boolean {
|
||||
setLanguage(language: Language): boolean {
|
||||
this.ws?.send(`{"SetLanguage":${JSON.stringify(language)}}`);
|
||||
return this.ws !== undefined;
|
||||
}
|
||||
|
@ -443,7 +444,7 @@ type ServerMsg = {
|
|||
start: number;
|
||||
operations: UserOperation[];
|
||||
};
|
||||
Language?: string;
|
||||
Language?: Language;
|
||||
UserInfo?: {
|
||||
id: number;
|
||||
info: UserInfo | null;
|
||||
|
|
Loading…
Add table
Reference in a new issue