Create monorepo from known-good production state
This commit is contained in:
commit
c034824338
651 changed files with 120469 additions and 0 deletions
39
bgbye/src/App.css
Normal file
39
bgbye/src/App.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
93
bgbye/src/App.js
Normal file
93
bgbye/src/App.js
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Box, ThemeProvider, createTheme, Paper, FormGroup, FormControlLabel, Checkbox, Divider, Typography } from '@mui/material';
|
||||
import { Toaster, toast } from 'react-hot-toast';
|
||||
import ImageUpload from './components/ImageUpload';
|
||||
import ResponsiveAppBar from './components/ResponsiveAppBar';
|
||||
import MethodSelector from './components/MethodSelector';
|
||||
import ModelsInfo from './components/ModelsInfo';
|
||||
|
||||
// Utility function to show error toast
|
||||
const showErrorToast = (message) => {
|
||||
toast.error(message, {
|
||||
duration: 4000,
|
||||
position: 'bottom-center',
|
||||
style: {
|
||||
background: '#FF4136',
|
||||
color: '#FFFFFF',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const APP_ID = 'bgbye';
|
||||
|
||||
function App() {
|
||||
const [processedPanels, setProcessedPanels] = useState(0);
|
||||
const [darkMode, setDarkMode] = useState(() => {
|
||||
const storedTheme = localStorage.getItem(APP_ID + '_theme');
|
||||
return storedTheme !== null ? storedTheme === 'true' : false;
|
||||
});
|
||||
const [selectedModels, setSelectedModels] = useState(
|
||||
Object.keys(ModelsInfo).reduce((acc, key) => {
|
||||
acc[key] = true;
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const handleProcessed = () => {
|
||||
setProcessedPanels((prevCount) => prevCount + 1);
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
setDarkMode((prevMode) => !prevMode);
|
||||
localStorage.setItem(APP_ID + '_theme', !darkMode);
|
||||
};
|
||||
|
||||
const handleModelChange = (event) => {
|
||||
setSelectedModels({
|
||||
...selectedModels,
|
||||
[event.target.name]: event.target.checked
|
||||
});
|
||||
};
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: darkMode ? 'dark' : 'light',
|
||||
background: {
|
||||
default: darkMode
|
||||
? 'linear-gradient(180deg, #10364a 30%, #0d171c 90%)'
|
||||
: 'linear-gradient(180deg, #cbdbf2 30%, #b0ccff 90%)',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
minHeight: '100vh',
|
||||
background: theme.palette.background.default,
|
||||
}}
|
||||
>
|
||||
<ResponsiveAppBar toggleTheme={toggleTheme} darkMode={darkMode}/>
|
||||
{Array.from({ length: processedPanels + 1 }).map((_, index) => (
|
||||
<Box key={index} sx={{ mt: 4 }}>
|
||||
<ImageUpload
|
||||
onProcessed={handleProcessed}
|
||||
theme={theme}
|
||||
fileID={index}
|
||||
selectedModels={selectedModels}
|
||||
showErrorToast={showErrorToast}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
<MethodSelector selectedModels={selectedModels} handleModelChange={handleModelChange} />
|
||||
</Box>
|
||||
<Toaster />
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
8
bgbye/src/App.test.js
Normal file
8
bgbye/src/App.test.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
7
bgbye/src/TODO.txt
Normal file
7
bgbye/src/TODO.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
Improve UI:
|
||||
|
||||
add dropdown for download types
|
||||
- webp/png
|
||||
- mov/gif/webm
|
||||
|
||||
add download video progressbar
|
||||
48
bgbye/src/components/GradientPickerPopout.js
Normal file
48
bgbye/src/components/GradientPickerPopout.js
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import Popover from '@mui/material/Popover';
|
||||
import ColorPicker from 'react-best-gradient-color-picker';
|
||||
import ColorizeIcon from '@mui/icons-material/Colorize';
|
||||
|
||||
function GradientPickerPopout({ buttonLabel, color, onChange }) {
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleChange = (newColor) => {
|
||||
if (onChange) {
|
||||
onChange(newColor);
|
||||
}
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'simple-popover' : undefined;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button aria-describedby={id} variant="contained" onClick={handleClick} endIcon={<ColorizeIcon/>} fullWidth>
|
||||
{buttonLabel}
|
||||
</Button>
|
||||
<Popover
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<ColorPicker value={color} onChange={handleChange} />
|
||||
</Popover>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default GradientPickerPopout;
|
||||
630
bgbye/src/components/ImageUpload.js
Normal file
630
bgbye/src/components/ImageUpload.js
Normal file
|
|
@ -0,0 +1,630 @@
|
|||
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Typography,
|
||||
useTheme,
|
||||
CircularProgress,
|
||||
ToggleButtonGroup,
|
||||
ToggleButton,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
LinearProgress,
|
||||
Paper,
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
useMediaQuery,
|
||||
} from '@mui/material';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import axios from 'axios';
|
||||
import { ImgComparisonSlider } from '@img-comparison-slider/react';
|
||||
import pLimit from 'p-limit';
|
||||
import GradientPickerPopout from './GradientPickerPopout';
|
||||
import ZoomInIcon from '@mui/icons-material/ZoomIn';
|
||||
import Magnifier from 'react18-image-magnifier'
|
||||
import ModelsInfo from './ModelsInfo';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import GradientIcon from '@mui/icons-material/Gradient';
|
||||
|
||||
const ImageUpload = ({ onProcessed, fileID, selectedModels, showErrorToast }) => {
|
||||
const [selectedFile, setSelectedFile] = useState(null);
|
||||
const [originalFilename, setOriginalFilename] = useState('');
|
||||
const [processedFiles, setProcessedFiles] = useState({});
|
||||
const [activeMethod, setActiveMethod] = useState(null);
|
||||
const [processing, setProcessing] = useState({});
|
||||
const [localSelectedModels, setLocalSelectedModels] = useState(null);
|
||||
const [fileType, setFileType] = useState(null);
|
||||
const [videoMethod, setVideoMethod] = useState('');
|
||||
const [videoId, setVideoId] = useState(null);
|
||||
const [videoProgress, setVideoProgress] = useState(0);
|
||||
const [statusMessage, setStatusMessage] = useState('');
|
||||
const [dragOver, setDragOver] = useState(false);
|
||||
|
||||
const [doZoom, setDoZoom] = useState(false);
|
||||
|
||||
const [transparent, setTransparent] = useState(true);
|
||||
const [colorBG, setColorBG] = useState('radial-gradient(circle, #fcdfa4 0%, #ffd83b 100%)'); //useState('radial-gradient(circle, #87CEFA 0%, #1E90FF 100%)');
|
||||
|
||||
const [imageWidth, setImageWidth] = useState('500px'); // Default width
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedFile) {
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
setImageWidth(`${image.width}px`);
|
||||
};
|
||||
image.src = selectedFile;
|
||||
}
|
||||
}, [selectedFile]);
|
||||
|
||||
|
||||
const theme = useTheme();
|
||||
const isPortrait = useMediaQuery('(orientation: portrait)');
|
||||
|
||||
const fileInputID = "fileInput" + fileID.toString();
|
||||
|
||||
const getModelAPIURL = (method)=>{
|
||||
console.log(method, ModelsInfo[method].apiUrlVar, process.env[ModelsInfo[method].apiUrlVar]);
|
||||
|
||||
return process.env[ModelsInfo[method].apiUrlVar];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!localSelectedModels) {
|
||||
setLocalSelectedModels(selectedModels);
|
||||
}
|
||||
}, [selectedModels, localSelectedModels]);
|
||||
|
||||
const dropZoneRef = useRef(null);
|
||||
|
||||
|
||||
|
||||
const processFile = useCallback(async (file, method) => {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('method', method);
|
||||
const isVideo = file.type.startsWith('video');
|
||||
const endpoint = isVideo ? 'remove_background_video' : 'remove_background';
|
||||
|
||||
try {
|
||||
const response = await axios.post(`${getModelAPIURL(method)}/${endpoint}/`, formData, {
|
||||
responseType: 'blob',
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
const fileUrl = URL.createObjectURL(response.data);
|
||||
return fileUrl;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error processing ${isVideo ? 'video' : 'image'} with ${method}:`, error);
|
||||
showErrorToast(`Error processing ${isVideo ? 'video' : 'image'} with ${method}`);
|
||||
}
|
||||
return null;
|
||||
}, [showErrorToast]);
|
||||
|
||||
const handleFileUpload = useCallback(async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
const isVideo = file.type.startsWith('video');
|
||||
setFileType(isVideo ? 'video' : 'image');
|
||||
setSelectedFile(URL.createObjectURL(file));
|
||||
setOriginalFilename(file.name);
|
||||
setProcessedFiles({});
|
||||
setActiveMethod(null);
|
||||
|
||||
const currentSelectedModels = {...selectedModels};
|
||||
setLocalSelectedModels(currentSelectedModels);
|
||||
|
||||
if (!isVideo) {
|
||||
const initialProcessing = Object.fromEntries(
|
||||
Object.entries(currentSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) => [method, true])
|
||||
);
|
||||
setProcessing(initialProcessing);
|
||||
|
||||
// Create a limit function that allows only 6 concurrent operations
|
||||
const limit = pLimit(6);
|
||||
|
||||
// Create an array of promises
|
||||
const promises = Object.entries(currentSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) =>
|
||||
limit(() => processFile(file, method).then(result => {
|
||||
setProcessedFiles(prev => ({...prev, [method]: result}));
|
||||
setProcessing(prev => ({...prev, [method]: false}));
|
||||
if (!activeMethod) {
|
||||
setActiveMethod(method);
|
||||
}
|
||||
}))
|
||||
);
|
||||
|
||||
// Wait for all promises to resolve
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
onProcessed();
|
||||
}, [processFile, onProcessed, selectedModels, activeMethod]);
|
||||
|
||||
const handleDragOver = (event) => {
|
||||
event.preventDefault();
|
||||
setDragOver(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (event) => {
|
||||
event.preventDefault();
|
||||
setDragOver(false);
|
||||
};
|
||||
|
||||
const handleDrop = useCallback((event) => {
|
||||
event.preventDefault();
|
||||
setDragOver(false);
|
||||
|
||||
const file = event.dataTransfer.files[0];
|
||||
if (file) {
|
||||
const fakeEvent = { target: { files: [file] } };
|
||||
handleFileUpload(fakeEvent);
|
||||
}
|
||||
}, [handleFileUpload]);
|
||||
|
||||
const handlePaste = useCallback((event) => {
|
||||
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
const blob = items[i].getAsFile();
|
||||
const file = new File([blob], "pasted-image.png", { type: blob.type });
|
||||
const fakeEvent = { target: { files: [file] } };
|
||||
handleFileUpload(fakeEvent);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [handleFileUpload]);
|
||||
|
||||
useEffect(() => {
|
||||
const dropZone = dropZoneRef.current;
|
||||
if (dropZone) {
|
||||
dropZone.addEventListener('paste', handlePaste);
|
||||
return () => {
|
||||
dropZone.removeEventListener('paste', handlePaste);
|
||||
};
|
||||
}
|
||||
}, [handlePaste]);
|
||||
|
||||
const handleMethodChange = (event, newMethod) => {
|
||||
if (newMethod !== null) {
|
||||
setActiveMethod(newMethod);
|
||||
}
|
||||
};
|
||||
|
||||
const handleVideoMethodChange = (event) => {
|
||||
setVideoMethod(event.target.value);
|
||||
};
|
||||
|
||||
const pollVideoStatus = useCallback(async (id, url) => {
|
||||
try {
|
||||
const response = await axios.get(`${url}/status/${id}`, {
|
||||
responseType: 'blob',
|
||||
withCredentials:false
|
||||
});
|
||||
|
||||
// Check if the response is JSON (status update) or blob (completed video)
|
||||
const contentType = response.headers['content-type'];
|
||||
if (contentType && contentType.indexOf('application/json') !== -1) {
|
||||
// It's a JSON response (status update)
|
||||
const data = await response.data.text().then(JSON.parse);
|
||||
if (data.status === 'processing') {
|
||||
setVideoProgress(data.progress);
|
||||
setStatusMessage(data.message);
|
||||
setTimeout(() => pollVideoStatus(id, url), 4000); // Poll every second
|
||||
} else if (data.status === 'error') {
|
||||
showErrorToast('Error processing video: ' + data.message);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
setStatusMessage('Error: ' + data.message);
|
||||
}
|
||||
} else {
|
||||
// It's a blob response (completed video)
|
||||
setVideoProgress(100);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
setActiveMethod(videoMethod);
|
||||
setStatusMessage('Processing complete');
|
||||
|
||||
const url = URL.createObjectURL(response.data);
|
||||
setProcessedFiles({ [videoMethod]: url });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling video status:', error);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
setStatusMessage('Error: Failed to get status update');
|
||||
showErrorToast('Error: Failed to get status update');
|
||||
}
|
||||
}, [videoMethod, showErrorToast]);
|
||||
|
||||
const getVideoDuration = (file) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video');
|
||||
video.preload = 'metadata';
|
||||
|
||||
video.onloadedmetadata = function() {
|
||||
window.URL.revokeObjectURL(video.src);
|
||||
resolve(video.duration);
|
||||
}
|
||||
|
||||
video.onerror = function() {
|
||||
reject("Invalid video. Please select another video file.");
|
||||
}
|
||||
|
||||
video.src = URL.createObjectURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
const handleProcessVideo = async () => {
|
||||
if (!selectedFile || !videoMethod) return;
|
||||
|
||||
setProcessing({ [videoMethod]: true });
|
||||
setVideoProgress(0);
|
||||
|
||||
try {
|
||||
const file = await fetch(selectedFile).then(r => r.blob());
|
||||
|
||||
// Estimate frame count
|
||||
const duration = await getVideoDuration(file);
|
||||
const estimatedFrameCount = Math.ceil(duration * 24); // Assuming 24 fps
|
||||
|
||||
//DISABLED VIDEO LENGTH LIMIT
|
||||
//if (estimatedFrameCount > 250) {
|
||||
// if (duration > 10){
|
||||
// showErrorToast(`Video too long (${estimatedFrameCount} estimated frames). Maximum allowed: 10 seconds.`);
|
||||
// setProcessing({ [videoMethod]: false });
|
||||
// return;
|
||||
// }
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('method', videoMethod);
|
||||
|
||||
const response = await axios.post(`${getModelAPIURL(videoMethod)}/remove_background_video/`, formData, {
|
||||
withCredentials: false,
|
||||
});
|
||||
|
||||
|
||||
setVideoId(response.data.video_id);
|
||||
pollVideoStatus(response.data.video_id, getModelAPIURL(videoMethod));
|
||||
} catch (error) {
|
||||
console.error('Error processing video:', error);
|
||||
setProcessing({ [videoMethod]: false });
|
||||
showErrorToast('Error processing video: ', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
if (fileType === 'video') {
|
||||
// Directly download the video file as a .webm
|
||||
const newFilename = `${originalFilename.split('.')[0]}_${activeMethod}.webm`;
|
||||
const link = document.createElement('a');
|
||||
link.href = processedFiles[activeMethod];
|
||||
link.download = newFilename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
// Handle image downloading with canvas manipulations
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const image = new Image();
|
||||
image.src = processedFiles[activeMethod];
|
||||
|
||||
image.onload = () => {
|
||||
canvas.width = image.width;
|
||||
canvas.height = image.height;
|
||||
|
||||
if (fileType === 'image' && transparent === false) {
|
||||
if (colorBG.includes("gradient")) {
|
||||
const tempDiv = document.createElement("div");
|
||||
tempDiv.style.display = 'none'; // Hide the div while it's appended
|
||||
tempDiv.style.background = colorBG;
|
||||
document.body.appendChild(tempDiv);
|
||||
const computedStyle = window.getComputedStyle(tempDiv);
|
||||
const bgImage = computedStyle.backgroundImage;
|
||||
document.body.removeChild(tempDiv);
|
||||
|
||||
if (bgImage.startsWith('linear-gradient')) {
|
||||
parseLinearGradient(ctx, bgImage, canvas.width, canvas.height);
|
||||
} else if (bgImage.startsWith('radial-gradient')) {
|
||||
parseRadialGradient(ctx, bgImage, canvas.width, canvas.height);
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = colorBG;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
const fileExtension = 'png';
|
||||
const newFilename = `${originalFilename.split('.')[0]}_${activeMethod}.${fileExtension}`;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = canvas.toDataURL(`image/${fileExtension}`);
|
||||
link.download = newFilename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function parseLinearGradient(ctx, bgImage, width, height) {
|
||||
const colors = bgImage.match(/rgba?\([^)]+\)/g);
|
||||
const linearGradient = ctx.createLinearGradient(0, 0, width, 0);
|
||||
colors.forEach((color, index) => {
|
||||
const position = index / (colors.length - 1);
|
||||
linearGradient.addColorStop(position, color);
|
||||
});
|
||||
ctx.fillStyle = linearGradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
function parseRadialGradient(ctx, bgImage, width, height) {
|
||||
const colors = bgImage.match(/rgba?\([^)]+\)/g);
|
||||
const radialGradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height) / 2);
|
||||
colors.forEach((color, index) => {
|
||||
const position = index / (colors.length - 1);
|
||||
radialGradient.addColorStop(position, color);
|
||||
});
|
||||
ctx.fillStyle = radialGradient;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
ref={dropZoneRef}
|
||||
tabIndex="0" // Make the box focusable
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
|
||||
// This allows the paste event to fire when the box is focused
|
||||
// and the user presses Ctrl+V or Cmd+V
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
border: (!selectedFile) ? '2px dashed' : 'none',
|
||||
borderColor: dragOver ? theme.palette.primary.main : theme.palette.text.disabled,
|
||||
borderRadius: 1,
|
||||
p: isPortrait ? 0 : 2,
|
||||
mt: 2,
|
||||
textAlign: 'center',
|
||||
cursor: !selectedFile && !processing ? 'pointer' : 'default',
|
||||
position: 'relative',
|
||||
backgroundColor: dragOver ? 'rgba(0, 0, 0, 0.1)' : 'transparent',
|
||||
transition: 'all 0.3s ease',
|
||||
}}
|
||||
onClick={() => !selectedFile && !Object.values(processing).some(Boolean) && document.getElementById(fileInputID).click()}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
{!selectedFile && (
|
||||
<input
|
||||
type="file"
|
||||
id={fileInputID}
|
||||
accept="image/*,video/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{selectedFile ? (
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: isPortrait ? 'column' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
width: '100%',
|
||||
maxWidth: '1024px',
|
||||
position: 'relative',
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
maxWidth: '1280px',
|
||||
mr: isPortrait ? 0 : 2,
|
||||
mb: isPortrait ? 2 : 0,
|
||||
width: '100%',
|
||||
}}>
|
||||
{fileType === 'image' ? (
|
||||
processedFiles[activeMethod] ? (
|
||||
<div
|
||||
className={transparent ? "checkerboard" : ""}
|
||||
style={!transparent ? { background: colorBG } : {}}
|
||||
>
|
||||
<ToggleButton
|
||||
value="zoom"
|
||||
selected={doZoom}
|
||||
onChange={() => setDoZoom(!doZoom)}
|
||||
aria-label="zoom in"
|
||||
size='small'
|
||||
color='primary'
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '3em',
|
||||
left: '3em',
|
||||
zIndex: 9999
|
||||
}}
|
||||
>
|
||||
<ZoomInIcon color='primary'/>
|
||||
</ToggleButton>
|
||||
|
||||
{doZoom && <Magnifier src={processedFiles[activeMethod]} width={'100%'}/>}
|
||||
|
||||
{!doZoom && <ImgComparisonSlider class="slider-example-focus">
|
||||
<img slot="first" src={selectedFile} alt="Original" style={{ width: '100%' }} />
|
||||
<img slot="second" src={processedFiles[activeMethod]} alt="Processed" style={{ width: '100%' }} />
|
||||
{false && <svg slot="handle" xmlns="http://www.w3.org/2000/svg" width="100" viewBox="-8 -3 16 6">
|
||||
<path stroke="#549ef7" d="M -5 -2 L -7 0 L -5 2 M -5 -2 L -5 2 M 5 -2 L 7 0 L 5 2 M 5 -2 L 5 2" strokeWidth="1" fill="#549ef7" vectorEffect="non-scaling-stroke"></path>
|
||||
</svg>}
|
||||
</ImgComparisonSlider>}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div style={{ position: 'relative', display: 'inline-block' }}>
|
||||
<CircularProgress style={{ position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }} />
|
||||
<img src={selectedFile} alt="Uploaded" style={{ width: '100%', display: "block", boxShadow: '0px 0px 10px 5px #6464647a' }} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
)
|
||||
) : (
|
||||
<video src={processedFiles[activeMethod] || selectedFile} controls style={{ width: '100%' }}>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: isPortrait ? 'absolute' : 'static',
|
||||
top: isPortrait ? '1em' : 'auto',
|
||||
right: isPortrait ? '1em' : 'auto',
|
||||
zIndex: isPortrait ? 1000 : 'auto',
|
||||
}}>
|
||||
{(!isPortrait || fileType !== 'video') && <Paper sx={{
|
||||
backgroundColor: isPortrait ? 'rgba(255,255,255,0.8)' : 'rgba(0,0,0,0)',
|
||||
padding: isPortrait ? 1 : 0,
|
||||
}} elevation={2}>
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
Methods
|
||||
</Typography>
|
||||
{selectedFile && localSelectedModels && fileType === 'image' && (
|
||||
<ToggleButtonGroup
|
||||
orientation="vertical"
|
||||
fullWidth
|
||||
value={activeMethod}
|
||||
exclusive
|
||||
color="warning"
|
||||
onChange={handleMethodChange}
|
||||
aria-label="background removal method"
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{Object.entries(localSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) => (
|
||||
<ToggleButton
|
||||
size="small"
|
||||
|
||||
key={method}
|
||||
value={method}
|
||||
aria-label={`${method} method`}
|
||||
disabled={processing[method]}
|
||||
sx={{
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="flex-start" width="100%">
|
||||
{isPortrait ? ModelsInfo[method].shortName : ModelsInfo[method].displayName}
|
||||
{processing[method] && <CircularProgress size={16} sx={{ ml: 1 }} />}
|
||||
</Box>
|
||||
</ToggleButton>
|
||||
))
|
||||
}
|
||||
</ToggleButtonGroup>
|
||||
)}
|
||||
</Paper>}
|
||||
{selectedFile && fileType === 'video' && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="video-method-label" >Method</InputLabel>
|
||||
<Select
|
||||
disabled={processing[videoMethod] || Object.keys(processedFiles).length > 0}
|
||||
labelId="video-method-label"
|
||||
value={videoMethod}
|
||||
label="Method"
|
||||
sx={{backgroundColor:isPortrait? theme.palette.info.contrastText :''}}
|
||||
onChange={handleVideoMethodChange}
|
||||
>
|
||||
{Object.entries(localSelectedModels)
|
||||
.filter(([_, isSelected]) => isSelected)
|
||||
.map(([method, _]) => (
|
||||
<MenuItem key={method} value={method}>{ModelsInfo[method].displayName}</MenuItem>
|
||||
))
|
||||
}
|
||||
</Select>
|
||||
</FormControl>
|
||||
{Object.keys(processedFiles).length === 0 && <Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleProcessVideo}
|
||||
disabled={!videoMethod || Object.values(processing).some(Boolean)}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
Process Video
|
||||
{processing[videoMethod] && <CircularProgress size={16} sx={{ ml: 1 }} />}
|
||||
</Button>}
|
||||
{processing[videoMethod] && (
|
||||
<Box sx={{ width: '100%', mt: 2 }}>
|
||||
<LinearProgress
|
||||
variant={videoProgress === 100 ? "indeterminate" : "determinate"}
|
||||
value={videoProgress}
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" align="center">
|
||||
{statusMessage} {videoProgress < 100 && `(${Math.round(videoProgress)}%)`}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{processedFiles[activeMethod] && (
|
||||
<>
|
||||
|
||||
|
||||
{!isPortrait && fileType=='image' && <FormControlLabel
|
||||
control={<Checkbox checked={transparent} onChange={(e)=>setTransparent(e.target.checked)} />}
|
||||
label="Transparent"
|
||||
sx={{color:theme.palette.text.primary}}
|
||||
/>}
|
||||
|
||||
{isPortrait && fileType=='image' && <ToggleButton sx={{backgroundColor:theme.palette.divider, p:0}} value="transparent" selected={!transparent} onChange={()=>{setTransparent(!transparent)}}><GradientIcon fontSize='large' color='primary'/></ToggleButton>}
|
||||
|
||||
{!transparent && fileType=='image' && <GradientPickerPopout
|
||||
buttonLabel={!isPortrait ? "Background" : ""}
|
||||
|
||||
color={colorBG}
|
||||
onChange={newColor => setColorBG(newColor)}
|
||||
/>}
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleDownload}
|
||||
endIcon={<DownloadIcon />}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{!isPortrait && "Download"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="h6" sx={{ color: theme.palette.text.primary }}>
|
||||
{dragOver ? "Click, drag and drop, or paste (ctrl-v) to upload an image or video" : "Click, drag and drop, or paste (ctrl-v) to upload an image or video"}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageUpload;
|
||||
80
bgbye/src/components/MethodSelector.js
Normal file
80
bgbye/src/components/MethodSelector.js
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Button, Popover, FormGroup, FormControlLabel, Checkbox, Typography, Divider, IconButton } from '@mui/material';
|
||||
import ChecklistIcon from '@mui/icons-material/Checklist';
|
||||
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
|
||||
import ModelsInfo from './ModelsInfo';
|
||||
|
||||
const MethodSelector = ({ selectedModels, handleModelChange }) => {
|
||||
const [anchorEl, setAnchorEl] = useState(null);
|
||||
|
||||
const handleClick = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
const id = open ? 'method-selector-popover' : undefined;
|
||||
|
||||
const handleInfoClick = (apiUrl) => {
|
||||
window.open(apiUrl, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="contained" onClick={handleClick} endIcon={<ChecklistIcon/>} size='small' sx={{mt:1}}>
|
||||
Select Methods
|
||||
</Button>
|
||||
<Popover
|
||||
elevation={2}
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
>
|
||||
<FormGroup sx={{ p: 1 }}>
|
||||
{Object.keys(selectedModels).map((model, index) => (
|
||||
<React.Fragment key={model}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedModels[model]}
|
||||
onChange={handleModelChange}
|
||||
name={model}
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span>{ModelsInfo[model].displayName}</span>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleInfoClick(ModelsInfo[model].sourceUrl)}
|
||||
style={{ marginLeft: '8px' }}
|
||||
>
|
||||
<HelpOutlineIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
{index < Object.keys(selectedModels).length - 1 && (
|
||||
<Divider orientation="horizontal" flexItem />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</FormGroup>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MethodSelector;
|
||||
65
bgbye/src/components/ModelsInfo.js
Normal file
65
bgbye/src/components/ModelsInfo.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// modelsInfo.js
|
||||
const ModelsInfo = {
|
||||
bria: {
|
||||
displayName: 'Bria RMBG1.4',
|
||||
shortName: "Bria",
|
||||
sourceUrl: 'https://huggingface.co/briaai/RMBG-1.4',
|
||||
apiUrlVar: 'REACT_APP_BRIA_URL'
|
||||
},
|
||||
inspyrenet: {
|
||||
displayName: 'InSPyReNet',
|
||||
shortName: "InSPyRe",
|
||||
sourceUrl: 'https://github.com/plemeri/transparent-background/tree/main',
|
||||
apiUrlVar: 'REACT_APP_INSPYRENET_URL'
|
||||
},
|
||||
u2net: {
|
||||
displayName: 'U2Net',
|
||||
shortName: "U2Net",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_U2NET_URL'
|
||||
},
|
||||
tracer: {
|
||||
displayName: 'Tracer-B7',
|
||||
shortName: "Tracer",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_TRACER_URL'
|
||||
},
|
||||
basnet: {
|
||||
displayName: 'BASNet',
|
||||
shortName: "BASNet",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_BASNET_URL'
|
||||
},
|
||||
deeplab: {
|
||||
displayName: 'DeepLabV3',
|
||||
shortName: "DeepLab",
|
||||
sourceUrl: 'https://github.com/OPHoperHPO/image-background-remove-tool#%EF%B8%8F-how-does-it-work',
|
||||
apiUrlVar: 'REACT_APP_DEEPLAB_URL'
|
||||
},
|
||||
u2net_human_seg: {
|
||||
displayName: 'U2Net Human',
|
||||
shortName: "U2Net🧍",
|
||||
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
|
||||
apiUrlVar: 'REACT_APP_U2NET_HUMAN_SEG_URL'
|
||||
},
|
||||
ormbg: {
|
||||
displayName: 'Open RMBG',
|
||||
shortName: "ORMBG",
|
||||
sourceUrl: 'https://huggingface.co/schirrmacher/ormbg',
|
||||
apiUrlVar: 'REACT_APP_ORMBG_URL'
|
||||
},
|
||||
'isnet-general-use': {
|
||||
displayName: 'ISNET-DIS',
|
||||
shortName: "DIS",
|
||||
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
|
||||
apiUrlVar: 'REACT_APP_ISNET_GENERAL_URL'
|
||||
},
|
||||
'isnet-anime': {
|
||||
displayName: 'ISNET-Anime',
|
||||
shortName: "Anime",
|
||||
sourceUrl: 'https://github.com/danielgatis/rembg?tab=readme-ov-file#models',
|
||||
apiUrlVar: 'REACT_APP_ISNET_ANIME_URL'
|
||||
}
|
||||
};
|
||||
|
||||
export default ModelsInfo;
|
||||
191
bgbye/src/components/ResponsiveAppBar.js
Normal file
191
bgbye/src/components/ResponsiveAppBar.js
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import * as React from 'react';
|
||||
import AppBar from '@mui/material/AppBar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import Container from '@mui/material/Container';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Button from '@mui/material/Button';
|
||||
import Tooltip from '@mui/material/Tooltip';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import AdbIcon from '@mui/icons-material/Adb';
|
||||
import { Deblur } from '@mui/icons-material';
|
||||
|
||||
import ModeNightIcon from '@mui/icons-material/ModeNight';
|
||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||
import Brightness3Icon from '@mui/icons-material/Brightness3';
|
||||
//const pages = ['Products', 'Pricing', 'Blog'];
|
||||
const pages = ['About', 'Help'];
|
||||
|
||||
const pageUrls = {
|
||||
About: 'https://fyrean.itch.io/',
|
||||
//Help: '#',
|
||||
};
|
||||
|
||||
const settings = ['Profile', 'Account', 'Dashboard', 'Logout'];
|
||||
|
||||
function ResponsiveAppBar({toggleTheme, darkMode}) {
|
||||
const [anchorElNav, setAnchorElNav] = React.useState(null);
|
||||
const [anchorElUser, setAnchorElUser] = React.useState(null);
|
||||
|
||||
const handleOpenNavMenu = (event) => {
|
||||
setAnchorElNav(event.currentTarget);
|
||||
};
|
||||
const handleOpenUserMenu = (event) => {
|
||||
setAnchorElUser(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseNavMenu = () => {
|
||||
setAnchorElNav(null);
|
||||
};
|
||||
|
||||
const handleCloseUserMenu = () => {
|
||||
setAnchorElUser(null);
|
||||
};
|
||||
|
||||
const handlePageClick = (page) => {
|
||||
if (!pageUrls[page])
|
||||
return;
|
||||
window.open(pageUrls[page], '_blank');
|
||||
handleCloseNavMenu();
|
||||
};
|
||||
|
||||
return (
|
||||
<AppBar position="fixed" sx={{}} >
|
||||
<Container maxWidth="xl" >
|
||||
<Toolbar disableGutters variant="dense" sx={{
|
||||
maxHeight:'20px',
|
||||
minHeight:0
|
||||
}}>
|
||||
<Deblur sx={{ display: { xs: 'none', md: 'flex' }, mr: 1 }} />
|
||||
<Typography
|
||||
variant="h6"
|
||||
noWrap
|
||||
component="a"
|
||||
href="https://bgbye.fyrean.com"
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: 'none', md: 'flex' },
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '.3rem',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
BGBye
|
||||
</Typography>
|
||||
|
||||
{false&&<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'none'} }}>
|
||||
<IconButton
|
||||
size="large"
|
||||
aria-label="account of current user"
|
||||
aria-controls="menu-appbar"
|
||||
aria-haspopup="true"
|
||||
onClick={handleOpenNavMenu}
|
||||
color="inherit"
|
||||
>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Menu
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElNav}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={Boolean(anchorElNav)}
|
||||
onClose={handleCloseNavMenu}
|
||||
sx={{
|
||||
display: { xs: 'block', md: 'none'}
|
||||
}}
|
||||
>
|
||||
{pages.map((page) => (
|
||||
<MenuItem key={page} onClick={() => handlePageClick(page)}>
|
||||
<Typography textAlign="center" variant="subtitle2">{page}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem key='theme' onClick={toggleTheme} variant="contained">
|
||||
{!darkMode ? <LightModeIcon /> : <ModeNightIcon />}
|
||||
{!darkMode ? ' Light' : ' Dark'}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>}
|
||||
<Deblur sx={{ display: { xs: 'flex', md: 'none' }, mr: 1 }} />
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
noWrap
|
||||
component="a"
|
||||
href="#app-bar-with-responsive-menu"
|
||||
sx={{
|
||||
mr: 2,
|
||||
display: { xs: 'flex', md: 'none' },
|
||||
flexGrow: 0,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 700,
|
||||
letterSpacing: '.3rem',
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
Matxinh
|
||||
</Typography>
|
||||
<Box sx={{ flexGrow: 1, display: { xs: 'flex', md: 'flex' } }}>
|
||||
{pages.map((page) => (
|
||||
<Button
|
||||
key={page}
|
||||
onClick={() => handlePageClick(page)}
|
||||
sx={{ my: 2, color: 'white', display: 'block' }}
|
||||
>
|
||||
{page}
|
||||
</Button>
|
||||
))}
|
||||
<IconButton onClick={toggleTheme} color={darkMode?'primary':'warning'} size="large">
|
||||
{!darkMode ? <LightModeIcon fontSize="inherit" /> : <Brightness3Icon fontSize="inherit" />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
{false&&<Box sx={{ flexGrow: 0 }}>
|
||||
<Tooltip title="Open settings">
|
||||
<IconButton onClick={handleOpenUserMenu} sx={{ p: 0 }}>
|
||||
<Avatar alt="Remy Sharp" src="/static/images/avatar/2.jpg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
sx={{ mt: '45px' }}
|
||||
id="menu-appbar"
|
||||
anchorEl={anchorElUser}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
open={Boolean(anchorElUser)}
|
||||
onClose={handleCloseUserMenu}
|
||||
>
|
||||
{settings.map((setting) => (
|
||||
<MenuItem key={setting} onClick={handleCloseUserMenu}>
|
||||
<Typography textAlign="center">{setting}</Typography>
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
</Menu>
|
||||
</Box>}
|
||||
</Toolbar>
|
||||
</Container>
|
||||
</AppBar>
|
||||
);
|
||||
}
|
||||
export default ResponsiveAppBar;
|
||||
50
bgbye/src/index.css
Normal file
50
bgbye/src/index.css
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.slider-example-focus {
|
||||
/* #549ef7 #ffa658
|
||||
The style below isn't necessary.
|
||||
It has been added for the sake of smoothness.
|
||||
|
||||
transition: box-shadow 200ms ease-in-out;*/
|
||||
box-shadow: 0px 0px 10px 5px #6464647a;
|
||||
display: block;
|
||||
|
||||
--default-handle-shadow: 0px 0px 5px rgba(0, 0, 0, 1);
|
||||
--divider-shadow: 0px 0px 5px rgba(0, 0, 0, 0.5);
|
||||
--divider-width:2;
|
||||
--default-handle-width: 100px;
|
||||
--divider-color: #549ef7;
|
||||
--default-handle-color: #549ef7;
|
||||
stroke-width: 3px;
|
||||
;
|
||||
}
|
||||
|
||||
.slider-example-focus:focus {
|
||||
outline: none;
|
||||
box-shadow: 0px 0px 10px 5px #6464647a;
|
||||
}
|
||||
|
||||
.checkerboard {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" y="0" width="10" height="10" fill="%23fff"/><rect x="0" y="10" width="10" height="10" fill="%23fff"/></svg>');
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
|
||||
.magnifying-glass {
|
||||
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><rect x="0" y="0" width="10" height="10" fill="%23ccc"/><rect x="10" y="10" width="10" height="10" fill="%23ccc"/><rect x="10" y="0" width="10" height="10" fill="%23fff"/><rect x="0" y="10" width="10" height="10" fill="%23fff"/></svg>');
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
17
bgbye/src/index.js
Normal file
17
bgbye/src/index.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
//import reportWebVitals from './reportWebVitals';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
//reportWebVitals();
|
||||
1
bgbye/src/logo.svg
Normal file
1
bgbye/src/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
13
bgbye/src/reportWebVitals.js
Normal file
13
bgbye/src/reportWebVitals.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
const reportWebVitals = onPerfEntry => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
5
bgbye/src/setupTests.js
Normal file
5
bgbye/src/setupTests.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
Loading…
Add table
Add a link
Reference in a new issue