Merge pull request #30 from Sanster/PR25

PR25 + style refine
This commit is contained in:
Qing 2022-04-06 23:05:30 +08:00 committed by GitHub
commit 3fc0008f0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 1415 additions and 1244 deletions

View File

@ -55,7 +55,7 @@ Frontend code are modified from [cleanup.pictures](https://github.com/initml/cle
great online services [here](https://cleanup.pictures/).
- Install dependencies:`cd lama_cleaner/app/ && yarn`
- Start development server: `yarn dev`
- Start development server: `yarn start`
- Build: `yarn build`
## Docker

View File

@ -1,16 +1,17 @@
{
"files": {
"main.css": "/static/css/main.1144a0ea.chunk.css",
"main.js": "/static/js/main.98890b3e.chunk.js",
"main.css": "/static/css/main.ba23cb34.chunk.css",
"main.js": "/static/js/main.03e0a3ad.chunk.js",
"runtime-main.js": "/static/js/runtime-main.5e86ac81.js",
"static/js/2.d3149f41.chunk.js": "/static/js/2.d3149f41.chunk.js",
"static/js/2.8c938027.chunk.js": "/static/js/2.8c938027.chunk.js",
"index.html": "/index.html",
"static/js/2.d3149f41.chunk.js.LICENSE.txt": "/static/js/2.d3149f41.chunk.js.LICENSE.txt"
"static/js/2.8c938027.chunk.js.LICENSE.txt": "/static/js/2.8c938027.chunk.js.LICENSE.txt",
"static/media/_index.scss": "/static/media/WorkSans-SemiBold.1e98db4e.ttf"
},
"entrypoints": [
"static/js/runtime-main.5e86ac81.js",
"static/js/2.d3149f41.chunk.js",
"static/css/main.1144a0ea.chunk.css",
"static/js/main.98890b3e.chunk.js"
"static/js/2.8c938027.chunk.js",
"static/css/main.ba23cb34.chunk.css",
"static/js/main.03e0a3ad.chunk.js"
]
}

View File

@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/><meta name="theme-color" content="#ffffff"/><title>lama-cleaner - Image inpainting powered by LaMa</title><link href="/static/css/main.1144a0ea.chunk.css" rel="stylesheet"></head><body class="h-screen"><noscript>You need to enable JavaScript to run this app.</noscript><div id="root" class="h-full"></div><script>"localhost"===location.hostname&&(self.FIREBASE_APPCHECK_DEBUG_TOKEN=!0)</script><script>!function(e){function r(r){for(var n,l,a=r[0],f=r[1],i=r[2],p=0,s=[];p<a.length;p++)l=a[p],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonplama-cleaner"]=this["webpackJsonplama-cleaner"]||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var i=0;i<a.length;i++)r(a[i]);var c=f;t()}([])</script><script src="/static/js/2.d3149f41.chunk.js"></script><script src="/static/js/main.98890b3e.chunk.js"></script></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"/><meta name="theme-color" content="#ffffff"/><title>lama-cleaner - Image inpainting powered by LaMa</title><link href="/static/css/main.ba23cb34.chunk.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>"localhost"===location.hostname&&(self.FIREBASE_APPCHECK_DEBUG_TOKEN=!0)</script><script>!function(e){function r(r){for(var n,l,a=r[0],f=r[1],i=r[2],p=0,s=[];p<a.length;p++)l=a[p],Object.prototype.hasOwnProperty.call(o,l)&&o[l]&&s.push(o[l][0]),o[l]=0;for(n in f)Object.prototype.hasOwnProperty.call(f,n)&&(e[n]=f[n]);for(c&&c(r);s.length;)s.shift()();return u.push.apply(u,i||[]),t()}function t(){for(var e,r=0;r<u.length;r++){for(var t=u[r],n=!0,a=1;a<t.length;a++){var f=t[a];0!==o[f]&&(n=!1)}n&&(u.splice(r--,1),e=l(l.s=t[0]))}return e}var n={},o={1:0},u=[];function l(r){if(n[r])return n[r].exports;var t=n[r]={i:r,l:!1,exports:{}};return e[r].call(t.exports,t,t.exports,l),t.l=!0,t.exports}l.m=e,l.c=n,l.d=function(e,r,t){l.o(e,r)||Object.defineProperty(e,r,{enumerable:!0,get:t})},l.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},l.t=function(e,r){if(1&r&&(e=l(e)),8&r)return e;if(4&r&&"object"==typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(l.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&r&&"string"!=typeof e)for(var n in e)l.d(t,n,function(r){return e[r]}.bind(null,n));return t},l.n=function(e){var r=e&&e.__esModule?function(){return e.default}:function(){return e};return l.d(r,"a",r),r},l.o=function(e,r){return Object.prototype.hasOwnProperty.call(e,r)},l.p="/";var a=this["webpackJsonplama-cleaner"]=this["webpackJsonplama-cleaner"]||[],f=a.push.bind(a);a.push=r,a=a.slice();for(var i=0;i<a.length;i++)r(a[i]);var c=f;t()}([])</script><script src="/static/js/2.8c938027.chunk.js"></script><script src="/static/js/main.03e0a3ad.chunk.js"></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,6 @@
"private": true,
"proxy": "http://localhost:8080",
"dependencies": {
"@headlessui/react": "^1.4.2",
"@heroicons/react": "^1.0.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.2",
@ -13,30 +12,21 @@
"@types/node": "^16.11.1",
"@types/react": "^17.0.30",
"@types/react-dom": "^17.0.9",
"autoprefixer": "10.x",
"cross-env": "7.x",
"delay-cli": "^1.1.0",
"npm-run-all": "4.x",
"postcss": "8.x",
"postcss-cli": "8.x",
"postcss-preset-env": "6.x",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "4.0.3",
"react-use": "^17.3.1",
"react-zoom-pan-pinch": "^2.1.3",
"tailwindcss": "2.x",
"recoil": "^0.6.1",
"typescript": "4.x"
},
"scripts": {
"dev": "run-p watch:css react-scripts:start",
"build": "run-s build:css react-scripts:build",
"start": "react-scripts start",
"build": "cross-env GENERATE_SOURCEMAP=false react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"build:css": "cross-env TAILWIND_MODE=build NODE_ENV=production postcss src/styles/tailwind.css -o src/styles/index.css",
"watch:css": "cross-env TAILWIND_MODE=watch NODE_ENV=development postcss src/styles/tailwind.css -o src/styles/index.css --watch",
"react-scripts:start": "delay 5 && react-scripts start",
"react-scripts:build": "cross-env GENERATE_SOURCEMAP=false react-scripts build"
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
@ -62,6 +52,7 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.26.1",
"eslint-plugin-react-hooks": "^4.2.0",
"prettier": "^2.4.1"
"prettier": "^2.4.1",
"sass": "^1.49.9"
}
}

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: [
require('tailwindcss'),
require('postcss-preset-env'),
],
};

View File

@ -19,9 +19,9 @@
-->
<title>lama-cleaner - Image inpainting powered by LaMa</title>
</head>
<body class="h-screen">
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root" class="h-full"></div>
<div id="root"></div>
<script>
if (location.hostname === 'localhost') {
self.FIREBASE_APPCHECK_DEBUG_TOKEN = true

View File

@ -1,133 +1,38 @@
import { ArrowLeftIcon } from '@heroicons/react/outline'
import React, { useEffect, useState } from 'react'
import { useToggle, useWindowSize } from 'react-use'
import Button from './components/Button'
import FileSelect from './components/FileSelect'
import React, { useEffect } from 'react'
import { useKeyPressEvent } from 'react-use'
import { useRecoilState } from 'recoil'
import useInputImage from './hooks/useInputImage'
import ShortcutsModal from './components/ShortcutsModal'
import Editor from './Editor'
import LandingPage from './components/LandingPage/LandingPage'
import { themeState } from './components/Header/ThemeChanger'
import Workspace from './components/Workspace'
import { fileState } from './store/Atoms'
import { keepGUIAlive } from './utils'
import Header from './components/Header/Header'
// 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] = useState<File>()
const [showShortcuts, toggleShowShortcuts] = useToggle(false)
const windowSize = useWindowSize()
const [file, setFile] = useRecoilState(fileState)
const [theme, setTheme] = useRecoilState(themeState)
const userInputImage = useInputImage()
// Set Input Image
useEffect(() => {
setFile(userInputImage)
}, [userInputImage])
}, [userInputImage, setFile])
// Dark Mode Hotkey
useKeyPressEvent('D', ev => {
ev?.preventDefault()
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
})
return (
<div className="h-full full-visible-h-safari flex flex-col">
<header className="absolute z-10 flex w-full p-1 justify-center sm:justify-between items-center sm:items-start bg-white backdrop-blur backdrop-filter bg-opacity-30">
{file ? (
<Button
icon={<ArrowLeftIcon className="w-6 h-6" />}
onClick={() => {
setFile(undefined)
}}
>
{windowSize.width > 640 ? 'Start new' : undefined}
</Button>
) : (
<></>
)}
{file ? (
<Button
onClick={toggleShowShortcuts}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="28"
height="28"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 16 16"
>
<rect
x="0"
y="0"
width="16"
height="16"
fill="none"
stroke="none"
/>
<g fill="currentColor">
<path d="M14 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h12zM2 4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2z" />
<path d="M13 10.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm0-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5 0A.25.25 0 0 1 8.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 8 8.75v-.5zm2 0a.25.25 0 0 1 .25-.25h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5a.25.25 0 0 1-.25-.25v-.5zm1 2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5-2A.25.25 0 0 1 6.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 6 8.75v-.5zm-2 0A.25.25 0 0 1 4.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 4 8.75v-.5zm-2 0A.25.25 0 0 1 2.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 2 8.75v-.5zm11-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0A.25.25 0 0 1 9.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 9 6.75v-.5zm-2 0A.25.25 0 0 1 7.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 7 6.75v-.5zm-2 0A.25.25 0 0 1 5.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 5 6.75v-.5zm-3 0A.25.25 0 0 1 2.25 6h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5A.25.25 0 0 1 2 6.75v-.5zm0 4a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm2 0a.25.25 0 0 1 .25-.25h5.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-5.5a.25.25 0 0 1-.25-.25v-.5z" />
</g>
</svg>
}
/>
) : (
<></>
)}
</header>
{showShortcuts && <ShortcutsModal onClose={toggleShowShortcuts} />}
<main
className={[
'h-full flex flex-1 flex-col sm:items-center sm:justify-center overflow-hidden',
'items-center justify-center',
].join(' ')}
>
{file ? (
<Editor file={file} />
) : (
<>
<div
className={[
'flex flex-col sm:flex-row items-center',
'space-y-5 sm:space-y-0 sm:space-x-6 p-5 pt-0 pb-10',
].join(' ')}
>
<div className="max-w-xl flex flex-col items-center sm:items-start p-0 m-0 space-y-5">
<h1 className="text-center sm:text-left text-xl sm:text-3xl">
Image inpainting powered by 🦙
<u>
<a href="https://github.com/saic-mdal/lama">LaMa</a>
</u>
</h1>
</div>
</div>
<div
className="h-20 sm:h-52 px-4 w-full"
style={{ maxWidth: '800px' }}
>
<FileSelect
onSelection={async f => {
setFile(f)
}}
/>
</div>
</>
)}
</main>
<div className="lama-cleaner" data-theme={theme}>
<Header />
{file ? <Workspace file={file} /> : <LandingPage />}
</div>
)
}

View File

@ -0,0 +1,190 @@
@use '../../styles/Mixins' as *;
.editor-container {
grid-area: main-content;
display: grid;
width: 100vw;
height: 100vh;
place-items: center;
}
.react-transform-wrapper {
display: grid !important;
width: 100% !important;
height: 100% !important;
}
.editor-canvas-container {
display: grid;
grid-template-areas: 'editor-content';
row-gap: 1rem;
}
.editor-canvas {
grid-area: editor-content;
z-index: 2;
}
.original-image-container {
grid-area: editor-content;
pointer-events: none;
display: grid;
grid-template-areas: 'original-image-content';
img {
grid-area: original-image-content;
}
.editor-slider {
grid-area: original-image-content;
height: 100%;
width: 6px;
justify-self: end;
background-color: var(--yellow-accent);
transition: all 300ms cubic-bezier(0.4, 0, 0.2, 1);
z-index: 2;
}
}
.editor-canvas-loading {
pointer-events: none;
animation: pulsing 750ms infinite;
}
.editor-toolkit-panel {
// width: 100%;
position: fixed;
bottom: 0.5rem;
// border: 1px solid rgb(100, 100, 120, 0.5);
border-radius: 3rem;
padding: 1rem 3rem;
display: grid;
grid-template-areas: 'toolkit-size-selector toolkit-brush-slider toolkit-btns';
column-gap: 2rem;
align-items: center;
justify-content: center;
background-color: var(--editor-toolkit-bg);
backdrop-filter: blur(12px);
animation: slideUp 0.2s ease-out;
border: 1px solid rgb(100, 100, 120, 0.4);
@include mobile {
padding: 1rem 2rem;
grid-template-areas:
'toolkit-size-selector toolkit-size-selector'
'toolkit-brush-slider toolkit-brush-slider'
'toolkit-btns toolkit-btns';
row-gap: 2rem;
justify-items: center;
}
.eyeicon-active {
background-color: var(--yellow-accent);
color: var(--btn-text-hover-color);
}
}
.editor-brush-slider {
grid-area: toolkit-brush-slider;
user-select: none;
display: grid;
grid-template-columns: repeat(2, max-content);
height: max-content;
column-gap: 1rem;
align-items: center;
@include slider-bar;
}
.editor-toolkit-btns {
grid-area: toolkit-btns;
display: grid;
grid-auto-flow: column;
column-gap: 1rem;
}
.brush-shape {
position: absolute;
border-radius: 50%;
background-color: #ffcc00bb;
border: 1px solid var(--yellow-accent);
pointer-events: none;
}
.editor-size-selector-options {
position: fixed;
display: grid;
}
.editor-size-selector {
grid-area: toolkit-size-selector;
display: grid;
grid-template-columns: repeat(2, max-content);
align-items: center;
}
.editor-size-selector-main {
@include accented-display(var(white));
user-select: none;
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
outline: none;
gap: 8px;
width: 128px;
border: 1px solid var(--editor-size-border-color);
color: var(--options-text-color);
svg {
width: 1rem;
height: 1rem;
margin-top: 0.25rem;
}
}
.editor-size-options {
@include accented-display(var(--btn-primary-bg));
width: 128px;
padding: 0;
display: grid;
justify-self: center;
position: fixed;
bottom: 4rem;
cursor: pointer;
color: var(--options-text-color);
background-color: var(--page-bg);
border: 1px solid var(--editor-size-border-color);
border-radius: 0.6rem;
@include mobile {
bottom: 11.5rem;
}
.editor-size-option {
display: flex;
align-items: center;
height: 40px;
user-select: none;
padding: 0.2rem 0.8rem;
&:first-of-type {
border-top-right-radius: 0.5rem;
border-top-left-radius: 0.5rem;
}
&:last-of-type {
border-bottom-left-radius: 0.5rem;
border-bottom-right-radius: 0.5rem;
}
&:hover {
background-color: var(--yellow-accent);
color: var(--btn-text-hover-color);
}
}
}

View File

@ -15,21 +15,15 @@ import {
TransformComponent,
TransformWrapper,
} from 'react-zoom-pan-pinch'
import {
useWindowSize,
useLocalStorage,
useKey,
useKeyPressEvent,
} from 'react-use'
import inpaint from './adapters/inpainting'
import Button from './components/Button'
import Slider from './components/Slider'
import SizeSelector from './components/SizeSelector'
import { downloadImage, loadImage, useImage } from './utils'
import { useWindowSize, useKey, useKeyPressEvent } from 'react-use'
import inpaint from '../../adapters/inpainting'
import Button from '../shared/Button'
import Slider from './Slider'
import SizeSelector from './SizeSelector'
import { downloadImage, loadImage, useImage } from '../../utils'
const TOOLBAR_SIZE = 200
const BRUSH_COLOR = 'rgba(189, 255, 1, 0.75)'
// const NO_COLOR = 'rgba(255,255,255,0)'
const BRUSH_COLOR = '#ffcc00bb'
interface EditorProps {
file: File
@ -78,17 +72,17 @@ export default function Editor(props: EditorProps) {
const [isPanning, setIsPanning] = useState<boolean>(false)
const [showOriginal, setShowOriginal] = useState(false)
const [isInpaintingLoading, setIsInpaintingLoading] = useState(false)
const [showSeparator, setShowSeparator] = 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
@ -127,11 +121,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)
@ -232,6 +229,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
@ -240,6 +240,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) {
@ -411,16 +422,22 @@ export default function Editor(props: EditorProps) {
ev?.preventDefault()
ev?.stopPropagation()
if (hadRunInpainting()) {
setShowSeparator(true)
setShowOriginal(true)
setShowOriginal(() => {
window.setTimeout(() => {
setSliderPos(100)
}, 10)
return true
})
}
},
ev => {
ev?.preventDefault()
ev?.stopPropagation()
if (hadRunInpainting()) {
setShowOriginal(false)
setTimeout(() => setShowSeparator(false), 300)
setSliderPos(0)
window.setTimeout(() => {
setShowOriginal(false)
}, 350)
}
}
)
@ -430,8 +447,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)
}
@ -512,11 +528,7 @@ export default function Editor(props: EditorProps) {
return (
<div
className="flex flex-col items-center"
style={{
height: '100%',
width: '100%',
}}
className="editor-container"
aria-hidden="true"
onMouseMove={onMouseMove}
onMouseUp={onPointerUp}
@ -541,20 +553,16 @@ export default function Editor(props: EditorProps) {
}}
>
<TransformComponent
wrapperStyle={{
width: '100%',
height: '100%',
}}
contentClass={
isInpaintingLoading
? 'animate-pulse-fast pointer-events-none transition-opacity'
: ''
}
contentClass={isInpaintingLoading ? 'editor-canvas-loading' : ''}
>
<>
<div className="editor-canvas-container">
<canvas
className="rounded-sm"
style={{ cursor: getCursor() }}
className="editor-canvas"
style={{
cursor: getCursor(),
clipPath: `inset(0 ${sliderPos}% 0 0)`,
transition: 'clip-path 350ms ease-in-out',
}}
onContextMenu={e => {
e.preventDefault()
}}
@ -573,138 +581,105 @@ export default function Editor(props: EditorProps) {
}}
/>
<div
className={[
'absolute top-0 right-0 pointer-events-none',
'overflow-hidden',
'border-primary',
showSeparator ? 'border-l-4' : '',
].join(' ')}
className="original-image-container"
style={{
width: showOriginal
? `${Math.round(original.naturalWidth)}px`
: '0px',
height: original.naturalHeight,
transitionProperty: 'width, height',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
transitionDuration: '300ms',
width: `${original.naturalWidth}px`,
height: `${original.naturalHeight}px`,
}}
>
{showOriginal && (
<div
className="editor-slider"
style={{
marginRight: `${sliderPos}%`,
}}
/>
)}
<img
className="absolute right-0"
className="original-image"
src={original.src}
alt="original"
width={`${original.naturalWidth}px`}
height={`${original.naturalHeight}px`}
style={{
width: `${original.naturalWidth}px`,
height: `${original.naturalHeight}px`,
maxWidth: 'none',
}}
/>
</div>
</>
</div>
</TransformComponent>
</TransformWrapper>
{showBrush && !isInpaintingLoading && !isPanning && (
<div
className="hidden sm:block absolute rounded-full border border-primary bg-primary bg-opacity-80 pointer-events-none"
style={getBrushStyle()}
/>
<div className="brush-shape" style={getBrushStyle()} />
)}
<div
className="fixed w-full bottom-0 flex items-center justify-center"
style={{ height: '90px' }}
>
<div
className={[
'flex items-center justify-center space-x-6',
'',
// 'bg-black backdrop-blur backdrop-filter bg-opacity-10',
].join(' ')}
>
<SizeSelector
value={sizeLimit || '1080'}
onChange={onSizeLimitChange}
originalWidth={original.naturalWidth}
originalHeight={original.naturalHeight}
<div className="editor-toolkit-panel">
<SizeSelector
onChange={onSizeLimitChange}
originalWidth={original.naturalWidth}
originalHeight={original.naturalHeight}
/>
<Slider
label="Brush"
min={10}
max={150}
value={brushSize}
onChange={setBrushSize}
/>
<div className="editor-toolkit-btns">
<Button
icon={<ArrowsExpandIcon />}
disabled={scale === minScale}
onClick={resetZoom}
/>
<Slider
label={
<span>
<span className="hidden md:inline">Brush</span>
</span>
<Button
icon={
<svg
width="19"
height="9"
viewBox="0 0 19 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1H2ZM1 8H0V9H1V8ZM8 9C8.55228 9 9 8.55229 9 8C9 7.44771 8.55228 7 8 7V9ZM16.5963 7.42809C16.8327 7.92721 17.429 8.14016 17.9281 7.90374C18.4272 7.66731 18.6402 7.07103 18.4037 6.57191L16.5963 7.42809ZM16.9468 5.83205L17.8505 5.40396L16.9468 5.83205ZM0 1V8H2V1H0ZM1 9H8V7H1V9ZM1.66896 8.74329L6.66896 4.24329L5.33104 2.75671L0.331035 7.25671L1.66896 8.74329ZM16.043 6.26014L16.5963 7.42809L18.4037 6.57191L17.8505 5.40396L16.043 6.26014ZM6.65079 4.25926C9.67554 1.66661 14.3376 2.65979 16.043 6.26014L17.8505 5.40396C15.5805 0.61182 9.37523 -0.710131 5.34921 2.74074L6.65079 4.25926Z"
fill="currentColor"
/>
</svg>
}
min={10}
max={150}
value={brushSize}
onChange={setBrushSize}
onClick={undo}
disabled={renders.length === 0}
/>
<div>
<Button
className="mr-2"
icon={<ArrowsExpandIcon className="w-6 h-6" />}
disabled={scale === minScale}
onClick={resetZoom}
/>
<Button
className="mr-2"
icon={
<svg
width="19"
height="9"
viewBox="0 0 19 9"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="w-6 h-6"
>
<path
d="M2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1H2ZM1 8H0V9H1V8ZM8 9C8.55228 9 9 8.55229 9 8C9 7.44771 8.55228 7 8 7V9ZM16.5963 7.42809C16.8327 7.92721 17.429 8.14016 17.9281 7.90374C18.4272 7.66731 18.6402 7.07103 18.4037 6.57191L16.5963 7.42809ZM16.9468 5.83205L17.8505 5.40396L16.9468 5.83205ZM0 1V8H2V1H0ZM1 9H8V7H1V9ZM1.66896 8.74329L6.66896 4.24329L5.33104 2.75671L0.331035 7.25671L1.66896 8.74329ZM16.043 6.26014L16.5963 7.42809L18.4037 6.57191L17.8505 5.40396L16.043 6.26014ZM6.65079 4.25926C9.67554 1.66661 14.3376 2.65979 16.043 6.26014L17.8505 5.40396C15.5805 0.61182 9.37523 -0.710131 5.34921 2.74074L6.65079 4.25926Z"
fill="currentColor"
/>
</svg>
}
onClick={undo}
disabled={renders.length === 0}
/>
<Button
className="mr-2"
icon={<EyeIcon className="w-6 h-6" />}
onDown={ev => {
ev.preventDefault()
setShowSeparator(true)
setShowOriginal(true)
}}
onUp={() => {
setShowOriginal(false)
setTimeout(() => setShowSeparator(false), 300)
}}
disabled={renders.length === 0}
>
{undefined}
</Button>
<Button
icon={<DownloadIcon className="w-6 h-6" />}
disabled={!renders.length}
onClick={download}
>
{undefined}
</Button>
</div>
<div
className="absolute bg-black backdrop-blur backdrop-filter bg-opacity-10 rounded-xl"
style={{
height: '58px',
width: '600px',
zIndex: -1,
marginLeft: '-1px',
<Button
icon={<EyeIcon />}
className={showOriginal ? 'eyeicon-active' : ''}
onDown={ev => {
ev.preventDefault()
setShowOriginal(() => {
window.setTimeout(() => {
setSliderPos(100)
}, 10)
return true
})
}}
onUp={() => {
setSliderPos(0)
window.setTimeout(() => {
setShowOriginal(false)
}, 350)
}}
disabled={renders.length === 0}
>
{undefined}
</div>
</Button>
<Button
icon={<DownloadIcon />}
disabled={!renders.length}
onClick={download}
>
{undefined}
</Button>
</div>
</div>
</div>

View File

@ -0,0 +1,102 @@
import React, { useCallback, useRef, useState } from 'react'
import { useClickAway } from 'react-use'
import { ChevronUpIcon } from '@heroicons/react/outline'
const sizes = ['720', '1080', '2000', 'Original']
type SizeSelectorProps = {
originalWidth: number
originalHeight: number
onChange: (value: number) => void
}
export default function SizeSelector(props: SizeSelectorProps) {
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 getValidSizes = useCallback(() => {
const validSizes: string[] = []
for (let i = 0; i < sizes.length; i += 1) {
if (sizes[i] === 'Original') {
validSizes.push(sizes[i])
}
if (parseInt(sizes[i], 10) < longSide) {
validSizes.push(sizes[i])
}
}
return validSizes
}, [longSide])
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) => {
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" ref={sizeSelectorRef}>
<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-icon">
<ChevronUpIcon />
</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,14 +14,9 @@ export default function Slider(props: SliderProps) {
const step = ((max || 100) - (min || 0)) / 100
return (
<div className="inline-flex items-center space-x-4 text-black">
<div className="editor-brush-slider">
<span>{label}</span>
<input
className={[
'appearance-none rounded-lg h-4',
'bg-primary',
'w-24 md:w-auto',
].join(' ')}
type="range"
step={step}
min={min}

View File

@ -0,0 +1,35 @@
@use '../../styles/Mixins/' as *;
.file-select-label {
display: grid;
cursor: pointer;
border: 2px dashed var(--border-color);
border-radius: 0.5rem;
min-width: 600px;
@include mobile {
min-width: 300px;
}
&:hover,
.file-select-label-hover {
color: black;
background-color: var(--yellow-accent);
}
}
.file-select-container {
display: grid;
padding: 4rem;
width: 100%;
height: 100%;
input {
display: none;
}
}
.file-select-message {
font-family: 'WorkSans';
text-align: center;
}

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react'
import useResolution from '../../hooks/useResolution'
type FileSelectProps = {
onSelection: (file: File) => void
@ -10,6 +11,8 @@ export default function FileSelect(props: FileSelectProps) {
const [dragHover, setDragHover] = useState(false)
const [uploadElemId] = useState(`file-upload-${Math.random().toString()}`)
const resolution = useResolution()
function onFileSelected(file: File) {
if (!file) {
return
@ -94,17 +97,11 @@ export default function FileSelect(props: FileSelectProps) {
}
return (
<label
htmlFor={uploadElemId}
className="block w-full h-full group relative cursor-pointer rounded-md font-medium focus-within:outline-none"
>
<label htmlFor={uploadElemId} className="file-select-label">
<div
className={[
'w-full h-full flex items-center justify-center px-6 pt-5 pb-6 text-md',
'border-2 border-dashed rounded-md',
'hover:border-black hover:bg-primary',
'text-center',
dragHover ? 'border-black bg-primary' : 'bg-gray-100 border-gray-300',
'file-select-container',
dragHover ? 'file-select-label-hover' : '',
].join(' ')}
onDrop={handleDrop}
onDragOver={ev => {
@ -118,7 +115,6 @@ export default function FileSelect(props: FileSelectProps) {
id={uploadElemId}
name={uploadElemId}
type="file"
className="sr-only"
onChange={ev => {
const file = ev.currentTarget.files?.[0]
if (file) {
@ -127,8 +123,11 @@ export default function FileSelect(props: FileSelectProps) {
}}
accept="image/png, image/jpeg"
/>
<p className="hidden sm:block">Click here or drag an image file</p>
<p className="sm:hidden">Tap here to load your picture</p>
<p className="file-select-message">
{resolution === 'desktop'
? 'Click here or drag an image file'
: 'Tap here to load your picture'}
</p>
</div>
</label>
)

View File

@ -0,0 +1,25 @@
header {
height: 60px;
padding: 1rem 2rem;
position: absolute;
top: 0;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
z-index: 20;
backdrop-filter: blur(12px);
border-bottom: 1px solid rgb(100, 100, 120, 0.2);
}
.shortcuts {
z-index: 1;
}
.header-icons-wrapper {
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
justify-self: end;
}

View File

@ -0,0 +1,40 @@
import { ArrowLeftIcon } from '@heroicons/react/outline'
import React from 'react'
import { useRecoilState } from 'recoil'
import { fileState } from '../../store/Atoms'
import Button from '../shared/Button'
import Shortcuts from '../Shortcuts/Shortcuts'
import useResolution from '../../hooks/useResolution'
import { ThemeChanger } from './ThemeChanger'
const Header = () => {
const [file, setFile] = useRecoilState(fileState)
const resolution = useResolution()
const renderHeader = () => {
return (
<header>
<div style={{ visibility: file ? 'visible' : 'hidden' }}>
<Button
icon={<ArrowLeftIcon />}
onClick={() => {
setFile(undefined)
}}
style={{ border: 0 }}
>
{resolution === 'desktop' ? 'Start New' : undefined}
</Button>
</div>
<div className="header-icons-wrapper">
<div style={{ visibility: file ? 'visible' : 'hidden' }}>
<Shortcuts />
</div>
<ThemeChanger />
</div>
</header>
)
}
return renderHeader()
}
export default Header

View File

@ -0,0 +1,18 @@
.theme-toggle-ui {
z-index: 10;
transition: all 0.2s ease-in;
user-select: none;
.theme-btn {
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
outline: none;
svg {
width: 22px;
height: 22px;
}
}
}

View File

@ -0,0 +1,44 @@
import React, { useEffect } from 'react'
import { atom, useRecoilState } from 'recoil'
import { SunIcon, MoonIcon } from '@heroicons/react/outline'
export const themeState = atom({
key: 'themeState',
default: 'light',
})
export const ThemeChanger = () => {
const [theme, setTheme] = useRecoilState(themeState)
useEffect(() => {
const darkThemeMq = window.matchMedia('(prefers-color-scheme: dark)')
if (darkThemeMq.matches) {
setTheme('dark')
} else {
setTheme('light')
}
}, [])
const themeSwitchHandler = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
}
return (
<div className="theme-toggle-ui">
<div
className="theme-btn"
onClick={themeSwitchHandler}
role="button"
tabIndex={0}
aria-hidden="true"
>
{theme === 'light' ? (
<MoonIcon />
) : (
<SunIcon style={{ color: '#ffcc00' }} />
)}
</div>
</div>
)
}

View File

@ -0,0 +1,30 @@
@use '../../styles/Mixins/' as *;
.landing-page {
display: grid;
place-self: center;
justify-items: center;
row-gap: 2rem;
grid-auto-rows: max-content;
@include mobile {
padding: 1rem;
}
h1 {
text-align: center;
font-size: 1.4rem;
@include mobile {
font-size: 1.2rem;
}
}
a {
color: var(--link-color);
}
}
.landing-file-selector {
display: grid;
}

View File

@ -0,0 +1,26 @@
import React from 'react'
import { useSetRecoilState } from 'recoil'
import { fileState } from '../../store/Atoms'
import FileSelect from '../FileSelect/FileSelect'
const LandingPage = () => {
const setFile = useSetRecoilState(fileState)
return (
<div className="landing-page">
<h1>
Image inpainting powered by 🦙
<a href="https://github.com/saic-mdal/lama">LaMa</a>
</h1>
<div className="landing-file-selector">
<FileSelect
onSelection={async f => {
setFile(f)
}}
/>
</div>
</div>
)
}
export default LandingPage

View File

@ -1,74 +0,0 @@
import React from 'react'
export default function MadeWidthBadge() {
return (
<svg
width="101"
height="43"
viewBox="0 0 101 43"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.158081 28.5402C0.158081 26.12 2.07738 24.1581 4.44495 24.1581H5.62173C6.08596 24.1581 6.46229 24.5428 6.46229 25.0173V25.7907C6.46229 26.2652 6.08596 26.6499 5.62173 26.6499H4.44495C3.47006 26.6499 2.4276 27.5437 2.4276 28.5402V29.7432C2.4276 30.2177 2.05127 30.6024 1.58704 30.6024H0.998643C0.534413 30.6024 0.158081 30.2177 0.158081 29.7432L0.158081 28.5402Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.6244 35.2423C15.6244 37.6625 13.7051 39.6244 11.3376 39.6244H10.1608C9.69654 39.6244 9.3202 39.2397 9.3202 38.7652V37.9919C9.3202 37.5173 9.69654 37.1326 10.1608 37.1326H11.3376C12.3124 37.1326 13.3549 36.2388 13.3549 35.2423V34.0394C13.3549 33.5648 13.7312 33.1801 14.1955 33.1801H14.7839C15.2481 33.1801 15.6244 33.5648 15.6244 34.0394V35.2423Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M11.3376 24.1581C13.7051 24.1581 15.6244 26.12 15.6244 28.5402V29.7432C15.6244 30.2177 15.2481 30.6024 14.7839 30.6024H14.0274C13.5631 30.6024 13.1868 30.2177 13.1868 29.7432V28.5402C13.1868 27.5437 12.3124 26.478 11.3376 26.478H10.1608C9.69654 26.478 9.3202 26.0934 9.3202 25.6188V25.0173C9.3202 24.5428 9.69654 24.1581 10.1608 24.1581L11.3376 24.1581Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4.44495 39.6244C2.07738 39.6244 0.158081 37.6625 0.158081 35.2423L0.158082 34.0394C0.158082 33.5648 0.534414 33.1801 0.998643 33.1801H1.75515C2.21938 33.1801 2.59571 33.5648 2.59571 34.0394L2.59571 35.2423C2.59571 36.2388 3.47006 37.3045 4.44495 37.3045L5.62173 37.3045C6.08596 37.3045 6.46229 37.6892 6.46229 38.1637V38.7652C6.46229 39.2397 6.08596 39.6244 5.62173 39.6244L4.44495 39.6244Z"
fill="black"
/>
<path
d="M36.6965 29.5082C36.289 26.5653 34.0253 24.8578 31.0888 24.8578C27.6285 24.8578 25.0219 27.3802 25.0219 31.662C25.0219 35.9373 27.5961 38.4663 31.0888 38.4663C34.2387 38.4663 36.3279 36.4289 36.6965 33.8999L34.2775 33.887C33.9735 35.4393 32.7058 36.3125 31.1212 36.3125C28.9738 36.3125 27.4345 34.7019 27.4345 31.662C27.4345 28.6738 28.9609 27.0116 31.1276 27.0116C32.7382 27.0116 33.9994 27.9236 34.2775 29.5082H36.6965Z"
fill="black"
/>
<path
d="M41.1319 25.0389H38.7905V38.2852H41.1319V25.0389Z"
fill="black"
/>
<path
d="M43.5396 38.2852H45.881V28.3504H43.5396V38.2852ZM44.7168 26.9404C45.4606 26.9404 46.0686 26.3712 46.0686 25.6727C46.0686 24.9677 45.4606 24.3985 44.7168 24.3985C43.9665 24.3985 43.3585 24.9677 43.3585 25.6727C43.3585 26.3712 43.9665 26.9404 44.7168 26.9404Z"
fill="black"
/>
<path
d="M48.2887 42.0107H50.6301V36.7199H50.7271C51.0958 37.4443 51.8654 38.4598 53.573 38.4598C55.9144 38.4598 57.6672 36.6035 57.6672 33.3307C57.6672 30.0192 55.8626 28.2211 53.5665 28.2211C51.8137 28.2211 51.0828 29.2754 50.7271 29.9933H50.5913V28.3504H48.2887V42.0107ZM50.5848 33.3178C50.5848 31.3904 51.4127 30.1421 52.9197 30.1421C54.4785 30.1421 55.2805 31.468 55.2805 33.3178C55.2805 35.1806 54.4656 36.5388 52.9197 36.5388C51.4256 36.5388 50.5848 35.2453 50.5848 33.3178Z"
fill="black"
/>
<path
d="M64.2645 38.2852C68.3005 38.2852 70.6936 35.7886 70.6936 31.6491C70.6936 27.5225 68.3005 25.0389 64.355 25.0389H59.7757V38.2852H64.2645ZM62.1753 36.209V27.1151H64.2192C66.9099 27.1151 68.3134 28.6156 68.3134 31.6491C68.3134 34.6955 66.9099 36.209 64.1481 36.209H62.1753Z"
fill="black"
/>
<path
d="M72.8701 38.2852H75.2115V32.4446C75.2115 31.1834 76.1622 30.2908 77.4494 30.2908C77.8439 30.2908 78.3355 30.362 78.536 30.4266V28.2728C78.3225 28.234 77.9539 28.2081 77.6951 28.2081C76.5568 28.2081 75.606 28.8549 75.2438 30.0062H75.1403V28.3504H72.8701V38.2852Z"
fill="black"
/>
<path
d="M84.0159 38.4792C86.9265 38.4792 88.7763 36.4289 88.7763 33.3566C88.7763 30.2779 86.9265 28.2211 84.0159 28.2211C81.1054 28.2211 79.2555 30.2779 79.2555 33.3566C79.2555 36.4289 81.1054 38.4792 84.0159 38.4792ZM84.0289 36.6035C82.4183 36.6035 81.6293 35.1676 81.6293 33.3501C81.6293 31.5327 82.4183 30.0774 84.0289 30.0774C85.6135 30.0774 86.4026 31.5327 86.4026 33.3501C86.4026 35.1676 85.6135 36.6035 84.0289 36.6035Z"
fill="black"
/>
<path
d="M90.7636 42.0107H93.105V36.7199H93.202C93.5707 37.4443 94.3404 38.4598 96.0479 38.4598C98.3893 38.4598 100.142 36.6035 100.142 33.3307C100.142 30.0192 98.3375 28.2211 96.0414 28.2211C94.2886 28.2211 93.5577 29.2754 93.202 29.9933H93.0662V28.3504H90.7636V42.0107ZM93.0597 33.3178C93.0597 31.3904 93.8876 30.1421 95.3946 30.1421C96.9534 30.1421 97.7554 31.468 97.7554 33.3178C97.7554 35.1806 96.9405 36.5388 95.3946 36.5388C93.9005 36.5388 93.0597 35.2453 93.0597 33.3178Z"
fill="black"
/>
<path
d="M1.03871 3.54545V13H2.39595V6.15376H2.48366L5.27202 12.9862H6.39844L9.18679 6.15838H9.2745V13H10.6317V3.54545H8.90057L5.89062 10.8949H5.77983L2.76989 3.54545H1.03871ZM14.6585 13.157C15.8311 13.157 16.4912 12.5614 16.7544 12.0305H16.8097V13H18.1578V8.29119C18.1578 6.22763 16.5328 5.81676 15.4063 5.81676C14.123 5.81676 12.9411 6.33381 12.4795 7.62642L13.7767 7.92188C13.9798 7.41868 14.4969 6.93395 15.4248 6.93395C16.3158 6.93395 16.7728 7.40021 16.7728 8.20348V8.2358C16.7728 8.73899 16.2558 8.72976 14.9816 8.87749C13.6382 9.03445 12.2625 9.3853 12.2625 10.9964C12.2625 12.3906 13.3105 13.157 14.6585 13.157ZM14.9585 12.049C14.1784 12.049 13.6151 11.6982 13.6151 11.0149C13.6151 10.2763 14.2707 10.0131 15.0693 9.90696C15.5171 9.84695 16.5789 9.72692 16.7774 9.52841V10.4425C16.7774 11.2827 16.108 12.049 14.9585 12.049ZM22.6507 13.1385C23.9434 13.1385 24.4512 12.3491 24.7005 11.8967H24.8159V13H26.1639V3.54545H24.7836V7.05859H24.7005C24.4512 6.62003 23.9803 5.81676 22.66 5.81676C20.9473 5.81676 19.687 7.16939 19.687 9.46839C19.687 11.7628 20.9288 13.1385 22.6507 13.1385ZM22.9554 11.9613C21.7228 11.9613 21.0811 10.8764 21.0811 9.45455C21.0811 8.04652 21.709 6.98935 22.9554 6.98935C24.1603 6.98935 24.8066 7.97266 24.8066 9.45455C24.8066 10.9457 24.1465 11.9613 22.9554 11.9613ZM31.1901 13.1431C32.7366 13.1431 33.8307 12.3814 34.1446 11.2273L32.8382 10.9918C32.5889 11.6612 31.9887 12.0028 31.2039 12.0028C30.0221 12.0028 29.2281 11.2365 29.1911 9.87003H34.2323V9.38068C34.2323 6.81854 32.6997 5.81676 31.0931 5.81676C29.1173 5.81676 27.8154 7.32173 27.8154 9.50071C27.8154 11.7028 29.0988 13.1431 31.1901 13.1431ZM29.1958 8.83594C29.2512 7.82955 29.9806 6.95703 31.1024 6.95703C32.1734 6.95703 32.8751 7.75107 32.8797 8.83594H29.1958ZM40.7416 13H42.145L43.5853 7.88033H43.6915L45.1318 13H46.5399L48.6219 5.90909H47.1954L45.8151 11.0934H45.7458L44.3609 5.90909H42.9344L41.5402 11.1165H41.471L40.0814 5.90909H38.6549L40.7416 13ZM49.9318 13H51.3121V5.90909H49.9318V13ZM50.6289 4.81499C51.1044 4.81499 51.5014 4.44567 51.5014 3.99325C51.5014 3.54084 51.1044 3.1669 50.6289 3.1669C50.1488 3.1669 49.7564 3.54084 49.7564 3.99325C49.7564 4.44567 50.1488 4.81499 50.6289 4.81499ZM56.4791 5.90909H55.0249V4.21023H53.6446V5.90909H52.6059V7.01705H53.6446V11.2042C53.64 12.4922 54.6233 13.1154 55.7128 13.0923C56.1514 13.0877 56.4468 13.0046 56.6084 12.9446L56.3591 11.8043C56.2668 11.8228 56.096 11.8643 55.8744 11.8643C55.4266 11.8643 55.0249 11.7166 55.0249 10.918V7.01705H56.4791V5.90909ZM59.5387 8.78977C59.5387 7.65874 60.2543 7.01243 61.2376 7.01243C62.1886 7.01243 62.7564 7.61719 62.7564 8.65589V13H64.1367V8.4897C64.1367 6.72159 63.1673 5.81676 61.7085 5.81676C60.6051 5.81676 59.9403 6.29688 59.608 7.06321H59.5202V3.54545H58.1584V13H59.5387V8.78977ZM75.1389 13.1477L79.8847 8.40199C81.0203 7.26633 81.0111 5.39204 79.8847 4.28409C78.7306 3.14844 76.9255 3.15767 75.7668 4.28409L75.1389 4.89347L74.5111 4.28409C73.3524 3.15767 71.5473 3.14844 70.3932 4.28409C69.2668 5.39204 69.2575 7.26633 70.3932 8.40199L75.1389 13.1477ZM86.2623 13H87.6104V11.8967H87.7258C87.9751 12.3491 88.4829 13.1385 89.7755 13.1385C91.4928 13.1385 92.7393 11.7628 92.7393 9.46839C92.7393 7.16939 91.4743 5.81676 89.7616 5.81676C88.4459 5.81676 87.9704 6.62003 87.7258 7.05859H87.6427V3.54545H86.2623V13ZM87.615 9.45455C87.615 7.97266 88.2613 6.98935 89.4662 6.98935C90.7172 6.98935 91.3451 8.04652 91.3451 9.45455C91.3451 10.8764 90.6988 11.9613 89.4662 11.9613C88.2797 11.9613 87.615 10.9457 87.615 9.45455ZM94.9078 15.6591C96.0481 15.6591 96.7683 15.0636 97.1792 13.9464L100.111 5.92294L98.6195 5.90909L96.8237 11.4119H96.7498L94.954 5.90909H93.4767L96.0712 13.0923L95.9004 13.5632C95.5495 14.505 95.0556 14.5835 94.2985 14.3757L93.9661 15.5067C94.1323 15.5806 94.4924 15.6591 94.9078 15.6591Z"
fill="black"
/>
</svg>
)
}

View File

@ -1,50 +0,0 @@
import { XIcon } from '@heroicons/react/outline'
import React, { ReactNode, useRef } from 'react'
import { useClickAway, useKey } from 'react-use'
import Button from './Button'
interface ModalProps {
children?: ReactNode
onClose?: () => void
className?: string
}
export default function Modal(props: ModalProps) {
const { children, onClose, className } = props
const ref = useRef(null)
useClickAway(ref, () => {
onClose?.()
})
useKey('Escape', onClose, {
event: 'keydown',
})
return (
<div
className={[
'absolute w-full h-full flex justify-center items-center',
'z-20',
'bg-gray-300 bg-opacity-40 backdrop-filter backdrop-blur-md',
].join(' ')}
>
<div
ref={ref}
className={`bg-white max-w-4xl relative rounded-md shadow-md ${
className || 'p-8 sm:p-12'
}`}
>
<Button
icon={<XIcon className="w-6 h-6" />}
className={[
'absolute right-4 top-4 rounded-full bg-gray-100 w-10 h-10',
'flex justify-center items-center py-0 px-0 sm:px-0',
].join(' ')}
onClick={onClose}
/>
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,59 @@
@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 {
display: grid;
row-gap: 1rem;
.shortcut-option {
display: grid;
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 {
justify-self: end;
font-family: 'WorkSans-Bold';
border: 1px solid var(--modal-hotkey-border-color);
padding: 0.4rem 1rem;
width: max-content;
border-radius: 0.4rem;
@include mobile {
padding: 0.2rem 0.4rem;
}
}
.shortcut-description {
justify-self: start;
text-align: left;
width: 18rem;
@include mobile {
text-align: left;
width: auto;
justify-self: start;
}
}
}

View File

@ -0,0 +1,56 @@
import React from 'react'
import { useKeyPressEvent } from 'react-use'
import { useRecoilState } from 'recoil'
import { shortcutsState } from '../../store/Atoms'
import Button from '../shared/Button'
const Shortcuts = () => {
const [shortcutVisibility, setShortcutState] = useRecoilState(shortcutsState)
const shortcutStateHandler = () => {
setShortcutState(prevShortcutState => {
return !prevShortcutState
})
}
useKeyPressEvent('h', ev => {
ev?.preventDefault()
shortcutStateHandler()
})
return (
<div className="shortcuts">
<Button
onClick={shortcutStateHandler}
disabled={shortcutVisibility}
style={{ border: 0 }}
icon={
<svg
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
role="img"
width="28"
height="28"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 16 16"
>
<rect
x="0"
y="0"
width="16"
height="16"
fill="none"
stroke="none"
/>
<g fill="currentColor">
<path d="M14 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h12zM2 4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H2z" />
<path d="M13 10.25a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm0-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5 0A.25.25 0 0 1 8.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 8 8.75v-.5zm2 0a.25.25 0 0 1 .25-.25h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5a.25.25 0 0 1-.25-.25v-.5zm1 2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-5-2A.25.25 0 0 1 6.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 6 8.75v-.5zm-2 0A.25.25 0 0 1 4.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 4 8.75v-.5zm-2 0A.25.25 0 0 1 2.25 8h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 2 8.75v-.5zm11-2a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm-2 0A.25.25 0 0 1 9.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 9 6.75v-.5zm-2 0A.25.25 0 0 1 7.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 7 6.75v-.5zm-2 0A.25.25 0 0 1 5.25 6h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5A.25.25 0 0 1 5 6.75v-.5zm-3 0A.25.25 0 0 1 2.25 6h1.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-1.5A.25.25 0 0 1 2 6.75v-.5zm0 4a.25.25 0 0 1 .25-.25h.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-.5a.25.25 0 0 1-.25-.25v-.5zm2 0a.25.25 0 0 1 .25-.25h5.5a.25.25 0 0 1 .25.25v.5a.25.25 0 0 1-.25.25h-5.5a.25.25 0 0 1-.25-.25v-.5z" />
</g>
</svg>
}
/>
</div>
)
}
export default Shortcuts

View File

@ -0,0 +1,69 @@
import React, { ReactNode } from 'react'
import { useSetRecoilState } from 'recoil'
import { shortcutsState } from '../../store/Atoms'
import Modal from '../shared/Modal'
interface Shortcut {
children: ReactNode
content: string
}
function ShortCut(props: Shortcut) {
const { children, content } = props
return (
<div className="shortcut-option">
<div className="shortcut-description">{content}</div>
<div className="shortcut-key">{children}</div>
</div>
)
}
export default function ShortcutsModal() {
const setShortcutState = useSetRecoilState(shortcutsState)
const shortcutStateHandler = () => {
setShortcutState(prevShortcutState => !prevShortcutState)
}
return (
<Modal
onClose={shortcutStateHandler}
title="Hotkeys"
className="modal-shortcuts"
>
<div className="shortcut-options">
<ShortCut content="Enable multi-stroke mask drawing">
<p>Hold Cmd/Ctrl</p>
</ShortCut>
<ShortCut content="Undo inpainting">
<p>Cmd/Ctrl + Z</p>
</ShortCut>
<ShortCut content="Pan">
<p>Space & Drag</p>
</ShortCut>
<ShortCut content="View original image">
<p>Hold Tab</p>
</ShortCut>
<ShortCut content="Reset zoom/pan">
<p>Esc</p>
</ShortCut>
<ShortCut content="Cancel mask drawing">
<p>Esc</p>
</ShortCut>
<ShortCut content="Decrease Brush Size">
<p>[</p>
</ShortCut>
<ShortCut content="Increase Brush Size">
<p>]</p>
</ShortCut>
<ShortCut content="Toggle Dark Mode">
<p>Shift + D</p>
</ShortCut>
<ShortCut content="Toggle Hotkeys Panel">
<p>H</p>
</ShortCut>
</div>
</Modal>
)
}

View File

@ -1,55 +0,0 @@
import React, { ReactNode } from 'react'
import Modal from './Modal'
interface Shortcut {
children: ReactNode
content: string
}
function ShortCut(props: Shortcut) {
const { children, content } = props
return (
<div className="h-full flex flex-row space-x-6 justify-between">
<div className="mr-12 border-2 rounded-xl px-2 py-1">{children}</div>
<div className="flex flex-col justify-center">{content}</div>
</div>
)
}
interface ShortcutsModalProps {
onClose?: () => void
}
export default function ShortcutsModal(props: ShortcutsModalProps) {
const { onClose } = props
return (
<Modal onClose={onClose} className="h-full sm:h-auto p-0 sm:p-0">
<div className="h-full sm:h-auto flex flex-col sm:flex-row">
<div className="flex sm:p-14 flex flex-col justify-center space-y-6">
<ShortCut content="Enable multi-stroke mask drawing">
<p>Hold Cmd/Ctrl</p>
</ShortCut>
<ShortCut content="Undo inpainting">
<p>Cmd/Ctrl + z</p>
</ShortCut>
<ShortCut content="Pan">
<p>Space & Drag</p>
</ShortCut>
<ShortCut content="View original image">
<p>Hold Tab</p>
</ShortCut>
<ShortCut content="Reset zoom/pan & Cancel mask drawing">
<p>Esc</p>
</ShortCut>
<ShortCut content="Decrease Brush Size">
<p>[</p>
</ShortCut>
<ShortCut content="Increase Brush Size">
<p>]</p>
</ShortCut>
</div>
</div>
</Modal>
)
}

View File

@ -1,120 +0,0 @@
import React, { FocusEvent, useCallback } from 'react'
import { Listbox } from '@headlessui/react'
import { CheckIcon, SelectorIcon } from '@heroicons/react/solid'
const sizes = ['720', '1080', '2000', 'Original']
type SizeSelectorProps = {
value: string
originalWidth: number
originalHeight: number
onChange: (value: string) => void
}
export default function SizeSelector(props: SizeSelectorProps) {
const { value, originalHeight, originalWidth, onChange } = props
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 = []
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)
}
}
return validSizes
}, [originalHeight, originalWidth])
const getValidSize = useCallback(() => {
if (getValidSizes().indexOf(value) === -1) {
return getValidSizes()[0]
}
return value
}, [value, getValidSizes])
return (
<div className="w-32">
<Listbox value={getValidSize()} onChange={onChange}>
<div className="relative">
<Listbox.Options
style={{ top: `-${getValidSizes().length * 40 + 5}px` }}
className="absolute mb-1 w-full overflow-auto text-base bg-opacity-10 bg-black backdrop-blur rounded-md max-h-60 outline-none sm:text-sm"
>
{getValidSizes().map(size => (
<Listbox.Option
key={size}
className={({ active }) =>
`${active ? 'bg-black bg-opacity-10' : 'text-gray-900'}
cursor-default select-none relative py-2 pl-4 pr-4`
}
value={size}
>
{({ selected, active }) => (
<>
<span
className={`${
selected ? 'font-medium' : 'font-normal'
} block truncate`}
>
{getSizeShowName(size)}
</span>
{/* {selected ? (
<span
className={`${
active ? 'text-amber-600' : 'text-amber-600'
}
absolute inset-y-0 left-0 flex items-center pl-3`}
>
<CheckIcon className="w-5 h-5" aria-hidden="true" />
</span>
) : null} */}
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
<Listbox.Button
onFocus={onButtonFocus}
className="relative w-full inline-flex w-full px-4 py-2 text-sm font-medium bg-black rounded-md bg-opacity-10 focus:outline-none "
>
<span className="block truncate">
{getSizeShowName(getValidSize())}
</span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<SelectorIcon
className="w-5 h-5 text-gray-400"
aria-hidden="true"
/>
</span>
</Listbox.Button>
</div>
</Listbox>
</div>
)
}

View File

@ -0,0 +1,21 @@
import React from 'react'
import { useRecoilValue } from 'recoil'
import Editor from './Editor/Editor'
import { shortcutsState } from '../store/Atoms'
import ShortcutsModal from './Shortcuts/ShortcutsModal'
interface WorkspaceProps {
file: File
}
const Workspace = ({ file }: WorkspaceProps) => {
const shortcutVisbility = useRecoilValue(shortcutsState)
return (
<>
<Editor file={file} />
{shortcutVisbility ? <ShortcutsModal /> : null}
</>
)
}
export default Workspace

View File

@ -0,0 +1,29 @@
.btn-primary {
display: grid;
grid-auto-flow: column;
column-gap: 1rem;
border: 1px solid var(--btn-border-color);
color: var(--btn-text-color);
font-family: 'WorkSans', sans-serif;
width: max-content;
padding: 0.5rem;
place-items: center;
border-radius: 0.5rem;
z-index: 1;
cursor: pointer;
&:hover {
background-color: var(--btn-primary-hover-bg);
color: var(--btn-text-hover-color);
}
svg {
width: 20px;
height: auto;
}
}
.btn-primary-disabled {
pointer-events: none;
opacity: 0.5;
}

View File

@ -1,15 +1,15 @@
import React, { ReactNode, useState } from 'react'
import React, { ReactNode } from 'react'
interface ButtonProps {
children?: ReactNode
className?: string
icon?: ReactNode
primary?: boolean
disabled?: boolean
onKeyDown?: () => void
onClick?: () => void
onDown?: (ev: PointerEvent) => void
onUp?: (ev: PointerEvent) => void
style?: React.CSSProperties
}
export default function Button(props: ButtonProps) {
@ -18,23 +18,12 @@ export default function Button(props: ButtonProps) {
className,
disabled,
icon,
primary,
onKeyDown,
onClick,
onDown,
onUp,
style,
} = props
const [active, setActive] = useState(false)
let background = ''
if (primary && !disabled) {
background = 'bg-primary hover:bg-black hover:text-white'
}
if (active) {
background = 'bg-black text-white'
}
if (!primary && !active) {
background = 'hover:bg-primary'
}
const blurOnClick = (e: React.MouseEvent<HTMLDivElement>) => {
e.currentTarget.blur()
@ -44,27 +33,25 @@ export default function Button(props: ButtonProps) {
return (
<div
role="button"
style={style}
onKeyDown={onKeyDown}
onClick={blurOnClick}
onPointerDown={(ev: React.PointerEvent<HTMLDivElement>) => {
setActive(true)
onDown?.(ev.nativeEvent)
}}
onPointerUp={(ev: React.PointerEvent<HTMLDivElement>) => {
setActive(false)
onUp?.(ev.nativeEvent)
}}
tabIndex={-1}
className={[
'inline-flex py-3 px-3 rounded-md cursor-pointer',
children ? 'space-x-3' : '',
background,
disabled ? 'pointer-events-none opacity-50' : '',
'btn-primary',
children ? 'btn-primary-content' : '',
disabled ? 'btn-primary-disabled' : '',
className,
].join(' ')}
>
{icon}
<span className="whitespace-nowrap select-none">{children}</span>
{children ? <span>{children}</span> : null}
</div>
)
}

View File

@ -7,9 +7,5 @@ interface LinkProps {
export default function Link(props: LinkProps) {
const { children, href } = props
return (
<a href={href} className="font-black underline">
{children}
</a>
)
return <a href={href}>{children}</a>
}

View File

@ -0,0 +1,31 @@
.modal-mask {
z-index: 9999;
height: 100%;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
background-color: var(--model-mask-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
.modal {
display: grid;
grid-auto-rows: max-content;
row-gap: 2rem;
place-self: center;
padding: 2rem;
border-radius: 0.95rem;
.modal-header {
display: grid;
grid-template-columns: repeat(2, auto);
align-items: center;
.btn-primary {
justify-self: end;
}
}
}

View File

@ -0,0 +1,36 @@
import { XIcon } from '@heroicons/react/outline'
import React, { ReactNode, useRef } from 'react'
import { useClickAway, useKey } from 'react-use'
import Button from './Button'
interface ModalProps {
children?: ReactNode
onClose?: () => void
title: string
className?: string
}
export default function Modal(props: ModalProps) {
const { children, onClose, className, title } = props
const ref = useRef(null)
useClickAway(ref, () => {
onClose?.()
})
useKey('Escape', onClose, {
event: 'keydown',
})
return (
<div className="modal-mask">
<div ref={ref} className={`modal ${className}`}>
<div className="modal-header">
<h3>{title}</h3>
<Button icon={<XIcon />} onClick={onClose} />
</div>
{children}
</div>
</div>
)
}

View File

@ -0,0 +1,31 @@
import { useCallback, useEffect, useState } from 'react'
const useResolution = () => {
const [width, setWidth] = useState(window.innerWidth)
const windowSizeHandler = useCallback(() => {
setWidth(window.innerWidth)
}, [])
useEffect(() => {
window.addEventListener('resize', windowSizeHandler)
return () => {
window.removeEventListener('resize', windowSizeHandler)
}
})
if (width < 768) {
return 'mobile'
}
if (width >= 768 && width < 1224) {
return 'tablet'
}
if (width >= 1224) {
return 'desktop'
}
}
export default useResolution

View File

@ -1,6 +1,14 @@
import React from 'react'
import ReactDOM from 'react-dom'
import './styles/index.css'
import './styles/_index.scss'
import { RecoilRoot } from 'recoil'
import App from './App'
ReactDOM.render(<App />, document.getElementById('root'))
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<App />
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
)

View File

@ -0,0 +1,11 @@
import { atom } from 'recoil'
export const fileState = atom<File | undefined>({
key: 'fileState',
default: undefined,
})
export const shortcutsState = atom<boolean>({
key: 'shortcutsState',
default: false,
})

View File

@ -0,0 +1,11 @@
.lama-cleaner {
display: grid;
grid-template-areas: 'main-content';
width: 100vw;
height: 100vh;
background-color: var(--page-bg);
color: var(--page-text-color);
transition-property: background-color, color;
transition-duration: 0.2s;
transition-timing-function: repeat(2, ease-out);
}

View File

@ -0,0 +1,40 @@
// Define Breakpoints
$breakpoints: (
mobile-res: 768px,
desktop-res: 1224px,
);
// Calcualte Min and Max Widths Based on Breakpoints
$sizes: (
mobile-max-width: calc(map-get($breakpoints, mobile-res) - 1px),
tablet-min-width: map-get($breakpoints, mobile-res),
tablet-max-width: calc(map-get($breakpoints, desktop-res) - 1px),
desktop-min-width: map-get($breakpoints, desktop-res),
);
// Mobile Mixin
@mixin mobile {
@media screen and (max-width: map-get($sizes, mobile-max-width)) {
@content;
}
}
// Tablet Mixin
@mixin tablet {
@media screen and (min-width: map-get($sizes, tablet-min-width)) and (max-width: map-get($sizes, tablet-max-width)) {
@content;
}
}
// Desktop Mixin
@mixin desktop {
@media screen and (min-width: map-get($sizes, desktop-min-width)) {
@content;
}
}
@mixin large {
@media screen and (min-width: map-get($sizes, tablet-min-width)) {
@content;
}
}

View File

@ -0,0 +1,53 @@
@mixin accented-display($bg-color) {
background: $bg-color;
color: rgb(0, 0, 0);
font-family: 'WorkSans';
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: 1px 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']::-moz-range-progress {
background: var(--yellow-accent);
}
}

View File

@ -0,0 +1,2 @@
@forward './Mixins';
@forward './MediaQueries';

View File

@ -0,0 +1,39 @@
@keyframes pulsing {
0% {
opacity: 1;
}
50% {
opacity: 0.75;
background-color: var(--animation-pulsing-bg);
}
100% {
opacity: 1;
}
}
@keyframes opacityReveal {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes slideDown {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(0);
}
}
@keyframes slideUp {
0% {
transform: translateY(100%);
}
100% {
transform: translateY(0);
}
}

View File

@ -0,0 +1,29 @@
:root {
// General
// Theme
--page-bg: rgb(255, 255, 255);
--page-text-color: #040404;
--yellow-accent: #ffcc00;
--link-color: rgb(0, 0, 0);
--border-color: rgb(100, 100, 120);
// Editor
--editor-toolkit-bg: rgba(255, 255, 255, 0.5);
--options-text-color: var(--page-text-color);
--editor-size-border-color: var(--border-color);
// Modal
--modal-bg: var(--page-bg);
--modal-text-color: rgb(0, 0, 0);
--modal-hotkey-border-color: rgb(0, 0, 0);
--model-mask-bg: rgba(209,213,219,0.4);
// Shared
--btn-primary-bg: rgb(210, 210, 220);
--btn-text-color: black;
--btn-text-hover-color: black;
--btn-border-color: rgb(100, 100, 120);
--btn-primary-hover-bg: var(--yellow-accent);
--animation-pulsing-bg: rgb(255, 255, 255, 0.5);
}

View File

@ -0,0 +1,30 @@
[data-theme='dark'] {
// General
// Theme
--page-bg: #040404;
--page-text-color: #F9F9F9;
--yellow-accent: #ffcc00;
--link-color: var(--yellow-accent);
--border-color: rgb(100, 100, 120);
// Editor
--editor-toolkit-bg: rgba(0, 0, 0, 0.5);
--options-text-color: var(--page-text-color);
--editor-size-border-color: var(--yellow-accent);
// Modal
--modal-bg: var(--page-bg);
--modal-text-color: var(--page-text-color);
// --modal-hotkey-bg: rgb(60, 60, 90);
--modal-hotkey-border-color: var(--page-text-color);;
--model-mask-bg: rgba(76, 76, 87, 0.4);
// Shared
--btn-primary-bg: rgb(140, 140, 180);
--btn-text-color: white;
--btn-text-hover-color: var(--page-bg);
--btn-border-color: var(--yellow-accent);
--btn-primary-hover-bg: var(--yellow-accent);
--animation-pulsing-bg: rgb(240, 240, 255);
}

View File

@ -0,0 +1,19 @@
@font-face {
font-family: 'WorkSans';
src: url('../media/fonts/Work_Sans/WorkSans-Regular.ttf');
}
@font-face {
font-family: 'WorkSans-Semibold';
src: url('../media/fonts/Work_Sans/WorkSans-SemiBold.ttf');
}
@font-face {
font-family: 'WorkSans-Bold';
src: url('../media/fonts/Work_Sans/WorkSans-Bold.ttf');
}
@font-face {
font-family: 'WorkSans-Black';
src: url('../media/fonts/Work_Sans/WorkSans-Black.ttf');
}

View File

@ -0,0 +1,32 @@
// General
@use './Fonts';
@use './Colors';
@use './ColorsDark';
@use './Animations';
// App
@use './App';
@use '../components/Editor/Editor';
@use '../components/LandingPage/LandingPage';
@use '../components/Header/Header';
@use '../components/Header/ThemeChanger';
@use '../components/Shortcuts/Shortcuts';
// Shared
@use '../components/FileSelect/FileSelect';
@use '../components/shared/Button';
@use '../components/shared/Modal';
// Main CSS
*,
*:before,
*:after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
font-family: 'WorkSans', sans-serif;
}

View File

@ -1,46 +0,0 @@
@tailwind base;
/* Your own custom base styles */
/* Start purging... */
@tailwind components;
/* Stop purging. */
/* Your own custom component styles */
/* Start purging... */
@tailwind utilities;
/* Stop purging. */
/* Your own custom utilities */
@import url('https://fonts.googleapis.com/css2?family=Work+Sans:wght@600;900&display=swap');
html,
body {
width: 100%;
height: 100%;
font-family: 'Work Sans';
font-weight: 600;
overflow: hidden;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 17px;
height: 17px;
background: black;
border-radius: 50%;
}
*:not(input):not(textarea) {
user-select: none; /* disable selection/Copy of UIWebView */
-webkit-user-select: none; /* disable selection/Copy of UIWebView */
-webkit-touch-callout: none; /* disable the IOS popup when long-press on a link */
}
@supports (-webkit-touch-callout: none) {
.full-visible-h-safari {
height: calc(100% - 80px); /* -webkit-fill-available;*/
}
}

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)
})
}
}

View File

@ -1,37 +0,0 @@
module.exports = {
mode: 'jit',
theme: {
extend: {
animation: {
'pulse-fast': 'pulse 0.7s cubic-bezier(0.4, 0, 0.6, 1) infinite',
},
colors: {
primary: '#BDFF01',
},
keyframes: {
pulse: {
'0%, 100%': { opacity: 0.8 },
'50%': { opacity: 0.7 },
},
},
},
},
variants: {},
plugins: [],
purge: {
// Filenames to scan for classes
content: [
'./src/**/*.html',
'./src/**/*.js',
'./src/**/*.jsx',
'./src/**/*.ts',
'./src/**/*.tsx',
'./public/index.html',
],
// Options passed to PurgeCSS
options: {
// Whitelist specific selectors by name
// safelist: [],
},
},
}

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@ class LaMa:
else:
model_path = download_model(LAMA_MODEL_URL)
print(f"Load LaMa model from: {model_path}")
model = torch.jit.load(model_path, map_location="cpu")
model = model.to(device)
model.eval()