Complete GUI Refactor
This patch brings in a massive number of changes to the frontend of the application. Please feel free to discuss the proposed changes with me at any time. Implemented Recoil as a state management system. Why Recoil? It is a robust library built by developers at Facebook for state management. It has an extremely simple API for implementation that is in sync with React syntax compared to any other state management system out there and works amazingly well. While the official release status is beta as it becomes fully featured, the library is already used in various systems at Facebook and is very stable for the use cases of this application. Why global state management? One of the major issues I saw with the current file structure is that there is minimal code splitting and it makes further development of the frontend a cumbersome task. I have broken down the frontend into various easy to access components isolating the GUI from the logic. To avoid prop drilling, we need global state management to handle the necessary tasks. This will also facilitate the addition of any new features greatly. Code Splitting. Majority of the components that can be isolated in the application have now been done so. All New Custom CSS & Removal of Tailwind While Tailwind is a great way to deploy beautiful interfaces quickly, anyone trying to stylize the application further needs to be familiar with Tailwind which makes it harder for more people to work on it. Not to mention, I am not a particular fan of flooding JSX elements with inline CSS classes. That makes reading the code extremely hard and bloats up component code drastically. As a replacement to Tailwind, I implemented a custom styling system using SCSS as a developer dependency. In the new system, all the general and shared styles are in the styles folder and all the component styles are in the same folder as the component for easy access.The _index.scss file now acts as a central import for every other stylesheet that needs to be loaded. What Changed? The entire application looks and feels like the current implementation with minimal changes. The green (#bdff01) highlight used in the application has now been changed to a bright yellow (rgb(255, 190, 0)) because I felt it better suited the new Dark Mode (see below). The swipe bar for comparing before and after images has now been removed and instead the comparison is a smooth fade effect. I felt this was better to analyze image changes rather than a swiper. // Can add the swipe back if needed. Dark Mode A brand new Dark Mode has been added for the application. Users can enable and disable by tapping the button in the header or by using the Shift + D hotkey. Other Misc New Features When the editor image is now zoomed out to its default size, the image now also gets centered back. TODO The currently used react-zoom-pinch-pan module is not mobile friendly. It does not allow brush strokes. Need to figure out a way to fix this. Further optimization of the frontend code with better code splitting and performance. When using the LaMa model, the first stroke has a delayed response from the backend but the ones that follow are almost immediate. I believe this is happening because of the initialization of the model on the first stroke. I wonder if either of us can look at it and see if this can somehow be preloaded so the user experience is smooth from the first stroke. Enable threading for the desktop application mode so flaskwebgui does not block the main applications Python console.
This commit is contained in:
parent
a40d92f23f
commit
eea85b834e
@ -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.54fbc69f.chunk.css",
|
||||
"main.js": "/static/js/main.d346743e.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.2516aa7d.chunk.js": "/static/js/2.2516aa7d.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.2516aa7d.chunk.js.LICENSE.txt": "/static/js/2.2516aa7d.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.2516aa7d.chunk.js",
|
||||
"static/css/main.54fbc69f.chunk.css",
|
||||
"static/js/main.d346743e.chunk.js"
|
||||
]
|
||||
}
|
@ -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.54fbc69f.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.2516aa7d.chunk.js"></script><script src="/static/js/main.d346743e.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
2
lama_cleaner/app/build/static/js/2.2516aa7d.chunk.js
Normal file
2
lama_cleaner/app/build/static/js/2.2516aa7d.chunk.js
Normal file
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
1
lama_cleaner/app/build/static/js/main.d346743e.chunk.js
Normal file
1
lama_cleaner/app/build/static/js/main.d346743e.chunk.js
Normal file
File diff suppressed because one or more lines are too long
BIN
lama_cleaner/app/build/static/media/WorkSans-Black.67c2c5a1.ttf
Normal file
BIN
lama_cleaner/app/build/static/media/WorkSans-Black.67c2c5a1.ttf
Normal file
Binary file not shown.
BIN
lama_cleaner/app/build/static/media/WorkSans-Bold.2bea7a7f.ttf
Normal file
BIN
lama_cleaner/app/build/static/media/WorkSans-Bold.2bea7a7f.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('tailwindcss'),
|
||||
require('postcss-preset-env'),
|
||||
],
|
||||
};
|
@ -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
|
||||
|
@ -1,11 +1,11 @@
|
||||
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 { ThemeChanger, themeState } from './components/shared/ThemeChanger'
|
||||
import Workspace from './components/Workspace'
|
||||
import { fileState } from './store/Atoms'
|
||||
|
||||
// Keeping GUI Window Open
|
||||
async function getRequest(url = '') {
|
||||
@ -29,105 +29,25 @@ if (!process.env.NODE_ENV || process.env.NODE_ENV === 'production') {
|
||||
}
|
||||
|
||||
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', () => {
|
||||
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}>
|
||||
<ThemeChanger />
|
||||
{file ? <Workspace file={file} /> : <LandingPage />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
133
lama_cleaner/app/src/components/Editor/Editor.scss
Normal file
133
lama_cleaner/app/src/components/Editor/Editor.scss
Normal file
@ -0,0 +1,133 @@
|
||||
@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;
|
||||
}
|
||||
|
||||
.original-image-container {
|
||||
grid-area: editor-content;
|
||||
pointer-events: none;
|
||||
animation: opacityReveal 350ms ease-in-out;
|
||||
}
|
||||
|
||||
.editor-canvas-loading {
|
||||
pointer-events: none;
|
||||
animation: pulsing 750ms infinite;
|
||||
}
|
||||
|
||||
.editor-toolkit-panel {
|
||||
position: fixed;
|
||||
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;
|
||||
|
||||
@include mobile {
|
||||
padding: 1rem 2rem;
|
||||
grid-template-areas:
|
||||
'toolkit-image-type toolkit-size-selector'
|
||||
'toolkit-brush-slider toolkit-brush-slider'
|
||||
'toolkit-btns toolkit-btns';
|
||||
row-gap: 2rem;
|
||||
justify-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
|
||||
input[type='range'] {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-toolkit-btns {
|
||||
grid-area: toolkit-btns;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
column-gap: 1rem;
|
||||
}
|
||||
|
||||
.brush-shape {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
background: rgb(255, 255, 255, 0.25);
|
||||
border: 1px dashed var(--border-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-size-selector {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.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;
|
||||
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);
|
||||
border-radius: 0.5rem;
|
||||
font-family: 'WorkSans-Bold';
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
color: rgb(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.image-type-tag {
|
||||
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);
|
||||
}
|
@ -21,11 +21,11 @@ import {
|
||||
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 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)'
|
||||
@ -78,7 +78,6 @@ 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']
|
||||
@ -411,7 +410,6 @@ export default function Editor(props: EditorProps) {
|
||||
ev?.preventDefault()
|
||||
ev?.stopPropagation()
|
||||
if (hadRunInpainting()) {
|
||||
setShowSeparator(true)
|
||||
setShowOriginal(true)
|
||||
}
|
||||
},
|
||||
@ -420,7 +418,6 @@ export default function Editor(props: EditorProps) {
|
||||
ev?.stopPropagation()
|
||||
if (hadRunInpainting()) {
|
||||
setShowOriginal(false)
|
||||
setTimeout(() => setShowSeparator(false), 300)
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -430,7 +427,6 @@ export default function Editor(props: EditorProps) {
|
||||
const currRender = renders[renders.length - 1]
|
||||
downloadImage(currRender.currentSrc, name)
|
||||
}
|
||||
|
||||
const onSizeLimitChange = (_sizeLimit: string) => {
|
||||
setSizeLimit(_sizeLimit)
|
||||
}
|
||||
@ -512,11 +508,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,19 +533,11 @@ 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"
|
||||
className="editor-canvas"
|
||||
style={{ cursor: getCursor() }}
|
||||
onContextMenu={e => {
|
||||
e.preventDefault()
|
||||
@ -572,58 +556,37 @@ export default function Editor(props: EditorProps) {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{showOriginal ? (
|
||||
<div
|
||||
className={[
|
||||
'absolute top-0 right-0 pointer-events-none',
|
||||
'overflow-hidden',
|
||||
'border-primary',
|
||||
showSeparator ? 'border-l-4' : '',
|
||||
].join(' ')}
|
||||
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',
|
||||
}}
|
||||
>
|
||||
<img
|
||||
className="absolute right-0"
|
||||
src={original.src}
|
||||
alt="original"
|
||||
width={`${original.naturalWidth}px`}
|
||||
height={`${original.naturalHeight}px`}
|
||||
className="original-image-container"
|
||||
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`,
|
||||
maxWidth: 'none',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</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(' ')}
|
||||
>
|
||||
<div className="editor-toolkit-panel">
|
||||
<p className="image-type-tag">
|
||||
{showOriginal ? 'Original' : 'Inpainted'}
|
||||
</p>
|
||||
<SizeSelector
|
||||
value={sizeLimit || '1080'}
|
||||
onChange={onSizeLimitChange}
|
||||
@ -631,25 +594,19 @@ export default function Editor(props: EditorProps) {
|
||||
originalHeight={original.naturalHeight}
|
||||
/>
|
||||
<Slider
|
||||
label={
|
||||
<span>
|
||||
<span className="hidden md:inline">Brush</span>
|
||||
</span>
|
||||
}
|
||||
label="Brush"
|
||||
min={10}
|
||||
max={150}
|
||||
value={brushSize}
|
||||
onChange={setBrushSize}
|
||||
/>
|
||||
<div>
|
||||
<div className="editor-toolkit-btns">
|
||||
<Button
|
||||
className="mr-2"
|
||||
icon={<ArrowsExpandIcon className="w-6 h-6" />}
|
||||
icon={<ArrowsExpandIcon />}
|
||||
disabled={scale === minScale}
|
||||
onClick={resetZoom}
|
||||
/>
|
||||
<Button
|
||||
className="mr-2"
|
||||
icon={
|
||||
<svg
|
||||
width="19"
|
||||
@ -657,7 +614,6 @@ export default function Editor(props: EditorProps) {
|
||||
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"
|
||||
@ -669,43 +625,26 @@ export default function Editor(props: EditorProps) {
|
||||
disabled={renders.length === 0}
|
||||
/>
|
||||
<Button
|
||||
className="mr-2"
|
||||
icon={<EyeIcon className="w-6 h-6" />}
|
||||
icon={<EyeIcon />}
|
||||
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" />}
|
||||
icon={<DownloadIcon />}
|
||||
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',
|
||||
}}
|
||||
>
|
||||
{undefined}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
77
lama_cleaner/app/src/components/Editor/SizeSelector.tsx
Normal file
77
lama_cleaner/app/src/components/Editor/SizeSelector.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
import React, { FocusEvent, useCallback, useRef } from 'react'
|
||||
|
||||
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 selectRef = useRef()
|
||||
|
||||
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])
|
||||
|
||||
const sizeChangeHandler = (e: any) => {
|
||||
onChange(e.target.value)
|
||||
e.target.blur()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="editor-size-selector">
|
||||
<p>Size:</p>
|
||||
<select value={getValidSize()} onChange={sizeChangeHandler}>
|
||||
{getValidSizes().map(size => (
|
||||
<option key={size} value={size}>
|
||||
{getSizeShowName(size)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -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}
|
35
lama_cleaner/app/src/components/FileSelect/FileSelect.scss
Normal file
35
lama_cleaner/app/src/components/FileSelect/FileSelect.scss
Normal 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-Bold';
|
||||
text-align: center;
|
||||
}
|
@ -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>
|
||||
)
|
16
lama_cleaner/app/src/components/Header/Header.scss
Normal file
16
lama_cleaner/app/src/components/Header/Header.scss
Normal file
@ -0,0 +1,16 @@
|
||||
header {
|
||||
grid-area: main-content;
|
||||
padding: 1rem 2rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, max-content);
|
||||
width: 100%;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
}
|
||||
|
||||
.shortcuts {
|
||||
justify-self: end;
|
||||
margin-right: 4rem;
|
||||
z-index: 1;
|
||||
}
|
31
lama_cleaner/app/src/components/Header/Header.tsx
Normal file
31
lama_cleaner/app/src/components/Header/Header.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { ArrowLeftIcon } from '@heroicons/react/outline'
|
||||
import React from 'react'
|
||||
import { useSetRecoilState } from 'recoil'
|
||||
import { fileState } from '../../store/Atoms'
|
||||
import Button from '../shared/Button'
|
||||
import Shortcuts from '../Shortcuts/Shortcuts'
|
||||
import useResolution from '../../hooks/useResolution'
|
||||
|
||||
const Header = () => {
|
||||
const setFile = useSetRecoilState(fileState)
|
||||
const resolution = useResolution()
|
||||
|
||||
const renderHeader = () => {
|
||||
return (
|
||||
<header>
|
||||
<Button
|
||||
icon={<ArrowLeftIcon className="w-6 h-6" />}
|
||||
onClick={() => {
|
||||
setFile(undefined)
|
||||
}}
|
||||
>
|
||||
{resolution === 'desktop' ? 'Start New' : undefined}
|
||||
</Button>
|
||||
<Shortcuts />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
return renderHeader()
|
||||
}
|
||||
|
||||
export default Header
|
30
lama_cleaner/app/src/components/LandingPage/LandingPage.scss
Normal file
30
lama_cleaner/app/src/components/LandingPage/LandingPage.scss
Normal 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;
|
||||
}
|
26
lama_cleaner/app/src/components/LandingPage/LandingPage.tsx
Normal file
26
lama_cleaner/app/src/components/LandingPage/LandingPage.tsx
Normal 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
|
@ -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>
|
||||
)
|
||||
}
|
32
lama_cleaner/app/src/components/Shortcuts/Shortcuts.scss
Normal file
32
lama_cleaner/app/src/components/Shortcuts/Shortcuts.scss
Normal file
@ -0,0 +1,32 @@
|
||||
.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);
|
||||
}
|
||||
|
||||
.shortcut-options {
|
||||
display: grid;
|
||||
row-gap: 1rem;
|
||||
|
||||
.shortcut-option {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
column-gap: 6rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
font-family: 'WorkSans-Bold';
|
||||
background-color: var(--modal-hotkey-bg);
|
||||
padding: 0.4rem 1rem;
|
||||
width: max-content;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
|
||||
.shortcut-description {
|
||||
justify-self: end;
|
||||
text-align: right;
|
||||
width: 15rem;
|
||||
}
|
||||
}
|
54
lama_cleaner/app/src/components/Shortcuts/Shortcuts.tsx
Normal file
54
lama_cleaner/app/src/components/Shortcuts/Shortcuts.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
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', () => {
|
||||
shortcutStateHandler()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="shortcuts">
|
||||
<Button
|
||||
onClick={shortcutStateHandler}
|
||||
disabled={shortcutVisibility}
|
||||
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
|
66
lama_cleaner/app/src/components/Shortcuts/ShortcutsModal.tsx
Normal file
66
lama_cleaner/app/src/components/Shortcuts/ShortcutsModal.tsx
Normal file
@ -0,0 +1,66 @@
|
||||
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-key">{children}</div>
|
||||
<div className="shortcut-description">{content}</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 & 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
23
lama_cleaner/app/src/components/Workspace.tsx
Normal file
23
lama_cleaner/app/src/components/Workspace.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import React from 'react'
|
||||
import { useRecoilValue } from 'recoil'
|
||||
import Editor from './Editor/Editor'
|
||||
import { shortcutsState } from '../store/Atoms'
|
||||
import Header from './Header/Header'
|
||||
import ShortcutsModal from './Shortcuts/ShortcutsModal'
|
||||
|
||||
interface WorkspaceProps {
|
||||
file: File
|
||||
}
|
||||
|
||||
const Workspace = ({ file }: WorkspaceProps) => {
|
||||
const shortcutVisbility = useRecoilValue(shortcutsState)
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Editor file={file} />
|
||||
{shortcutVisbility ? <ShortcutsModal /> : null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Workspace
|
28
lama_cleaner/app/src/components/shared/Button.scss
Normal file
28
lama_cleaner/app/src/components/shared/Button.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.btn-primary {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
column-gap: 1rem;
|
||||
background-color: var(--btn-primary-bg);
|
||||
color: black;
|
||||
font-family: 'WorkSans-Bold', 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);
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 20px;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-primary-disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
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
|
||||
@ -18,23 +17,11 @@ export default function Button(props: ButtonProps) {
|
||||
className,
|
||||
disabled,
|
||||
icon,
|
||||
primary,
|
||||
onKeyDown,
|
||||
onClick,
|
||||
onDown,
|
||||
onUp,
|
||||
} = 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()
|
||||
@ -47,24 +34,21 @@ export default function Button(props: ButtonProps) {
|
||||
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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
}
|
19
lama_cleaner/app/src/components/shared/Modal.scss
Normal file
19
lama_cleaner/app/src/components/shared/Modal.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.modal {
|
||||
display: grid;
|
||||
grid-auto-rows: max-content;
|
||||
row-gap: 2rem;
|
||||
place-self: center;
|
||||
padding: 2rem;
|
||||
border-radius: 0.95rem;
|
||||
z-index: 9999;
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
align-items: center;
|
||||
|
||||
.btn-primary {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
34
lama_cleaner/app/src/components/shared/Modal.tsx
Normal file
34
lama_cleaner/app/src/components/shared/Modal.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
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 ref={ref} className={`modal ${className}`}>
|
||||
<div className="modal-header">
|
||||
<h3>{title}</h3>
|
||||
<Button icon={<XIcon />} onClick={onClose} />
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
29
lama_cleaner/app/src/components/shared/ThemeChanger.scss
Normal file
29
lama_cleaner/app/src/components/shared/ThemeChanger.scss
Normal file
@ -0,0 +1,29 @@
|
||||
.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;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 0.25rem;
|
||||
z-index: 10;
|
||||
outline: none;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='dark'] {
|
||||
.theme-changer {
|
||||
background: rgb(255, 190, 0);
|
||||
box-shadow: none;
|
||||
transform: rotate(-75deg);
|
||||
outline: none;
|
||||
}
|
||||
}
|
25
lama_cleaner/app/src/components/shared/ThemeChanger.tsx
Normal file
25
lama_cleaner/app/src/components/shared/ThemeChanger.tsx
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react'
|
||||
import { atom, useRecoilState } from 'recoil'
|
||||
|
||||
export const themeState = atom({
|
||||
key: 'themeState',
|
||||
default: 'dark',
|
||||
})
|
||||
|
||||
export const ThemeChanger = () => {
|
||||
const [theme, setTheme] = useRecoilState(themeState)
|
||||
|
||||
const themeSwitchHandler = () => {
|
||||
const newTheme = theme === 'light' ? 'dark' : 'light'
|
||||
setTheme(newTheme)
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="theme-changer"
|
||||
onClick={themeSwitchHandler}
|
||||
aria-label="Switch Theme"
|
||||
/>
|
||||
)
|
||||
}
|
31
lama_cleaner/app/src/hooks/useResolution.tsx
Normal file
31
lama_cleaner/app/src/hooks/useResolution.tsx
Normal 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
|
@ -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')
|
||||
)
|
||||
|
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Black.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Black.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Bold.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Bold.ttf
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Italic.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Italic.ttf
Normal file
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Light.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Light.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Medium.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Medium.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Regular.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Regular.ttf
Normal file
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-SemiBold.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-SemiBold.ttf
Normal file
Binary file not shown.
Binary file not shown.
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Thin.ttf
Normal file
BIN
lama_cleaner/app/src/media/fonts/Work_Sans/WorkSans-Thin.ttf
Normal file
Binary file not shown.
Binary file not shown.
11
lama_cleaner/app/src/store/Atoms.tsx
Normal file
11
lama_cleaner/app/src/store/Atoms.tsx
Normal 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,
|
||||
})
|
11
lama_cleaner/app/src/styles/App.scss
Normal file
11
lama_cleaner/app/src/styles/App.scss
Normal 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);
|
||||
}
|
40
lama_cleaner/app/src/styles/Mixins/_MediaQueries.scss
Normal file
40
lama_cleaner/app/src/styles/Mixins/_MediaQueries.scss
Normal 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;
|
||||
}
|
||||
}
|
0
lama_cleaner/app/src/styles/Mixins/_Mixins.scss
Normal file
0
lama_cleaner/app/src/styles/Mixins/_Mixins.scss
Normal file
2
lama_cleaner/app/src/styles/Mixins/_index.scss
Normal file
2
lama_cleaner/app/src/styles/Mixins/_index.scss
Normal file
@ -0,0 +1,2 @@
|
||||
@forward './Mixins';
|
||||
@forward './MediaQueries';
|
21
lama_cleaner/app/src/styles/_Animations.scss
Normal file
21
lama_cleaner/app/src/styles/_Animations.scss
Normal file
@ -0,0 +1,21 @@
|
||||
@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;
|
||||
}
|
||||
}
|
23
lama_cleaner/app/src/styles/_Colors.scss
Normal file
23
lama_cleaner/app/src/styles/_Colors.scss
Normal file
@ -0,0 +1,23 @@
|
||||
:root {
|
||||
// General
|
||||
|
||||
// Theme
|
||||
--page-bg: rgb(240, 240, 250);
|
||||
--page-text-color: rgb(0, 0, 0);
|
||||
--link-color: rgb(0, 0, 0);
|
||||
--yellow-accent: rgb(255, 190, 0);
|
||||
--border-color: rgb(100, 100, 120);
|
||||
|
||||
// Editor
|
||||
--editor-toolkit-bg: rgb(240, 240, 250, 0.15);
|
||||
|
||||
// Modal
|
||||
--modal-bg: var(--page-bg);
|
||||
--modal-text-color: rgb(0, 0, 0);
|
||||
--modal-hotkey-bg: rgb(240, 240, 240);
|
||||
|
||||
// Shared
|
||||
--btn-primary-bg: rgb(210, 210, 220);
|
||||
--btn-primary-hover-bg: var(--yellow-accent);
|
||||
--animation-pulsing-bg: rgb(255, 255, 255, 0.5);
|
||||
}
|
23
lama_cleaner/app/src/styles/_ColorsDark.scss
Normal file
23
lama_cleaner/app/src/styles/_ColorsDark.scss
Normal file
@ -0,0 +1,23 @@
|
||||
[data-theme='dark'] {
|
||||
// General
|
||||
|
||||
// Theme
|
||||
--page-bg: rgb(20, 20, 30);
|
||||
--page-text-color: rgb(200, 200, 210);
|
||||
--link-color: rgb(255, 190, 0);
|
||||
--yellow-accent: rgb(255, 190, 0);
|
||||
--border-color: rgb(100, 100, 120);
|
||||
|
||||
// Editor
|
||||
--editor-toolkit-bg: rgb(20, 20, 30, 0.15);
|
||||
|
||||
// Modal
|
||||
--modal-bg: var(--page-bg);
|
||||
--modal-text-color: var(--page-text-color);
|
||||
--modal-hotkey-bg: rgb(60, 60, 90);
|
||||
|
||||
// Shared
|
||||
--btn-primary-bg: rgb(140, 140, 180);
|
||||
--btn-primary-hover-bg: var(--yellow-accent);
|
||||
--animation-pulsing-bg: rgb(240, 240, 255);
|
||||
}
|
19
lama_cleaner/app/src/styles/_Fonts.scss
Normal file
19
lama_cleaner/app/src/styles/_Fonts.scss
Normal 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');
|
||||
}
|
32
lama_cleaner/app/src/styles/_index.scss
Normal file
32
lama_cleaner/app/src/styles/_index.scss
Normal 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/Shortcuts/Shortcuts';
|
||||
|
||||
// Shared
|
||||
@use '../components/shared/ThemeChanger';
|
||||
@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;
|
||||
}
|
@ -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;*/
|
||||
}
|
||||
}
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue
Block a user