upload files

This commit is contained in:
SandPoot
2021-04-22 20:39:21 -03:00
parent 0c5d19ecc8
commit 5bb6078def
23 changed files with 1578 additions and 120 deletions
+4
View File
@@ -171,6 +171,8 @@ export const sortBy = (...iterateeFns) => array => {
return mappedArray;
};
export const sort = sortBy();
/**
* A fast implementation of reduce.
*/
@@ -235,6 +237,8 @@ export const uniqBy = iterateeFn => array => {
return result;
};
export const uniq = uniqBy();
/**
* Creates an array of grouped elements, the first of which contains
* the first elements of the given arrays, the second of which contains
+206
View File
@@ -0,0 +1,206 @@
/**
* @file
* @copyright 2020 watermelon914 (https://github.com/watermelon914)
* @license MIT
*/
import { clamp01 } from 'common/math';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Section, Input, Stack } from '../components';
import { Window } from '../layouts';
const ARROW_KEY_UP = 38;
const ARROW_KEY_DOWN = 40;
let lastScrollTime = 0;
export const ListInput = (props, context) => {
const { act, data } = useBackend(context);
const {
title,
message,
buttons,
timeout,
} = data;
// Search
const [showSearchBar, setShowSearchBar] = useLocalState(
context, 'search_bar', false);
const [displayedArray, setDisplayedArray] = useLocalState(
context, 'displayed_array', buttons);
// KeyPress
const [searchArray, setSearchArray] = useLocalState(
context, 'search_array', []);
const [searchIndex, setSearchIndex] = useLocalState(
context, 'search_index', 0);
const [lastCharCode, setLastCharCode] = useLocalState(
context, 'last_char_code', null);
// Selected Button
const [selectedButton, setSelectedButton] = useLocalState(
context, 'selected_button', buttons[0]);
const handleKeyDown = e => {
e.preventDefault();
if (lastScrollTime > performance.now()) {
return;
}
lastScrollTime = performance.now() + 125;
if (e.keyCode === ARROW_KEY_UP || e.keyCode === ARROW_KEY_DOWN) {
let direction = 1;
if (e.keyCode === ARROW_KEY_UP) direction = -1;
let index = 0;
for (index; index < buttons.length; index++) {
if (buttons[index] === selectedButton) break;
}
index += direction;
if (index < 0) index = buttons.length - 1;
else if (index >= buttons.length) index = 0;
setSelectedButton(buttons[index]);
setLastCharCode(null);
document.getElementById(buttons[index]).focus();
return;
}
const charCode = String.fromCharCode(e.keyCode).toLowerCase();
if (!charCode) return;
let foundValue;
if (charCode === lastCharCode && searchArray.length > 0) {
const nextIndex = searchIndex + 1;
if (nextIndex < searchArray.length) {
foundValue = searchArray[nextIndex];
setSearchIndex(nextIndex);
}
else {
foundValue = searchArray[0];
setSearchIndex(0);
}
}
else {
const resultArray = displayedArray.filter(value =>
value.substring(0, 1).toLowerCase() === charCode
);
if (resultArray.length > 0) {
setSearchArray(resultArray);
setSearchIndex(0);
foundValue = resultArray[0];
}
}
if (foundValue) {
setLastCharCode(charCode);
setSelectedButton(foundValue);
document.getElementById(foundValue).focus();
}
};
return (
<Window
title={title}
width={325}
height={325}>
{timeout !== undefined && <Loader value={timeout} />}
<Window.Content>
<Stack fill vertical>
<Stack.Item grow>
<Section
fill
scrollable
className="ListInput__Section"
title={message}
tabIndex={0}
onKeyDown={handleKeyDown}
buttons={(
<Button
compact
icon="search"
color="transparent"
selected={showSearchBar}
tooltip="Search Bar"
tooltipPosition="left"
onClick={() => {
setShowSearchBar(!showSearchBar);
setDisplayedArray(buttons);
}}
/>
)}>
{displayedArray.map(button => (
<Button
key={button}
fluid
color="transparent"
id={button}
selected={selectedButton === button}
onClick={() => {
if (selectedButton === button) {
act("choose", { choice: button });
}
else {
setSelectedButton(button);
}
setLastCharCode(null);
}}>
{button}
</Button>
))}
</Section>
</Stack.Item>
{showSearchBar && (
<Stack.Item>
<Input
fluid
onInput={(e, value) => setDisplayedArray(
buttons.filter(val => (
val.toLowerCase().search(value.toLowerCase()) !== -1
))
)}
/>
</Stack.Item>
)}
<Stack.Item>
<Stack textAlign="center">
<Stack.Item grow basis={0}>
<Button
fluid
color="good"
lineHeight={2}
content="Confirm"
disabled={selectedButton === null}
onClick={() => act("choose", { choice: selectedButton })}
/>
</Stack.Item>
<Stack.Item grow basis={0}>
<Button
fluid
color="bad"
lineHeight={2}
content="Cancel"
onClick={() => act("cancel")}
/>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
export const Loader = props => {
const { value } = props;
return (
<div className="ListInput__Loader">
<Box
className="ListInput__LoaderProgress"
style={{
width: clamp01(value) * 100 + '%',
}} />
</div>
);
};
@@ -0,0 +1,146 @@
import { useBackend } from '../backend';
import { Box, Button, Icon, Section, Stack } from '../components';
import { Window } from '../layouts';
export const OutfitEditor = (props, context) => {
const { act, data } = useBackend(context);
const { outfit, saveable, dummy64 } = data;
return (
<Window
width={380}
height={600}>
<Window.Content>
<Box
as="img"
fillPositionedParent
width="100%"
height="100%"
opacity={0.5}
py={3}
src={`data:image/jpeg;base64,${dummy64}`}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
<Section
fill
title={
<Stack>
<Stack.Item grow={1}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
<Button
ml={0.5}
color="transparent"
icon="pencil-alt"
title="Rename this outfit"
onClick={() => act("rename", {})} />
{outfit.name}
</Stack.Item>
<Stack.Item align="end" shrink={0}>
<Button
color="transparent"
icon="info"
tooltip="Ctrl-click a button to select *any* item instead of what will probably fit in that slot."
tooltipPosition="bottom-left" />
<Button
icon="code"
tooltip="Edit this outfit on a VV window"
tooltipPosition="bottom-left"
onClick={() => act("vv")} />
<Button
color={!saveable && "bad"}
icon={saveable ? "save" : "trash-alt"}
tooltip={saveable
? "Save this outfit to the custom outfit list"
: "Remove this outfit from the custom outfit list"}
tooltipPosition="bottom-left"
onClick={() => act(saveable ? "save" : "delete")} />
</Stack.Item>
</Stack>
}>
<Box textAlign="center">
<Stack mb={2}>
<OutfitSlot name="Headgear" icon="hard-hat" slot="head" />
<OutfitSlot name="Glasses" icon="glasses" slot="glasses" />
<OutfitSlot name="Ears" icon="headphones-alt" slot="ears" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Neck" icon="stethoscope" slot="neck" />
<OutfitSlot name="Mask" icon="theater-masks" slot="mask" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Uniform" icon="tshirt" slot="uniform" />
<OutfitSlot name="Suit" icon="user-tie" slot="suit" />
<OutfitSlot name="Gloves" icon="mitten" slot="gloves" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Suit Storage" icon="briefcase-medical" slot="suit_store" />
<OutfitSlot name="Back" icon="shopping-bag" slot="back" />
<OutfitSlot name="ID" icon="id-card-o" slot="id" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Belt" icon="band-aid" slot="belt" />
<OutfitSlot name="Left Hand" icon="hand-paper" slot="l_hand" />
<OutfitSlot name="Right Hand" icon="hand-paper" slot="r_hand" />
</Stack>
<Stack mb={2}>
<OutfitSlot name="Shoes" icon="socks" slot="shoes" />
<OutfitSlot name="Left Pocket" icon="envelope-open-o" iconRot={180} slot="l_pocket" />
<OutfitSlot name="Right Pocket" icon="envelope-open-o" iconRot={180} slot="r_pocket" />
</Stack>
</Box>
</Section>
</Window.Content>
</Window>
);
};
const OutfitSlot = (props, context) => {
const { act, data } = useBackend(context);
const { name, icon, iconRot, slot } = props;
const { outfit } = data;
const currItem = outfit[slot];
return (
<Stack.Item grow={1} basis={0}>
<Button fluid height={2}
bold
// todo: intuitive way to clear items
onClick={e => act(e.ctrlKey ? "ctrlClick" : "click", { slot })} >
<Icon name={icon} rotation={iconRot} />
{name}
</Button>
<Box height="32px">
{currItem?.sprite && (
<>
<Box
as="img"
src={`data:image/jpeg;base64,${currItem?.sprite}`}
title={currItem?.desc}
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
<Icon
position="absolute"
name="times"
color="label"
style={{ cursor: 'pointer' }}
onClick={() => act("clear", { slot })} />
</>
)}
</Box>
<Box
color="label"
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}
title={currItem?.path}>
{currItem?.name || "Empty"}
</Box>
</Stack.Item>
);
};
@@ -0,0 +1,81 @@
import { useBackend } from '../backend';
import { Button, Section, Stack } from '../components';
import { Window } from '../layouts';
export const OutfitManager = (props, context) => {
const { act, data } = useBackend(context);
const { outfits } = data;
return (
<Window
title="Outfit Manager"
width={300}
height={300}>
<Window.Content>
<Section
fill
scrollable
title="Custom Outfit Manager"
buttons={
<>
<Button
icon="file-upload"
tooltip="Load an outfit from a file"
tooltipPosition="left"
onClick={() => act("load")} />
<Button
icon="copy"
tooltip="Copy an already existing outfit"
tooltipPosition="left"
onClick={() => act("copy")} />
<Button
icon="plus"
tooltip="Create a new outfit"
tooltipPosition="left"
onClick={() => act("new")} />
</>
}>
<Stack vertical>
{outfits?.map(outfit => (
<Stack.Item key={outfit.ref}>
<Stack>
<Stack.Item grow={1} shrink={1}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
<Button
fluid
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}
content={outfit.name}
onClick={() => act("edit", { outfit: outfit.ref })} />
</Stack.Item>
<Stack.Item ml={0.5}>
<Button
icon="save"
tooltip="Save this outfit to a file"
tooltipPosition="left"
onClick={() => act("save", { outfit: outfit.ref })} />
</Stack.Item>
<Stack.Item ml={0.5}>
<Button
color="bad"
icon="trash-alt"
tooltip="Delete this outfit"
tooltipPosition="left"
onClick={() => act("delete", { outfit: outfit.ref })} />
</Stack.Item>
</Stack>
</Stack.Item>
))}
</Stack>
</Section>
</Window.Content>
</Window>
);
};
@@ -0,0 +1,214 @@
import { filter, map, sortBy, uniq } from 'common/collections';
import { flow } from 'common/fp';
import { createSearch } from 'common/string';
import { useBackend, useLocalState } from '../backend';
import { Box, Button, Icon, Input, Section, Stack, Tabs } from '../components';
import { Window } from '../layouts';
// here's an important mental define:
// custom outfits give a ref keyword instead of path
const getOutfitKey = outfit => outfit.path || outfit.ref;
const useOutfitTabs = (context, categories) => {
return useLocalState(context, 'selected-tab', categories[0]);
};
export const SelectEquipment = (props, context) => {
const { act, data } = useBackend(context);
const {
name,
icon64,
current_outfit,
favorites,
} = data;
const isFavorited = entry => favorites?.includes(entry.path);
const outfits = map(entry => ({
...entry,
favorite: isFavorited(entry),
}))([
...data.outfits,
...data.custom_outfits,
]);
// even if no custom outfits were sent, we still want to make sure there's
// at least a 'Custom' tab so the button to create a new one pops up
const categories = uniq([
...outfits.map(entry => entry.category),
'Custom',
]);
const [tab] = useOutfitTabs(context, categories);
const [searchText, setSearchText] = useLocalState(
context, 'searchText', '');
const searchFilter = createSearch(searchText, entry => (
entry.name + entry.path
));
const visibleOutfits = flow([
filter(entry => entry.category === tab),
filter(searchFilter),
sortBy(
entry => !entry.favorite,
entry => !entry.priority,
entry => entry.name
),
])(outfits);
const getOutfitEntry = current_outfit => outfits.find(outfit => (
getOutfitKey(outfit) === current_outfit
));
const currentOutfitEntry = getOutfitEntry(current_outfit);
return (
<Window
width={650}
height={415}>
<Window.Content>
<Stack fill>
<Stack.Item>
<Stack fill vertical>
<Stack.Item>
<Input
fluid
autoFocus
placeholder="Search"
value={searchText}
onInput={(e, value) => setSearchText(value)} />
</Stack.Item>
<Stack.Item>
<DisplayTabs categories={categories} />
</Stack.Item>
<Stack.Item mt={0} grow={1} basis={0}>
<OutfitDisplay
entries={visibleOutfits}
currentTab={tab} />
</Stack.Item>
</Stack>
</Stack.Item>
<Stack.Item grow={1} basis={0}>
<Stack fill vertical>
<Stack.Item>
<Section>
<CurrentlySelectedDisplay entry={currentOutfitEntry} />
</Section>
</Stack.Item>
<Stack.Item grow={1}>
<Section
fill
title={name}
textAlign="center">
<Box
as="img"
m={0}
src={`data:image/jpeg;base64,${icon64}`}
height="100%"
style={{
'-ms-interpolation-mode': 'nearest-neighbor',
}} />
</Section>
</Stack.Item>
</Stack>
</Stack.Item>
</Stack>
</Window.Content>
</Window>
);
};
const DisplayTabs = (props, context) => {
const { categories } = props;
const [tab, setTab] = useOutfitTabs(context, categories);
return (
<Tabs textAlign="center">
{categories.map(category => (
<Tabs.Tab
key={category}
selected={tab === category}
onClick={() => setTab(category)}>
{category}
</Tabs.Tab>
))}
</Tabs>
);
};
const OutfitDisplay = (props, context) => {
const { act, data } = useBackend(context);
const { current_outfit } = data;
const { entries, currentTab } = props;
return (
<Section fill scrollable>
{entries.map(entry => (
<Button
key={getOutfitKey(entry)}
fluid
ellipsis
icon={entry.favorite && 'star'}
iconColor="gold"
content={entry.name}
title={entry.path || entry.name}
selected={getOutfitKey(entry) === current_outfit}
onClick={() => act('preview', { path: getOutfitKey(entry) })} />
))}
{currentTab === "Custom" && (
<Button
color="transparent"
icon="plus"
fluid
onClick={() => act('customoutfit')}>
Create a custom outfit...
</Button>
)}
</Section>
);
};
const CurrentlySelectedDisplay = (props, context) => {
const { act, data } = useBackend(context);
const { current_outfit } = data;
const { entry } = props;
return (
<Stack align="center">
{entry?.path && (
<Stack.Item>
<Icon
size={1.6}
name={entry.favorite ? 'star' : 'star-o'}
color="gold"
style={{ cursor: 'pointer' }}
onClick={() => act('togglefavorite', {
path: entry.path,
})} />
</Stack.Item>
)}
<Stack.Item grow={1} basis={0}>
<Box color="label">
Currently selected:
</Box>
<Box
title={entry?.path}
style={{
'overflow': 'hidden',
'white-space': 'nowrap',
'text-overflow': 'ellipsis',
}}>
{entry?.name}
</Box>
</Stack.Item>
<Stack.Item>
<Button
mr={0.8}
lineHeight={2}
color="green"
onClick={() => act('applyoutfit', {
path: current_outfit,
})}>
Confirm
</Button>
</Stack.Item>
</Stack>
);
};