Complete GUI Refactor # Patch 1

This commit is contained in:
blessedcoolant
2022-03-30 12:45:34 +13:00
parent eea85b834e
commit b282421c98
23 changed files with 389 additions and 177 deletions

View File

@@ -6,27 +6,10 @@ import LandingPage from './components/LandingPage/LandingPage'
import { ThemeChanger, themeState } from './components/shared/ThemeChanger'
import Workspace from './components/Workspace'
import { fileState } from './store/Atoms'
import { keepGUIAlive } from './utils'
// Keeping GUI Window Open
async function getRequest(url = '') {
const response = await fetch(url, {
method: 'GET',
cache: 'no-cache',
})
return response.json()
}
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'production') {
document.addEventListener('DOMContentLoaded', function () {
const url = document.location
const route = '/flaskwebgui-keep-server-alive'
const intervalRequest = 3 * 1000
function keepAliveServer() {
getRequest(url + route).then(data => console.log(data))
}
setInterval(keepAliveServer, intervalRequest)
})
}
keepGUIAlive()
function App() {
const [file, setFile] = useRecoilState(fileState)
@@ -39,7 +22,8 @@ function App() {
}, [userInputImage, setFile])
// Dark Mode Hotkey
useKeyPressEvent('D', () => {
useKeyPressEvent('D', ev => {
ev?.preventDefault()
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
})

View File

@@ -22,12 +22,28 @@
.editor-canvas {
grid-area: editor-content;
z-index: 2;
}
.original-image-container {
grid-area: editor-content;
pointer-events: none;
animation: opacityReveal 350ms ease-in-out;
display: grid;
grid-template-areas: 'original-image-content';
img {
grid-area: original-image-content;
}
.editor-slider {
grid-area: original-image-content;
height: 100%;
width: 4px;
justify-self: end;
background-color: var(--yellow-accent);
transition: all 350ms ease-in-out;
z-index: 2;
}
}
.editor-canvas-loading {
@@ -40,13 +56,13 @@
bottom: 0;
padding: 1rem 4rem;
display: grid;
// grid-template-columns: repeat(4, max-content);
grid-template-areas: 'toolkit-image-type toolkit-size-selector toolkit-brush-slider toolkit-btns';
column-gap: 2rem;
align-items: center;
background-color: var(--editor-toolkit-bg);
backdrop-filter: blur(10px);
border-radius: 0.5rem 0.5rem 0 0;
animation: slideUp 0.2s ease-out;
@include mobile {
padding: 1rem 2rem;
@@ -68,9 +84,7 @@
column-gap: 1rem;
align-items: center;
input[type='range'] {
outline: none;
}
@include slider-bar;
}
.editor-toolkit-btns {
@@ -83,15 +97,11 @@
.brush-shape {
position: absolute;
border-radius: 50%;
background: rgb(255, 255, 255, 0.25);
background: rgba(255, 190, 0, 0.75);
border: 1px dashed var(--border-color);
pointer-events: none;
}
.editor-size-selector {
display: grid;
}
.editor-size-selector-options {
position: fixed;
display: grid;
@@ -103,31 +113,57 @@
grid-template-columns: repeat(2, max-content);
align-items: center;
column-gap: 0.5rem;
}
select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background: var(--yellow-accent);
outline: none;
border: 1px dashed var(--border-color);
.editor-size-selector-main {
@include accented-display(var(--yellow-accent));
display: grid;
grid-template-columns: repeat(2, max-content);
column-gap: 0.25rem;
align-items: center;
cursor: pointer;
outline: none;
svg {
width: 1rem;
height: 1rem;
margin-top: 0.25rem;
}
}
.editor-size-options {
@include accented-display(var(--btn-primary-bg));
padding: 0;
display: grid;
justify-self: center;
margin-left: 2.7rem;
position: fixed;
bottom: 4rem;
@include mobile {
bottom: 11.5rem;
margin-left: 2.9rem;
}
.editor-size-option {
padding: 0.2rem 0.8rem;
border-bottom: 1px dashed var(--border-color);
border-radius: 0.5rem;
font-family: 'WorkSans-Bold';
font-size: 1rem;
padding: 0.5rem;
text-align: center;
color: rgb(0, 0, 0);
&:last-of-type {
border-bottom: none;
}
&:hover {
background-color: var(--yellow-accent);
}
}
}
.image-type-tag {
@include accented-display(var(--yellow-accent));
grid-area: toolkit-image-type;
z-index: 2;
background-color: var(--yellow-accent);
padding: 0.5rem;
border-radius: 0.5rem;
width: 100px;
text-align: center;
font-family: 'WorkSans-Bold';
color: rgb(0, 0, 0);
}

View File

@@ -15,12 +15,7 @@ import {
TransformComponent,
TransformWrapper,
} from 'react-zoom-pan-pinch'
import {
useWindowSize,
useLocalStorage,
useKey,
useKeyPressEvent,
} from 'react-use'
import { useWindowSize, useKey, useKeyPressEvent } from 'react-use'
import inpaint from '../../adapters/inpainting'
import Button from '../shared/Button'
import Slider from './Slider'
@@ -28,7 +23,7 @@ import SizeSelector from './SizeSelector'
import { downloadImage, loadImage, useImage } from '../../utils'
const TOOLBAR_SIZE = 200
const BRUSH_COLOR = 'rgba(189, 255, 1, 0.75)'
const BRUSH_COLOR = 'rgba(255, 190, 0, 0.65)'
// const NO_COLOR = 'rgba(255,255,255,0)'
interface EditorProps {
@@ -80,14 +75,15 @@ export default function Editor(props: EditorProps) {
const [isInpaintingLoading, setIsInpaintingLoading] = useState(false)
const [scale, setScale] = useState<number>(1)
const [minScale, setMinScale] = useState<number>()
// ['1080', '2000', 'Original']
const [sizeLimit, setSizeLimit] = useLocalStorage('sizeLimit', '1080')
const [sizeLimit, setSizeLimit] = useState<number>(1080)
const windowSize = useWindowSize()
const viewportRef = useRef<ReactZoomPanPinchRef | undefined | null>()
const [isDraging, setIsDraging] = useState(false)
const [isMultiStrokeKeyPressed, setIsMultiStrokeKeyPressed] = useState(false)
const [sliderPos, setSliderPos] = useState<number>(0)
const draw = useCallback(() => {
if (!context) {
return
@@ -126,11 +122,14 @@ export default function Editor(props: EditorProps) {
setIsInpaintingLoading(true)
refreshCanvasMask()
try {
const res = await inpaint(file, maskCanvas.toDataURL(), sizeLimit)
const res = await inpaint(
file,
maskCanvas.toDataURL(),
sizeLimit.toString()
)
if (!res) {
throw new Error('empty response')
}
// TODO: fix the render if it failed loading
const newRender = new Image()
await loadImage(newRender, res)
renders.push(newRender)
@@ -231,6 +230,9 @@ export default function Editor(props: EditorProps) {
setMinScale(1)
}
const imageSizeLimit = Math.max(original.width, original.height)
setSizeLimit(imageSizeLimit)
if (context?.canvas) {
context.canvas.width = original.naturalWidth
context.canvas.height = original.naturalHeight
@@ -239,6 +241,17 @@ export default function Editor(props: EditorProps) {
}
}, [context?.canvas, draw, original, isOriginalLoaded, windowSize])
useEffect(() => {
window.addEventListener('resize', () => {
resetZoom()
})
return () => {
window.removeEventListener('resize', () => {
resetZoom()
})
}
}, [windowSize])
// Zoom reset
const resetZoom = useCallback(() => {
if (!minScale || !original || !windowSize) {
@@ -410,14 +423,22 @@ export default function Editor(props: EditorProps) {
ev?.preventDefault()
ev?.stopPropagation()
if (hadRunInpainting()) {
setShowOriginal(true)
setShowOriginal(() => {
window.setTimeout(() => {
setSliderPos(100)
}, 10)
return true
})
}
},
ev => {
ev?.preventDefault()
ev?.stopPropagation()
if (hadRunInpainting()) {
setShowOriginal(false)
setSliderPos(0)
window.setTimeout(() => {
setShowOriginal(false)
}, 350)
}
}
)
@@ -427,7 +448,7 @@ export default function Editor(props: EditorProps) {
const currRender = renders[renders.length - 1]
downloadImage(currRender.currentSrc, name)
}
const onSizeLimitChange = (_sizeLimit: string) => {
const onSizeLimitChange = (_sizeLimit: number) => {
setSizeLimit(_sizeLimit)
}
@@ -538,7 +559,11 @@ export default function Editor(props: EditorProps) {
<div className="editor-canvas-container">
<canvas
className="editor-canvas"
style={{ cursor: getCursor() }}
style={{
cursor: getCursor(),
clipPath: `inset(0 ${sliderPos}% 0 0)`,
transition: 'clip-path 350ms ease-in-out',
}}
onContextMenu={e => {
e.preventDefault()
}}
@@ -556,25 +581,32 @@ export default function Editor(props: EditorProps) {
}
}}
/>
{showOriginal ? (
<div
className="original-image-container"
<div
className="original-image-container"
style={{
width: `${original.naturalWidth}px`,
height: `${original.naturalHeight}px`,
}}
>
{showOriginal && (
<div
className="editor-slider"
style={{
marginRight: `${sliderPos}%`,
}}
/>
)}
<img
className="original-image"
src={original.src}
alt="original"
style={{
width: `${original.naturalWidth}px`,
height: `${original.naturalHeight}px`,
}}
>
<img
className="original-image"
src={original.src}
alt="original"
style={{
width: `${original.naturalWidth}px`,
height: `${original.naturalHeight}px`,
}}
/>
</div>
) : null}
/>
</div>
</div>
</TransformComponent>
</TransformWrapper>
@@ -588,7 +620,6 @@ export default function Editor(props: EditorProps) {
{showOriginal ? 'Original' : 'Inpainted'}
</p>
<SizeSelector
value={sizeLimit || '1080'}
onChange={onSizeLimitChange}
originalWidth={original.naturalWidth}
originalHeight={original.naturalHeight}
@@ -628,10 +659,18 @@ export default function Editor(props: EditorProps) {
icon={<EyeIcon />}
onDown={ev => {
ev.preventDefault()
setShowOriginal(true)
setShowOriginal(() => {
window.setTimeout(() => {
setSliderPos(100)
}, 10)
return true
})
}}
onUp={() => {
setShowOriginal(false)
setSliderPos(0)
window.setTimeout(() => {
setShowOriginal(false)
}, 350)
}}
disabled={renders.length === 0}
>

View File

@@ -1,77 +1,103 @@
import React, { FocusEvent, useCallback, useRef } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import ChevronDoubleDownIcon from '@heroicons/react/solid/ChevronDoubleDownIcon'
import { useClickAway } from 'react-use'
const sizes = ['720', '1080', '2000', 'Original']
type SizeSelectorProps = {
value: string
originalWidth: number
originalHeight: number
onChange: (value: string) => void
onChange: (value: number) => void
}
export default function SizeSelector(props: SizeSelectorProps) {
const { value, originalHeight, originalWidth, onChange } = props
const selectRef = useRef()
const { originalHeight, originalWidth, onChange } = props
const [showOptions, setShowOptions] = useState<boolean>(false)
const sizeSelectorRef = useRef(null)
const [activeSize, setActiveSize] = useState<string>('Original')
const longSide: number = Math.max(originalWidth, originalHeight)
const getSizeShowName = (size: string) => {
if (size === 'Original') {
return `${originalWidth}x${originalHeight}`
}
const length: number = parseInt(size, 10)
const longSide: number =
originalWidth > originalHeight ? originalWidth : originalHeight
const scale = length / longSide
if (originalWidth > originalHeight) {
const newHeight = Math.ceil(scale * originalHeight)
return `${size}x${newHeight}`
}
const newWidth = Math.ceil(scale * originalWidth)
return `${newWidth}x${size}`
}
const onButtonFocus = (e: FocusEvent<any>) => {
e.currentTarget.blur()
}
const getValidSizes = useCallback((): string[] => {
const longSide: number =
originalWidth > originalHeight ? originalWidth : originalHeight
const validSizes = []
const getValidSizes = useCallback(() => {
const validSizes: string[] = []
for (let i = 0; i < sizes.length; i += 1) {
const s = sizes[i]
if (s === 'Original') {
validSizes.push(s)
} else if (parseInt(s, 10) <= longSide) {
validSizes.push(s)
if (sizes[i] === 'Original') {
validSizes.push(sizes[i])
}
if (parseInt(sizes[i], 10) < longSide) {
validSizes.push(sizes[i])
}
}
return validSizes
}, [originalHeight, originalWidth])
}, [longSide])
const getValidSize = useCallback(() => {
if (getValidSizes().indexOf(value) === -1) {
return getValidSizes()[0]
}
return value
}, [value, getValidSizes])
const getSizeShowName = useCallback(
(size: string) => {
if (size === 'Original') {
return `${originalWidth}x${originalHeight}`
}
const scale = parseInt(size, 10) / longSide
if (originalWidth > originalHeight) {
const newHeight = Math.ceil(originalHeight * scale)
return `${size}x${newHeight}`
}
const newWidth = Math.ceil(originalWidth * scale)
return `${newWidth}x${size}`
},
[originalWidth, originalHeight, longSide]
)
const showOptionsHandler = () => {
setShowOptions(currentShowOptionsState => !currentShowOptionsState)
}
useClickAway(sizeSelectorRef, () => {
setShowOptions(false)
})
const sizeChangeHandler = (e: any) => {
onChange(e.target.value)
e.target.blur()
const currentRes = e.target.textContent.split('x')
if (originalWidth > originalHeight) {
setActiveSize(currentRes[0])
onChange(currentRes[0])
} else {
setActiveSize(currentRes[1])
onChange(currentRes[1])
}
setShowOptions(!showOptions)
}
return (
<div className="editor-size-selector">
<div className="editor-size-selector" ref={sizeSelectorRef}>
<p>Size:</p>
<select value={getValidSize()} onChange={sizeChangeHandler}>
{getValidSizes().map(size => (
<option key={size} value={size}>
{getSizeShowName(size)}
</option>
))}
</select>
<div
className="editor-size-selector-main"
role="button"
tabIndex={0}
onClick={showOptionsHandler}
aria-hidden="true"
>
<p>{getSizeShowName(activeSize.toString())}</p>
<div className="editor-size-selector-chevron">
<ChevronDoubleDownIcon />
</div>
</div>
{showOptions && (
<div className="editor-size-options">
{getValidSizes().map(size => (
<div
className="editor-size-option"
role="button"
tabIndex={0}
key={size}
onClick={sizeChangeHandler}
aria-hidden="true"
>
{getSizeShowName(size)}
</div>
))}
</div>
)}
</div>
)
}

View File

@@ -14,7 +14,7 @@ const Header = () => {
return (
<header>
<Button
icon={<ArrowLeftIcon className="w-6 h-6" />}
icon={<ArrowLeftIcon />}
onClick={() => {
setFile(undefined)
}}

View File

@@ -1,8 +1,18 @@
@use '../../styles/Mixins/' as *;
.modal-shortcuts {
grid-area: main-content;
background-color: var(--modal-bg);
color: var(--modal-text-color);
box-shadow: 0px 0px 20px rgb(0, 0, 40, 0.2);
@include mobile {
display: grid;
width: 100%;
height: auto;
margin-top: -11rem;
animation: slideDown 0.2s ease-out;
}
}
.shortcut-options {
@@ -14,6 +24,12 @@
grid-template-columns: repeat(2, auto);
column-gap: 6rem;
align-items: center;
@include mobile {
grid-template-columns: auto;
column-gap: 0;
row-gap: 0.6rem;
}
}
.shortcut-key {
@@ -22,11 +38,21 @@
padding: 0.4rem 1rem;
width: max-content;
border-radius: 0.4rem;
@include mobile {
padding: 0.2rem 0.4rem;
}
}
.shortcut-description {
justify-self: end;
text-align: right;
width: 15rem;
@include mobile {
text-align: left;
width: auto;
justify-self: start;
}
}
}

View File

@@ -13,7 +13,8 @@ const Shortcuts = () => {
})
}
useKeyPressEvent('h', () => {
useKeyPressEvent('h', ev => {
ev?.preventDefault()
shortcutStateHandler()
})

View File

@@ -1,29 +1,18 @@
.theme-changer {
border: none;
border-radius: 50%;
width: 30px;
height: 30px;
background: transparent;
box-shadow: inset 4px 10px 0px rgb(80, 80, 80);
transform: rotate(-75deg);
transition: all 0.2s ease-in;
margin: 1rem;
.theme-toggle-ui {
position: absolute;
right: 1rem;
top: 0.25rem;
right: 2.5rem;
top: 1rem;
z-index: 10;
outline: none;
transition: all 0.2s ease-in;
user-select: none;
&:hover {
.theme-btn {
cursor: pointer;
}
}
[data-theme='dark'] {
.theme-changer {
background: rgb(255, 190, 0);
box-shadow: none;
transform: rotate(-75deg);
outline: none;
svg {
width: 36px;
height: 36px;
}
}
}

View File

@@ -1,5 +1,6 @@
import React from 'react'
import { atom, useRecoilState } from 'recoil'
import { SunIcon, MoonIcon } from '@heroicons/react/outline'
export const themeState = atom({
key: 'themeState',
@@ -15,11 +16,20 @@ export const ThemeChanger = () => {
}
return (
<button
type="button"
className="theme-changer"
onClick={themeSwitchHandler}
aria-label="Switch Theme"
/>
<div className="theme-toggle-ui">
<div
className="theme-btn"
onClick={themeSwitchHandler}
role="button"
tabIndex={0}
aria-hidden="true"
>
{theme === 'light' ? (
<MoonIcon />
) : (
<SunIcon style={{ color: 'rgb(255, 190, 0)' }} />
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,57 @@
@mixin accented-display($bg-color) {
background: $bg-color;
color: rgb(0, 0, 0);
font-family: 'WorkSans-Bold';
padding: 0.5rem;
border-radius: 0.5rem;
}
@mixin slider-bar {
input[type='range'] {
-webkit-appearance: none;
appearance: none;
width: 100%;
cursor: pointer;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
}
input[type='range']:focus {
outline: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
height: 1.2rem;
width: 1.2rem;
border-radius: 50%;
border: 2px solid rgb(0, 0, 0);
z-index: 2;
background: var(--yellow-accent);
margin-top: -0.5rem;
}
input[type='range']::-webkit-slider-runnable-track {
border-radius: 2rem;
height: 0.2rem;
background: var(--btn-primary-bg);
}
input[type='range']::-moz-range-track {
border-radius: 2rem;
background: var(--btn-primary-bg);
}
input[type='range']::-ms-fill-lower {
background-color: red;
}
input[type='range']::-moz-range-progress {
background: var(--yellow-accent);
}
}

View File

@@ -19,3 +19,21 @@
opacity: 1;
}
}
@keyframes slideDown {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@keyframes slideUp {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}

View File

@@ -9,7 +9,7 @@
--border-color: rgb(100, 100, 120);
// Editor
--editor-toolkit-bg: rgb(240, 240, 250, 0.15);
--editor-toolkit-bg: rgb(240, 240, 250, 0.5);
// Modal
--modal-bg: var(--page-bg);

View File

@@ -9,7 +9,7 @@
--border-color: rgb(100, 100, 120);
// Editor
--editor-toolkit-bg: rgb(20, 20, 30, 0.15);
--editor-toolkit-bg: rgb(20, 20, 30, 0.5);
// Modal
--modal-bg: var(--page-bg);

View File

@@ -156,3 +156,29 @@ export function resizeImageFile(
reader.readAsDataURL(file)
})
}
export function keepGUIAlive() {
async function getRequest(url = '') {
const response = await fetch(url, {
method: 'GET',
cache: 'no-cache',
})
return response.json()
}
const keepAliveServer = () => {
const url = document.location
const route = '/flaskwebgui-keep-server-alive'
getRequest(url + route).then(data => {
return data
})
}
if (!process.env.NODE_ENV || process.env.NODE_ENV === 'production') {
document.addEventListener('DOMContentLoaded', () => {
const intervalRequest = 3 * 1000
keepAliveServer()
setInterval(keepAliveServer, intervalRequest)
})
}
}