reactify guild overview display and guild settings overlay
This commit is contained in:
parent
c12873d929
commit
392a8cf4bc
1
makefile
1
makefile
@ -29,6 +29,7 @@ move:
|
||||
clean:
|
||||
mkdir -p ./dist
|
||||
rm -r ./dist
|
||||
rm -r ./db
|
||||
|
||||
reset-server:
|
||||
psql postgres postgres < ./src/server/sql/init.sql
|
||||
|
@ -1,14 +1,41 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, useCallback, useMemo } from 'react';
|
||||
|
||||
export enum ButtonType {
|
||||
BRAND = '',
|
||||
POSITIVE = 'positive',
|
||||
NEGATIVE = 'negative',
|
||||
PERDU = 'perdu',
|
||||
}
|
||||
|
||||
interface ButtonProps {
|
||||
onClick: () => void
|
||||
type?: ButtonType;
|
||||
onClick?: () => void;
|
||||
shaking?: boolean;
|
||||
children: React.ReactNode
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const DefaultButtonProps: ButtonProps = {
|
||||
type: ButtonType.BRAND
|
||||
}
|
||||
|
||||
const Button: FC<ButtonProps> = (props: ButtonProps) => {
|
||||
const { onClick, shaking, children } = props;
|
||||
return <div className={'button ' + (shaking ? 'shaking-horizontal' : '')} onClick={onClick}>{children}</div>
|
||||
const { type, onClick, shaking, children } = { ...DefaultButtonProps, ...props };
|
||||
|
||||
const className = useMemo(
|
||||
() => [
|
||||
'button',
|
||||
type,
|
||||
shaking && 'shaking-horizontal',
|
||||
].filter(c => typeof c === 'string').join(' '),
|
||||
[ type, shaking ]
|
||||
);
|
||||
|
||||
const clickHandler = useCallback(() => {
|
||||
if (shaking) return; // ignore clicks while shaking
|
||||
if (onClick) onClick();
|
||||
}, [ shaking, onClick ]);
|
||||
|
||||
return <div className={className} onClick={clickHandler}>{children}</div>
|
||||
}
|
||||
|
||||
export default Button;
|
23
src/client/webapp/elements/components/display-popup.tsx
Normal file
23
src/client/webapp/elements/components/display-popup.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React, { FC } from 'react';
|
||||
|
||||
export interface DisplayPopupProps {
|
||||
tip: string | null;
|
||||
children?: React.ReactNode; // buttons
|
||||
}
|
||||
|
||||
const DisplayPopup: FC<DisplayPopupProps> = (props: DisplayPopupProps) => {
|
||||
const { tip, children } = props;
|
||||
|
||||
return (
|
||||
<div className="popup changes">
|
||||
<div className="content">
|
||||
<div className="tip">{tip}</div>
|
||||
<div className="actions">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DisplayPopup;
|
75
src/client/webapp/elements/components/display.tsx
Normal file
75
src/client/webapp/elements/components/display.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { FC, useEffect, useMemo, useState } from "react";
|
||||
import ElementsUtil from "../require/elements-util";
|
||||
import Button, { ButtonType } from "./button";
|
||||
import DisplayPopup from "./display-popup";
|
||||
|
||||
interface DisplayProps {
|
||||
children: React.ReactNode;
|
||||
|
||||
changes: boolean;
|
||||
resetChanges: () => void;
|
||||
saveChanges: () => void;
|
||||
|
||||
saving: boolean;
|
||||
saveFailed: boolean;
|
||||
errorMessage: string | null;
|
||||
}
|
||||
|
||||
const Display: FC<DisplayProps> = (props: DisplayProps) => {
|
||||
const { children, changes, resetChanges, saveChanges, saving, saveFailed, errorMessage } = props;
|
||||
|
||||
const [ saveButtonShaking, setSaveButtonShaking ] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (saveFailed) {
|
||||
await ElementsUtil.delayToggleState(setSaveButtonShaking, 400);
|
||||
}
|
||||
})();
|
||||
}, [ saveFailed ]);
|
||||
|
||||
const changesButtonText = useMemo(() => {
|
||||
if (saving) {
|
||||
return 'Saving...';
|
||||
} else if (saveFailed) {
|
||||
return 'Try Again';
|
||||
} else {
|
||||
return 'Save Changes';
|
||||
}
|
||||
}, [ saving, saveFailed ]);
|
||||
|
||||
const popup = useMemo(() => {
|
||||
if (errorMessage) {
|
||||
return (
|
||||
<DisplayPopup tip={errorMessage}>
|
||||
<Button type={ButtonType.PERDU} onClick={resetChanges}>Reset</Button>
|
||||
</DisplayPopup>
|
||||
);
|
||||
} else if (changes) {
|
||||
return (
|
||||
<DisplayPopup tip={'You have unsaved changes'}>
|
||||
<Button type={ButtonType.PERDU} onClick={resetChanges}>Reset</Button>
|
||||
<Button type={ButtonType.POSITIVE} onClick={saveChanges} shaking={saveButtonShaking}>{changesButtonText}</Button>
|
||||
</DisplayPopup>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [ errorMessage, changes, resetChanges, saveChanges ]);
|
||||
|
||||
return (
|
||||
<div className="display">
|
||||
<div className="scroll">
|
||||
{children}
|
||||
</div>
|
||||
{popup}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Display;
|
93
src/client/webapp/elements/components/input-image-edit.tsx
Normal file
93
src/client/webapp/elements/components/input-image-edit.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { FC, useEffect, useRef, useState } from 'react';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
|
||||
import * as FileType from 'file-type';
|
||||
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
interface ImageEditInputProps {
|
||||
alt?: string;
|
||||
|
||||
maxSize: number;
|
||||
|
||||
value: Buffer | null;
|
||||
setValue: React.Dispatch<React.SetStateAction<Buffer | null>>;
|
||||
|
||||
setErrorMessage: React.Dispatch<React.SetStateAction<string | null>>;
|
||||
}
|
||||
|
||||
const ImageEditInput: FC<ImageEditInputProps> = (props: ImageEditInputProps) => {
|
||||
const { alt, maxSize, value, setValue, setErrorMessage } = props;
|
||||
|
||||
const acceptedExtTypes = [ 'png', 'jpg', 'jpeg' ];
|
||||
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
const [ imgSrc, setImgSrc ] = useState<string>('./img/loading.svg');
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
if (value) {
|
||||
try {
|
||||
const src = await ElementsUtil.getImageBufferSrc(value);
|
||||
setImgSrc(src);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('unable to get image buffer src', e);
|
||||
setImgSrc('./img/error.png');
|
||||
setErrorMessage('Unable to get image src');
|
||||
}
|
||||
} else {
|
||||
setImgSrc('./img/loading.svg');
|
||||
}
|
||||
})();
|
||||
}, [ value ]);
|
||||
|
||||
useEffect(() => {
|
||||
if (imgRef.current) {
|
||||
imgRef.current.src = imgSrc;
|
||||
}
|
||||
}, [ imgSrc ])
|
||||
|
||||
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = e.target.files;
|
||||
if (!files || files.length === 0) {
|
||||
LOG.debug('no files');
|
||||
return;
|
||||
}
|
||||
const file = files[0] as File;
|
||||
if (file.size > maxSize) {
|
||||
e.target.value = '';
|
||||
LOG.debug('image too large');
|
||||
setErrorMessage(`Image too large. (${ElementsUtil.humanSize(file.size)}>${ElementsUtil.humanSize(maxSize)})`);
|
||||
return;
|
||||
}
|
||||
const buff = Buffer.from(await file.arrayBuffer());
|
||||
const typeResult = await FileType.fromBuffer(buff);
|
||||
if (!typeResult || !acceptedExtTypes.includes(typeResult.ext)) {
|
||||
e.target.value = '';
|
||||
setErrorMessage(`Invalid image type. Accepted types: ${acceptedExtTypes.join(', ')}`);
|
||||
return;
|
||||
}
|
||||
setErrorMessage(null);
|
||||
setValue(buff);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="image-edit-input-react">
|
||||
<label className="label">
|
||||
<div className="image">
|
||||
<img ref={imgRef} className="value" src={imgSrc} alt={alt ?? 'icon'}></img>
|
||||
<div className="modify"><img className="pencil" src="./img/pencil-icon.png" alt="modify"></img></div>
|
||||
</div>
|
||||
<input type="file" accept={acceptedExtTypes.map(t => `.${t}`).join(',')} onChange={handleFileChange}></input>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImageEditInput;
|
43
src/client/webapp/elements/components/input-text.tsx
Normal file
43
src/client/webapp/elements/components/input-text.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { FC, useMemo, useState } from 'react';
|
||||
|
||||
export interface TextInputProps {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
|
||||
value: string;
|
||||
setValue: React.Dispatch<React.SetStateAction<string>>;
|
||||
|
||||
onEnterKeyDown?: () => void;
|
||||
}
|
||||
|
||||
const TextInput: FC<TextInputProps> = (props: TextInputProps) => {
|
||||
const { label, placeholder, value, setValue, onEnterKeyDown } = props;
|
||||
|
||||
const labelElement = useMemo(() => {
|
||||
return label && <div className="label">{label}</div>
|
||||
}, [ label ]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setValue(e.target.value);
|
||||
}
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (onEnterKeyDown) onEnterKeyDown();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-input-react">{/* TODO: remove -react */}
|
||||
{labelElement}
|
||||
<input type="text" placeholder={placeholder} onChange={handleChange} onKeyDown={handleKeyDown} value={value} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextInput;
|
@ -26,22 +26,12 @@ const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
||||
if (node.current) {
|
||||
ReactDOM.unmountComponentAtNode(node.current.parentElement as Element);
|
||||
}
|
||||
if (keyDownEventHandler) {
|
||||
window.removeEventListener('keydown', keyDownEventHandler);
|
||||
if (keyDownHandler) {
|
||||
window.removeEventListener('keydown', keyDownHandler);
|
||||
}
|
||||
// otherwise, this isn't in the DOM anyway
|
||||
};
|
||||
|
||||
const keyDownEventHandler = useMemo(() => {
|
||||
const eventHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
removeSelf();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', eventHandler);
|
||||
return eventHandler;
|
||||
}, []);
|
||||
|
||||
const checkMouseDown = (e: React.MouseEvent) => {
|
||||
if (e.target === node.current) {
|
||||
setMouseDownInChild(false);
|
||||
@ -58,7 +48,23 @@ const Overlay: FC<OverlayProps> = (props: OverlayProps) => {
|
||||
}
|
||||
};
|
||||
|
||||
return <div className="overlay" ref={node} onMouseDown={checkMouseDown} onMouseUp={checkMouseUp} onClick={removeSelf}>{children}</div>
|
||||
const keyDownHandler = useMemo(() => {
|
||||
const eventHandler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
removeSelf();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', eventHandler);
|
||||
return eventHandler;
|
||||
}, []);
|
||||
|
||||
const clickHandler = (e: React.MouseEvent) => {
|
||||
if (e.target === node.current) {
|
||||
removeSelf();
|
||||
}
|
||||
}
|
||||
|
||||
return <div className="overlay" ref={node} onMouseDown={checkMouseDown} onMouseUp={checkMouseUp} onClick={clickHandler}>{children}</div>
|
||||
};
|
||||
|
||||
export default Overlay;
|
||||
|
@ -10,7 +10,6 @@ import Q from '../q-module';
|
||||
import UI from '../ui';
|
||||
|
||||
import createErrorMessageOverlay from './overlay-error-message';
|
||||
import createGuildSettingsOverlay from './overlay-guild-settings';
|
||||
import createCreateInviteTokenOverlay from './overlay-create-invite-token';
|
||||
import createCreateChannelOverlay from './overlay-create-channel';
|
||||
import createTokenLogOverlay from './overlay-token-log';
|
||||
@ -18,6 +17,7 @@ import CombinedGuild from '../guild-combined';
|
||||
|
||||
import React from 'react';
|
||||
import ReactHelper from './require/react-helper';
|
||||
import GuildSettingsOverlay from './overlays/overlay-guild-settings';
|
||||
|
||||
export default function createGuildTitleContextMenu(document: Document, q: Q, ui: UI, guild: CombinedGuild): Element {
|
||||
if (ui.activeConnection === null) {
|
||||
@ -25,7 +25,6 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui
|
||||
return ReactHelper.createElementFromJSX(<div></div>);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const menuItems: JSX.Element[] = [];
|
||||
|
||||
if (ui.activeConnection.privileges.includes('modify_profile')) {
|
||||
@ -87,8 +86,9 @@ export default function createGuildTitleContextMenu(document: Document, q: Q, ui
|
||||
const overlay = createErrorMessageOverlay(document, 'Error Opening Settings', 'Could not load guild information');
|
||||
document.body.appendChild(overlay);
|
||||
} else {
|
||||
const overlay = createGuildSettingsOverlay(document, q, guild, guildMeta);
|
||||
document.body.appendChild(overlay);
|
||||
ElementsUtil.presentReactOverlay(document, <GuildSettingsOverlay guild={guild} guildMeta={guildMeta} />);
|
||||
// const overlay = createGuildSettingsOverlay(document, q, guild, guildMeta);
|
||||
// document.body.appendChild(overlay);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
129
src/client/webapp/elements/displays/display-guild-overview.tsx
Normal file
129
src/client/webapp/elements/displays/display-guild-overview.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { GuildMetadata } from '../../data-types';
|
||||
import Globals from '../../globals';
|
||||
import CombinedGuild from '../../guild-combined';
|
||||
import Display from '../components/display';
|
||||
import ElementsUtil from '../require/elements-util';
|
||||
import TextInput from '../components/input-text';
|
||||
import ImageEditInput from '../components/input-image-edit';
|
||||
|
||||
export interface GuildOverviewDisplayProps {
|
||||
guild: CombinedGuild;
|
||||
guildMeta: GuildMetadata;
|
||||
}
|
||||
const GuildOverviewDisplay: FC<GuildOverviewDisplayProps> = (props: GuildOverviewDisplayProps) => {
|
||||
const { guild, guildMeta } = props;
|
||||
|
||||
const [ savedName, setSavedName ] = useState<string>(guildMeta.name);
|
||||
const [ savedIconBuff, setSavedIconBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ name, setName ] = useState<string>(guildMeta.name);
|
||||
const [ iconBuff, setIconBuff ] = useState<Buffer | null>(null);
|
||||
|
||||
const [ saving, setSaving ] = useState<boolean>(false);
|
||||
|
||||
const [ iconFailed, setIconFailed ] = useState<boolean>(false);
|
||||
const [ saveFailed, setSaveFailed ] = useState<boolean>(false);
|
||||
|
||||
const [ imageInputErrorMessage, setImageInputErrorMessage ] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const iconResource = await guild.fetchResource(guildMeta.iconResourceId);
|
||||
setSavedIconBuff(iconResource.data);
|
||||
setIconBuff(iconResource.data);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('Error loading icon resource', e);
|
||||
setIconFailed(true);
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const changes = useMemo(() => {
|
||||
return name !== savedName || iconBuff?.toString('hex') !== savedIconBuff?.toString('hex')
|
||||
}, [ name, savedName, iconBuff, savedIconBuff ]);
|
||||
|
||||
const errorMessage = useMemo(() => {
|
||||
if (iconFailed) {
|
||||
return 'Unable to load icon';
|
||||
} else if (imageInputErrorMessage) {
|
||||
return imageInputErrorMessage;
|
||||
} else if (iconBuff && iconBuff.length > Globals.MAX_GUILD_ICON_SIZE) {
|
||||
return `Icon is too large. (${ElementsUtil.humanSize(iconBuff.length)}>${ElementsUtil.humanSize(Globals.MAX_GUILD_ICON_SIZE)}) Try a 512x512 icon.`;
|
||||
} else if (name.length === 0) {
|
||||
return 'Name is empty';
|
||||
} else if (name.length > Globals.MAX_GUILD_NAME_LENGTH) {
|
||||
return `Name is too long. (${name.length}>${Globals.MAX_GUILD_NAME_LENGTH} characters)`;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, [ iconFailed, imageInputErrorMessage, iconBuff, name ]);
|
||||
|
||||
const resetChanges = () => {
|
||||
setImageInputErrorMessage(null);
|
||||
setName(savedName);
|
||||
setIconBuff(savedIconBuff);
|
||||
}
|
||||
|
||||
const saveChanges = useCallback(async () => {
|
||||
if (errorMessage) return;
|
||||
if (saving) return;
|
||||
|
||||
setSaving(true);
|
||||
|
||||
if (name !== savedName) {
|
||||
// Save name
|
||||
try {
|
||||
await guild.requestSetGuildName(name);
|
||||
setSavedName(name);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error setting guild name', e);
|
||||
setSaveFailed(true);
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (iconBuff && iconBuff?.toString('hex') !== savedIconBuff?.toString('hex')) {
|
||||
// Save icon
|
||||
try {
|
||||
LOG.debug('saving icon');
|
||||
await guild.requestSetGuildIcon(iconBuff);
|
||||
setSavedIconBuff(iconBuff);
|
||||
} catch (e: unknown) {
|
||||
LOG.error('error setting guild icon', e);
|
||||
setSaveFailed(true);
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setSaving(false);
|
||||
}, [ errorMessage, saving, name, savedName, iconBuff ]);
|
||||
|
||||
return (
|
||||
<Display
|
||||
changes={changes}
|
||||
resetChanges={resetChanges} saveChanges={saveChanges}
|
||||
saving={saving} saveFailed={saveFailed} errorMessage={errorMessage}
|
||||
>
|
||||
<div className="metadata">
|
||||
<div className="icon">
|
||||
<ImageEditInput maxSize={Globals.MAX_GUILD_ICON_SIZE} value={iconBuff} setValue={setIconBuff} setErrorMessage={setImageInputErrorMessage} />
|
||||
</div>
|
||||
<div className="name">
|
||||
<TextInput label={'Guild Name'} placeholder={savedName} value={name} setValue={setName} onEnterKeyDown={saveChanges} />
|
||||
</div>
|
||||
</div>
|
||||
</Display>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuildOverviewDisplay;
|
@ -107,7 +107,7 @@ export default function createGuildSettingsOverlay(document: Document, q: Q, gui
|
||||
});
|
||||
|
||||
BaseElements.bindImageUploadEvents(q.$$$(element, '.image-input-upload') as HTMLInputElement, {
|
||||
maxSize: Globals.MAX_ICON_SIZE,
|
||||
maxSize: Globals.MAX_GUILD_ICON_SIZE,
|
||||
acceptedMimeTypes: [ 'image/png', 'image/jpeg', 'image/jpg' ],
|
||||
onChangeStart: async () => await updatePopups(),
|
||||
onCleared: () => {},
|
||||
|
@ -1,78 +0,0 @@
|
||||
import * as electronRemote from '@electron/remote';
|
||||
const electronConsole = electronRemote.getGlobal('console') as Console;
|
||||
import Logger from '../../../logger/logger';
|
||||
const LOG = Logger.create(__filename, electronConsole);
|
||||
|
||||
import * as FileType from 'file-type'
|
||||
|
||||
import BaseElements, { HTMLElementWithRemoveSelf } from './require/base-elements';
|
||||
import ElementsUtil from './require/elements-util';
|
||||
|
||||
import Q from '../q-module';
|
||||
import createImageContextMenu from './context-menu-img';
|
||||
import CombinedGuild from '../guild-combined';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default function createImageOverlay(document: Document, q: Q, guild: CombinedGuild, resourceId: string, resourceName: string): HTMLElementWithRemoveSelf {
|
||||
const element = BaseElements.createOverlay(document, (
|
||||
<div className="content popup-image">
|
||||
<img src="./img/loading.svg" alt={resourceName} title={resourceName}></img>
|
||||
<div className="download">
|
||||
<div className="info">
|
||||
<div className="name">{resourceName}</div>
|
||||
<div className="size">Loading Size...</div>
|
||||
</div>
|
||||
<div className="button">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
const resource = await guild.fetchResource(resourceId);
|
||||
const src = await ElementsUtil.getImageBufferSrc(resource.data);
|
||||
(q.$$$(element, '.content img') as HTMLImageElement).src = src;
|
||||
q.$$$(element, '.download .size').innerText = ElementsUtil.humanSize(resource.data.length);
|
||||
|
||||
const { mime, ext } = (await FileType.fromBuffer(resource.data)) ?? { mime: null, ext: null };
|
||||
if (mime === null || ext === null) throw new Error('unable to get mime/ext');
|
||||
|
||||
q.$$$(element, '.content img').addEventListener('contextmenu', (e) => {
|
||||
const contextMenu = createImageContextMenu(document, q, guild, resourceName, resource.data, mime as string, ext as string, false);
|
||||
document.body.appendChild(contextMenu);
|
||||
const relativeTo = { x: e.pageX, y: e.pageY };
|
||||
ElementsUtil.alignContextElement(contextMenu, relativeTo, { top: 'centerY', left: 'right' });
|
||||
});
|
||||
|
||||
q.$$$(element, '.button').innerText = 'Save';
|
||||
q.$$$(element, '.button').addEventListener('click', ElementsUtil.createDownloadListener({
|
||||
downloadBuff: resource.data,
|
||||
resourceName: resourceName,
|
||||
downloadStartFunc: () => {
|
||||
q.$$$(element, '.button').innerText = 'Downloading...';
|
||||
},
|
||||
downloadFailFunc: async () => {
|
||||
q.$$$(element, '.button').innerText = 'Try Again';
|
||||
await ElementsUtil.shakeElement(q.$$$(element, '.button'), 400);
|
||||
},
|
||||
writeStartFunc: () => {
|
||||
q.$$$(element, '.button').innerText = 'Writing...';
|
||||
},
|
||||
writeFailFunc: async () => {
|
||||
q.$$$(element, '.button').innerText = 'Try Again';
|
||||
await ElementsUtil.shakeElement(q.$$$(element, '.button'), 400);
|
||||
},
|
||||
successFunc: (_downloadPath: string) => {
|
||||
q.$$$(element, '.button').innerText = 'Reveal in Explorer';
|
||||
}
|
||||
}));
|
||||
} catch (e) {
|
||||
LOG.error('error loading overlay image', e);
|
||||
(q.$$$(element, '.content img') as HTMLImageElement).src = './img/error.png';
|
||||
q.$$$(element, '.download .size').innerText = 'err';
|
||||
q.$$$(element, '.button').innerText = 'Error';
|
||||
}
|
||||
})();
|
||||
return element;
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
import React, { FC, useState } from "react";
|
||||
import { GuildMetadata } from "../../data-types";
|
||||
import CombinedGuild from "../../guild-combined";
|
||||
import GuildOverviewDisplay from "../displays/display-guild-overview";
|
||||
|
||||
export interface GuildSettingsOverlayProps {
|
||||
guild: CombinedGuild;
|
||||
guildMeta: GuildMetadata;
|
||||
}
|
||||
const GuildSettingsOverlay: FC<GuildSettingsOverlayProps> = (props: GuildSettingsOverlayProps) => {
|
||||
const { guild, guildMeta } = props;
|
||||
|
||||
return (
|
||||
<div className="content display-swapper guild-settings">
|
||||
<div className="options">
|
||||
<div className="title">{guildMeta.name}</div>
|
||||
<div className="choosable chosen">Overview</div>
|
||||
<div className="choosable">Channels</div>
|
||||
<div className="choosable">Roles</div>
|
||||
<div className="choosable">Invites</div>
|
||||
</div>
|
||||
<GuildOverviewDisplay guild={guild} guildMeta={guildMeta} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GuildSettingsOverlay;
|
@ -47,7 +47,7 @@ const ImageOverlay: FC<ImageOverlayProps> = (props: ImageOverlayProps) => {
|
||||
return;
|
||||
}
|
||||
})();
|
||||
});
|
||||
}, []);
|
||||
|
||||
const onImageContextMenu = (e: React.MouseEvent) => {
|
||||
// TODO: This should be in react!
|
||||
|
@ -13,7 +13,7 @@ export default class Globals {
|
||||
static MAX_AVATAR_SIZE = 1024 * 128; // 128 KB max avatar size
|
||||
static MAX_DISPLAY_NAME_LENGTH = 32; // 32 char max display name length
|
||||
|
||||
static MAX_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size
|
||||
static MAX_GUILD_ICON_SIZE = 1024 * 128; // 128 KB max guild icon size
|
||||
static MAX_GUILD_NAME_LENGTH = 64; // 64 char max guild name length
|
||||
|
||||
static MAX_CHANNEL_NAME_LENGTH = 32; // 32 char max channel name length
|
||||
|
116
src/client/webapp/styles/components.scss
Normal file
116
src/client/webapp/styles/components.scss
Normal file
@ -0,0 +1,116 @@
|
||||
@import "theme.scss";
|
||||
|
||||
.text-input-react {
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
color: $interactive-normal;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
input {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
color: $text-normal;
|
||||
background-color: $background-input;
|
||||
border: 1px solid $border-input;
|
||||
border-radius: 3px;
|
||||
max-height: 100px;
|
||||
overflow-y: scroll;
|
||||
padding: 8px;
|
||||
|
||||
&:hover {
|
||||
border-color: $border-input-hover;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $border-input-focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.image-edit-input-react {
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
img.value {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modify {
|
||||
position: absolute;
|
||||
background-color: $brand;
|
||||
padding: 6px;
|
||||
border-radius: 16px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
|
||||
img.pencil {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.display {
|
||||
$content-border-radius: 4px;
|
||||
|
||||
flex: 1;
|
||||
border-top-right-radius: $content-border-radius;
|
||||
border-bottom-right-radius: $content-border-radius;
|
||||
background-color: $background-primary;
|
||||
position: relative;
|
||||
|
||||
> .scroll {
|
||||
margin: 32px;
|
||||
}
|
||||
|
||||
> .popup {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
.content {
|
||||
margin: 16px;
|
||||
padding: 12px 12px 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: $interactive-active;
|
||||
background-color: $background-popup-message;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
.button {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ body > .overlay,
|
||||
background-color: $background-overlay;
|
||||
|
||||
/* General Controls */
|
||||
input[type=text], .text-input {
|
||||
.text-input {
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
color: $text-normal;
|
||||
@ -37,14 +37,6 @@ body > .overlay,
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.75em;
|
||||
font-weight: bold;
|
||||
color: $interactive-normal;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
/* Popup Image */
|
||||
|
||||
> .content.popup-image {
|
||||
@ -370,59 +362,6 @@ body > .overlay,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .display {
|
||||
flex: 1;
|
||||
border-top-right-radius: $content-border-radius;
|
||||
border-bottom-right-radius: $content-border-radius;
|
||||
background-color: $background-primary;
|
||||
position: relative;
|
||||
|
||||
> .scroll {
|
||||
margin: 32px;
|
||||
}
|
||||
|
||||
> .popup {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
|
||||
&:not(.enabled) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin: 16px;
|
||||
padding: 12px 12px 12px 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: $interactive-active;
|
||||
background-color: $background-popup-message;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
|
||||
.button {
|
||||
padding: 8px 12px;
|
||||
font-size: 0.85em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
:not(:last-child) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* guild Settings Overlay */
|
||||
@ -434,35 +373,6 @@ body > .overlay,
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.image-input-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: relative;
|
||||
|
||||
> img {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.modify {
|
||||
position: absolute;
|
||||
background-color: $brand;
|
||||
padding: 6px;
|
||||
border-radius: 16px;
|
||||
right: -4px;
|
||||
bottom: -4px;
|
||||
|
||||
> img {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
@import "theme.scss";
|
||||
@import "fonts.scss";
|
||||
|
||||
@import "components.scss";
|
||||
|
||||
@import "buttons.scss";
|
||||
@import "channel-feed.scss";
|
||||
@import "channel-list.scss";
|
||||
|
Loading…
Reference in New Issue
Block a user