Compare commits
No commits in common. "master" and "01fde3584446e27cfe1b688c8bb4ef3452958ba8" have entirely different histories.
master
...
01fde35844
2
.gitignore
vendored
@ -1,6 +1,4 @@
|
||||
*.db
|
||||
*.py[cod]
|
||||
.idea/
|
||||
.web
|
||||
.web/
|
||||
__pycache__/
|
39
.web/.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
/_static
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# DS_Store
|
||||
.DS_Store
|
21
.web/components/reflex/chakra_color_mode_provider.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { useColorMode as chakraUseColorMode } from "@chakra-ui/react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { useEffect } from "react"
|
||||
import { ColorModeContext } from "/utils/context.js"
|
||||
|
||||
export default function ChakraColorModeProvider({ children }) {
|
||||
const {colorMode, toggleColorMode} = chakraUseColorMode()
|
||||
const {theme, setTheme} = useTheme()
|
||||
|
||||
useEffect(() => {
|
||||
if (colorMode != theme) {
|
||||
toggleColorMode()
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
return (
|
||||
<ColorModeContext.Provider value={[ colorMode, toggleColorMode ]}>
|
||||
{children}
|
||||
</ColorModeContext.Provider>
|
||||
)
|
||||
}
|
20
.web/components/reflex/radix_themes_color_mode_provider.js
Normal file
@ -0,0 +1,20 @@
|
||||
import { useTheme } from "next-themes"
|
||||
import { useEffect, useState } from "react"
|
||||
import { ColorModeContext } from "/utils/context.js"
|
||||
|
||||
|
||||
export default function RadixThemesColorModeProvider({ children }) {
|
||||
const {theme, setTheme} = useTheme()
|
||||
const [colorMode, setColorMode] = useState("light")
|
||||
|
||||
useEffect(() => setColorMode(theme), [theme])
|
||||
|
||||
const toggleColorMode = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light")
|
||||
}
|
||||
return (
|
||||
<ColorModeContext.Provider value={[ colorMode, toggleColorMode ]}>
|
||||
{children}
|
||||
</ColorModeContext.Provider>
|
||||
)
|
||||
}
|
1
.web/env.json
Normal file
@ -0,0 +1 @@
|
||||
{"PING": "http://localhost:8000/ping", "EVENT": "ws://localhost:8000/_event", "UPLOAD": "http://localhost:8000/_upload"}
|
8
.web/jsconfig.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["public/*"]
|
||||
}
|
||||
}
|
||||
}
|
6
.web/next.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
basePath: "",
|
||||
compress: true,
|
||||
reactStrictMode: true,
|
||||
trailingSlash: true,
|
||||
};
|
3650
.web/package-lock.json
generated
Normal file
32
.web/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "reflex",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"export": "next build && next export -o _static",
|
||||
"export-sitemap": "next build && next-sitemap && next export -o _static",
|
||||
"prod": "next start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "^2.0.19",
|
||||
"@chakra-ui/react": "^2.6.1",
|
||||
"@chakra-ui/system": "^2.5.7",
|
||||
"@emotion/react": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"axios": "1.4.0",
|
||||
"focus-visible": "5.2.0",
|
||||
"framer-motion": "^10.16.4",
|
||||
"json5": "2.2.3",
|
||||
"next": "13.5.4",
|
||||
"next-sitemap": "4.1.8",
|
||||
"next-themes": "0.2.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"socket.io-client": "4.6.1",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"universal-cookie": "4.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "10.4.14",
|
||||
"postcss": "8.4.24"
|
||||
}
|
||||
}
|
88
.web/pages/404.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { Event, getAllLocalStorageItems, getRefValue, getRefValues, isTrue, preventDefault, refs, spreadArraysOrObjects, uploadFiles, useEventLoop } from "/utils/state"
|
||||
import { ColorModeContext, EventLoopContext, initialEvents, StateContext } from "/utils/context.js"
|
||||
import "focus-visible/dist/focus-visible"
|
||||
import { Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, Text } from "@chakra-ui/react"
|
||||
import { getEventURL } from "/utils/state.js"
|
||||
import Error from "next/error"
|
||||
import { useClientSideRouting } from "/utils/client_side_routing"
|
||||
import NextHead from "next/head"
|
||||
|
||||
|
||||
|
||||
export default function Component() {
|
||||
const state = useContext(StateContext)
|
||||
const router = useRouter()
|
||||
const [ colorMode, toggleColorMode ] = useContext(ColorModeContext)
|
||||
const focusRef = useRef();
|
||||
|
||||
// Main event loop.
|
||||
const [addEvents, connectError] = useContext(EventLoopContext)
|
||||
|
||||
// Set focus to the specified element.
|
||||
useEffect(() => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
})
|
||||
|
||||
// Route after the initial page hydration.
|
||||
useEffect(() => {
|
||||
const change_complete = () => addEvents(initialEvents())
|
||||
router.events.on('routeChangeComplete', change_complete)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', change_complete)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
const routeNotFound = useClientSideRouting()
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
{isTrue(connectError !== null) ? (
|
||||
<Fragment>
|
||||
<Modal isOpen={connectError !== null}>
|
||||
<ModalOverlay>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{`Connection Error`}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Text>
|
||||
{`Cannot connect to server: `}
|
||||
{(connectError !== null) ? connectError.message : ''}
|
||||
{`. Check if server is reachable at `}
|
||||
{getEventURL().href}
|
||||
</Text>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
<Fragment>
|
||||
{isTrue(routeNotFound) ? (
|
||||
<Fragment>
|
||||
<Error statusCode={404}/>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment>
|
||||
|
||||
</Fragment>
|
||||
)}
|
||||
</Fragment>
|
||||
<NextHead>
|
||||
<title>
|
||||
{`404 - Not Found`}
|
||||
</title>
|
||||
<meta content={`The page was not found`} name={`description`}/>
|
||||
<meta content={`favicon.ico`} property={`og:image`}/>
|
||||
</NextHead>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
46
.web/pages/_app.js
Normal file
@ -0,0 +1,46 @@
|
||||
import { ChakraProvider, extendTheme } from "@chakra-ui/react"
|
||||
import theme from "/utils/theme.js"
|
||||
import { css, Global } from "@emotion/react"
|
||||
import ChakraColorModeProvider from "/components/reflex/chakra_color_mode_provider.js"
|
||||
|
||||
|
||||
import { EventLoopProvider } from "/utils/context.js";
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
|
||||
|
||||
import '/styles/styles.css'
|
||||
|
||||
const GlobalStyles = css`
|
||||
/* Hide the blue border around Chakra components. */
|
||||
.js-focus-visible :focus:not([data-focus-visible-added]) {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
function AppWrap({children}) {
|
||||
|
||||
|
||||
return (
|
||||
<ChakraProvider theme={extendTheme(theme)}>
|
||||
<Global styles={GlobalStyles}/>
|
||||
<ChakraColorModeProvider>
|
||||
{children}
|
||||
</ChakraColorModeProvider>
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<ThemeProvider defaultTheme="light" storageKey="chakra-ui-color-mode" attribute="class">
|
||||
<AppWrap>
|
||||
<EventLoopProvider>
|
||||
<Component {...pageProps} />
|
||||
</EventLoopProvider>
|
||||
</AppWrap>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
15
.web/pages/_document.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { Head, Html, Main, NextScript } from "next/document"
|
||||
|
||||
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html>
|
||||
<Head/>
|
||||
<body>
|
||||
<Main/>
|
||||
<NextScript/>
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
158
.web/pages/dashboard.js
Normal file
@ -0,0 +1,158 @@
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { Event, getAllLocalStorageItems, getRefValue, getRefValues, isTrue, preventDefault, refs, spreadArraysOrObjects, uploadFiles, useEventLoop } from "/utils/state"
|
||||
import { ColorModeContext, EventLoopContext, initialEvents, StateContext } from "/utils/context.js"
|
||||
import "focus-visible/dist/focus-visible"
|
||||
import { Box, Heading, HStack, Image, Link, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, Spacer, Text, VStack } from "@chakra-ui/react"
|
||||
import { getEventURL } from "/utils/state.js"
|
||||
import NextLink from "next/link"
|
||||
import { HamburgerIcon } from "@chakra-ui/icons"
|
||||
import NextHead from "next/head"
|
||||
|
||||
|
||||
|
||||
export default function Component() {
|
||||
const state = useContext(StateContext)
|
||||
const router = useRouter()
|
||||
const [ colorMode, toggleColorMode ] = useContext(ColorModeContext)
|
||||
const focusRef = useRef();
|
||||
|
||||
// Main event loop.
|
||||
const [addEvents, connectError] = useContext(EventLoopContext)
|
||||
|
||||
// Set focus to the specified element.
|
||||
useEffect(() => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
})
|
||||
|
||||
// Route after the initial page hydration.
|
||||
useEffect(() => {
|
||||
const change_complete = () => addEvents(initialEvents())
|
||||
router.events.on('routeChangeComplete', change_complete)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', change_complete)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
{isTrue(connectError !== null) ? (
|
||||
<Fragment>
|
||||
<Modal isOpen={connectError !== null}>
|
||||
<ModalOverlay>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{`Connection Error`}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Text>
|
||||
{`Cannot connect to server: `}
|
||||
{(connectError !== null) ? connectError.message : ''}
|
||||
{`. Check if server is reachable at `}
|
||||
{getEventURL().href}
|
||||
</Text>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
<HStack alignItems={`flex-start`} sx={{"transition": "left 0.5s, width 0.5s", "position": "relative"}}>
|
||||
<Box sx={{"display": ["none", "none", "block"], "minWidth": "20em", "height": "100%", "position": "sticky", "top": "0px", "borderRight": "1px solid #F4F3F6"}}>
|
||||
<VStack sx={{"height": "100%"}}>
|
||||
<HStack sx={{"width": "100%", "borderBottom": "1px solid #F4F3F6", "padding": "1em"}}>
|
||||
<Image src={`/icon.png`} sx={{"height": "4.5em"}}/>
|
||||
<Spacer/>
|
||||
</HStack>
|
||||
<VStack alignItems={`flex-start`} sx={{"width": "100%", "overflowY": "auto", "padding": "1em"}}>
|
||||
<Link as={NextLink} href={`/`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/strona g\\u0142\\u00f3wna") || (((state.router.page.path === "/") && "Strona g\\u0142\\u00f3wna") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/strona g\\u0142\\u00f3wna") || (((state.router.page.path === "/") && "Strona g\\u0142\\u00f3wna") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Strona główna`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
<Link as={NextLink} href={`/dashboard`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/dashboard") || (((state.router.page.path === "/") && "Dashboard") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/dashboard") || (((state.router.page.path === "/") && "Dashboard") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Dashboard`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
<Link as={NextLink} href={`/settings`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/settings") || (((state.router.page.path === "/") && "Settings") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/settings") || (((state.router.page.path === "/") && "Settings") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Settings`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
</VStack>
|
||||
<Spacer/>
|
||||
</VStack>
|
||||
</Box>
|
||||
<Box sx={{"paddingTop": "5em", "paddingX": ["auto", "2em"], "flex": "1"}}>
|
||||
<Box sx={{"alignItems": "flex-start", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "borderRadius": "0.375rem", "padding": "1em", "marginBottom": "2em"}}>
|
||||
<VStack>
|
||||
<Heading sx={{"fontSize": "3em"}}>
|
||||
{`BlurMe`}
|
||||
</Heading>
|
||||
<Text>
|
||||
{`Dodaj zdjęcia, które chcesz zanonimizować!`}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{"position": "fixed", "right": "1.5em", "top": "1.5em", "zIndex": "500"}}>
|
||||
<Menu>
|
||||
<MenuButton sx={{"width": "3em", "height": "3em", "backgroundColor": "white", "border": "1px solid #F4F3F6", "borderRadius": "0.375rem"}}>
|
||||
<HamburgerIcon sx={{"size": "4em", "color": "black"}}/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/`} sx={{"width": "100%"}}>
|
||||
{`Strona główna`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/dashboard`} sx={{"width": "100%"}}>
|
||||
{`Dashboard`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/settings`} sx={{"width": "100%"}}>
|
||||
{`Settings`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuDivider/>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`https://github.com/reflex-dev`} sx={{"width": "100%"}}>
|
||||
{`About`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`mailto:founders@=reflex.dev`} sx={{"width": "100%"}}>
|
||||
{`Contact`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</HStack>
|
||||
<NextHead>
|
||||
<title>
|
||||
{`Dashboard`}
|
||||
</title>
|
||||
<meta content={`A Reflex app.`} name={`description`}/>
|
||||
<meta content={`favicon.ico`} property={`og:image`}/>
|
||||
<meta content={`width=device-width, shrink-to-fit=no, initial-scale=1`} name={`viewport`}/>
|
||||
</NextHead>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
250
.web/pages/index.js
Normal file
@ -0,0 +1,250 @@
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { Event, getAllLocalStorageItems, getRefValue, getRefValues, isTrue, preventDefault, refs, spreadArraysOrObjects, uploadFiles, useEventLoop } from "/utils/state"
|
||||
import { ColorModeContext, EventLoopContext, initialEvents, StateContext } from "/utils/context.js"
|
||||
import "focus-visible/dist/focus-visible"
|
||||
import { Box, Button, Heading, HStack, Image, Link, List, ListItem, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, OrderedList, Spacer, Text, VStack, Wrap, WrapItem } from "@chakra-ui/react"
|
||||
import { getEventURL } from "/utils/state.js"
|
||||
import NextLink from "next/link"
|
||||
import { HamburgerIcon } from "@chakra-ui/icons"
|
||||
import NextHead from "next/head"
|
||||
|
||||
|
||||
|
||||
export default function Component() {
|
||||
const state = useContext(StateContext)
|
||||
const router = useRouter()
|
||||
const [ colorMode, toggleColorMode ] = useContext(ColorModeContext)
|
||||
const focusRef = useRef();
|
||||
|
||||
// Main event loop.
|
||||
const [addEvents, connectError] = useContext(EventLoopContext)
|
||||
|
||||
// Set focus to the specified element.
|
||||
useEffect(() => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
})
|
||||
|
||||
// Route after the initial page hydration.
|
||||
useEffect(() => {
|
||||
const change_complete = () => addEvents(initialEvents())
|
||||
router.events.on('routeChangeComplete', change_complete)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', change_complete)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
{isTrue(connectError !== null) ? (
|
||||
<Fragment>
|
||||
<Modal isOpen={connectError !== null}>
|
||||
<ModalOverlay>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{`Connection Error`}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Text>
|
||||
{`Cannot connect to server: `}
|
||||
{(connectError !== null) ? connectError.message : ''}
|
||||
{`. Check if server is reachable at `}
|
||||
{getEventURL().href}
|
||||
</Text>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
<HStack alignItems={`flex-start`} sx={{"transition": "left 0.5s, width 0.5s", "position": "relative"}}>
|
||||
<Box sx={{"display": ["none", "none", "block"], "minWidth": "20em", "height": "100%", "position": "sticky", "top": "0px", "borderRight": "1px solid #F4F3F6"}}>
|
||||
<VStack sx={{"height": "100%"}}>
|
||||
<HStack sx={{"width": "100%", "borderBottom": "1px solid #F4F3F6", "padding": "1em"}}>
|
||||
<Image src={`/icon.png`} sx={{"height": "4.5em"}}/>
|
||||
<Spacer/>
|
||||
</HStack>
|
||||
<VStack alignItems={`flex-start`} sx={{"width": "100%", "overflowY": "auto", "padding": "1em"}}>
|
||||
<Link as={NextLink} href={`/`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/strona g\\u0142\\u00f3wna") || (((state.router.page.path === "/") && "Strona g\\u0142\\u00f3wna") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/strona g\\u0142\\u00f3wna") || (((state.router.page.path === "/") && "Strona g\\u0142\\u00f3wna") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Strona główna`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
<Link as={NextLink} href={`/dashboard`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/dashboard") || (((state.router.page.path === "/") && "Dashboard") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/dashboard") || (((state.router.page.path === "/") && "Dashboard") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Dashboard`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
<Link as={NextLink} href={`/settings`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/settings") || (((state.router.page.path === "/") && "Settings") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/settings") || (((state.router.page.path === "/") && "Settings") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Settings`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
</VStack>
|
||||
<Spacer/>
|
||||
</VStack>
|
||||
</Box>
|
||||
<Box sx={{"paddingTop": "5em", "paddingX": ["auto", "2em"], "flex": "1"}}>
|
||||
<Box sx={{"alignItems": "flex-start", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "borderRadius": "0.375rem", "padding": "1em", "marginBottom": "2em"}}>
|
||||
<VStack>
|
||||
<Heading sx={{"fontSize": "3em", "marginBottom": "25px", "backgroundImage": "linear-gradient(271.68deg, #7566fe 0.75%, #f96caf 88.52%)", "backgroundClip": "text", "padding": "10px"}}>
|
||||
{`Witaj w BlurMe!`}
|
||||
</Heading>
|
||||
<Wrap>
|
||||
<WrapItem sx={{"flex-direction": "column", "w": "45%"}}>
|
||||
<Text sx={{"fontSize": "1.2em"}}>
|
||||
{`Aplikacja do anonimizacji zdjęć: usunięcie wybranych informacji identyfikujących ze zdjęć z wydarzeń i miejsc publicznych nigdy nie było tak proste!`}
|
||||
</Text>
|
||||
</WrapItem>
|
||||
<WrapItem>
|
||||
<Text>
|
||||
{`Tu będzie obrazek`}
|
||||
</Text>
|
||||
</WrapItem>
|
||||
</Wrap>
|
||||
<Link as={NextLink} href={`settings`} sx={{"color": "rgb(107,99,246)", "button": true, "padding": "50xp", "fontSize": "1.2em"}}>
|
||||
<Button>
|
||||
{`Zacznij anonimizować`}
|
||||
</Button>
|
||||
</Link>
|
||||
<Text sx={{"fontSize": "1.5em", "fontWeight": "bold", "marginTop": "10px", "marginBottom": "10px"}}>
|
||||
{`Dlaczego Blurme?`}
|
||||
</Text>
|
||||
<List sx={{"textAlign": "left"}}>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Prywatność: `}
|
||||
</Text>
|
||||
{`Nasza aplikacja zapewnia pełną ochronę prywatności, eliminując ryzyko identyfikacji osób niezwiązanych z anonimizacją.`}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Prosta obsługa: `}
|
||||
</Text>
|
||||
{`Intuicyjny interfejs użytkownika sprawia, że korzystanie z Blurme jest łatwe dla każdego.`}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Elastyczność: `}
|
||||
</Text>
|
||||
{`Wybierz, która twarz na zdjęciu ma pozostać niezmieniona, a my zadbamy o resztę. Indywidualizuj swoje zdjęcia zgodnie z własnymi potrzebami.`}
|
||||
</ListItem>
|
||||
</List>
|
||||
<Text sx={{"fontSize": "1.5em", "fontWeight": "bold", "marginTop": "20px", "marginBottom": "10px"}}>
|
||||
{`Jak to działa?`}
|
||||
</Text>
|
||||
<OrderedList>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Wgraj Zdjęcie: `}
|
||||
</Text>
|
||||
{`Prosty proces rozpoczyna się od dodania zdjęcia, które chcesz anonimizować.`}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Zaznacz Twarz do Ochrony: `}
|
||||
</Text>
|
||||
{`Oznacz twarz, którą chcesz zachować.`}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Anonimizuj Resztę: `}
|
||||
</Text>
|
||||
{`Naciśnij przycisk, a Blurme automatycznie zastosuje efekt rozmycia do wszystkich obszarów poza zaznaczoną twarzą.`}
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Text as={`b`}>
|
||||
{`Pobierz i Podziel Się: `}
|
||||
</Text>
|
||||
{`Gotowe! Pobierz anonimizowane zdjęcie i dziel się nim bez obaw o prywatność.`}
|
||||
</ListItem>
|
||||
</OrderedList>
|
||||
<Text sx={{"fontSize": "1.5em", "fontWeight": "bold", "marginTop": "20px", "marginBottom": "10px"}}>
|
||||
{`Do Czego Może Ci Się Przydać?`}
|
||||
</Text>
|
||||
<List>
|
||||
<ListItem sx={{"textAlign": "left"}}>
|
||||
<Text as={`b`}>
|
||||
{`Ochrona Tożsamości: `}
|
||||
</Text>
|
||||
{`Idealne do usuwania twarzy nieznajomych lub przypadkowych przechodniów z tła.`}
|
||||
</ListItem>
|
||||
<ListItem sx={{"textAlign": "left"}}>
|
||||
<Text as={`b`}>
|
||||
{`Zachowanie Prywatności: `}
|
||||
</Text>
|
||||
{`Przydatne w sytuacjach, gdzie chcesz udostępnić zdjęcia publicznie, ale z zachowaniem prywatności pewnych osób.`}
|
||||
</ListItem>
|
||||
<ListItem sx={{"textAlign": "left"}}>
|
||||
<Text as={`b`}>
|
||||
{`Kreatywność: `}
|
||||
</Text>
|
||||
{`Wyraź swoją kreatywność, zachowując jednocześnie istotne elementy na zdjęciach.`}
|
||||
</ListItem>
|
||||
</List>
|
||||
<Text sx={{"textAlign": "center"}}>
|
||||
{`Blurme to więcej niż tylko narzędzie - to rozwiązanie, które pozwala Ci kontrolować, jak prezentujesz swoje zdjęcia online, zachowując jednocześnie pełną prywatność. Przekształć swoje obrazy już teraz!`}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{"position": "fixed", "right": "1.5em", "top": "1.5em", "zIndex": "500"}}>
|
||||
<Menu>
|
||||
<MenuButton sx={{"width": "3em", "height": "3em", "backgroundColor": "white", "border": "1px solid #F4F3F6", "borderRadius": "0.375rem"}}>
|
||||
<HamburgerIcon sx={{"size": "4em", "color": "black"}}/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/`} sx={{"width": "100%"}}>
|
||||
{`Strona główna`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/dashboard`} sx={{"width": "100%"}}>
|
||||
{`Dashboard`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/settings`} sx={{"width": "100%"}}>
|
||||
{`Settings`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuDivider/>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`https://github.com/reflex-dev`} sx={{"width": "100%"}}>
|
||||
{`About`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`mailto:founders@=reflex.dev`} sx={{"width": "100%"}}>
|
||||
{`Contact`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</HStack>
|
||||
<NextHead>
|
||||
<title>
|
||||
{`Strona główna`}
|
||||
</title>
|
||||
<meta content={`A Reflex app.`} name={`description`}/>
|
||||
<meta content={`/github.svg`} property={`og:image`}/>
|
||||
<meta content={`width=device-width, shrink-to-fit=no, initial-scale=1`} name={`viewport`}/>
|
||||
</NextHead>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
158
.web/pages/settings.js
Normal file
@ -0,0 +1,158 @@
|
||||
import { Fragment, useContext, useEffect, useRef, useState } from "react"
|
||||
import { useRouter } from "next/router"
|
||||
import { Event, getAllLocalStorageItems, getRefValue, getRefValues, isTrue, preventDefault, refs, spreadArraysOrObjects, uploadFiles, useEventLoop } from "/utils/state"
|
||||
import { ColorModeContext, EventLoopContext, initialEvents, StateContext } from "/utils/context.js"
|
||||
import "focus-visible/dist/focus-visible"
|
||||
import { Box, Heading, HStack, Image, Link, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Modal, ModalBody, ModalContent, ModalHeader, ModalOverlay, Spacer, Text, VStack } from "@chakra-ui/react"
|
||||
import { getEventURL } from "/utils/state.js"
|
||||
import NextLink from "next/link"
|
||||
import { HamburgerIcon } from "@chakra-ui/icons"
|
||||
import NextHead from "next/head"
|
||||
|
||||
|
||||
|
||||
export default function Component() {
|
||||
const state = useContext(StateContext)
|
||||
const router = useRouter()
|
||||
const [ colorMode, toggleColorMode ] = useContext(ColorModeContext)
|
||||
const focusRef = useRef();
|
||||
|
||||
// Main event loop.
|
||||
const [addEvents, connectError] = useContext(EventLoopContext)
|
||||
|
||||
// Set focus to the specified element.
|
||||
useEffect(() => {
|
||||
if (focusRef.current) {
|
||||
focusRef.current.focus();
|
||||
}
|
||||
})
|
||||
|
||||
// Route after the initial page hydration.
|
||||
useEffect(() => {
|
||||
const change_complete = () => addEvents(initialEvents())
|
||||
router.events.on('routeChangeComplete', change_complete)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', change_complete)
|
||||
}
|
||||
}, [router])
|
||||
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Fragment>
|
||||
{isTrue(connectError !== null) ? (
|
||||
<Fragment>
|
||||
<Modal isOpen={connectError !== null}>
|
||||
<ModalOverlay>
|
||||
<ModalContent>
|
||||
<ModalHeader>
|
||||
{`Connection Error`}
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<Text>
|
||||
{`Cannot connect to server: `}
|
||||
{(connectError !== null) ? connectError.message : ''}
|
||||
{`. Check if server is reachable at `}
|
||||
{getEventURL().href}
|
||||
</Text>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</ModalOverlay>
|
||||
</Modal>
|
||||
</Fragment>
|
||||
) : (
|
||||
<Fragment/>
|
||||
)}
|
||||
</Fragment>
|
||||
<HStack alignItems={`flex-start`} sx={{"transition": "left 0.5s, width 0.5s", "position": "relative"}}>
|
||||
<Box sx={{"display": ["none", "none", "block"], "minWidth": "20em", "height": "100%", "position": "sticky", "top": "0px", "borderRight": "1px solid #F4F3F6"}}>
|
||||
<VStack sx={{"height": "100%"}}>
|
||||
<HStack sx={{"width": "100%", "borderBottom": "1px solid #F4F3F6", "padding": "1em"}}>
|
||||
<Image src={`/icon.png`} sx={{"height": "4.5em"}}/>
|
||||
<Spacer/>
|
||||
</HStack>
|
||||
<VStack alignItems={`flex-start`} sx={{"width": "100%", "overflowY": "auto", "padding": "1em"}}>
|
||||
<Link as={NextLink} href={`/`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/strona g\\u0142\\u00f3wna") || (((state.router.page.path === "/") && "Strona g\\u0142\\u00f3wna") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/strona g\\u0142\\u00f3wna") || (((state.router.page.path === "/") && "Strona g\\u0142\\u00f3wna") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Strona główna`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
<Link as={NextLink} href={`/dashboard`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/dashboard") || (((state.router.page.path === "/") && "Dashboard") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/dashboard") || (((state.router.page.path === "/") && "Dashboard") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Dashboard`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
<Link as={NextLink} href={`/settings`} sx={{"width": "100%"}}>
|
||||
<HStack sx={{"bg": isTrue((state.router.page.path === "/settings") || (((state.router.page.path === "/") && "Settings") === "Strona g\\u0142\\u00f3wna")) ? `#d9a2ca` : `transparent`, "color": isTrue((state.router.page.path === "/settings") || (((state.router.page.path === "/") && "Settings") === "Strona g\\u0142\\u00f3wna")) ? `#1A1060` : `black`, "borderRadius": "0.375rem", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "width": "100%", "paddingX": "1em", "height": "2em"}}>
|
||||
<Text>
|
||||
{`Settings`}
|
||||
</Text>
|
||||
</HStack>
|
||||
</Link>
|
||||
</VStack>
|
||||
<Spacer/>
|
||||
</VStack>
|
||||
</Box>
|
||||
<Box sx={{"paddingTop": "5em", "paddingX": ["auto", "2em"], "flex": "1"}}>
|
||||
<Box sx={{"alignItems": "flex-start", "boxShadow": "0px 0px 0px 1px rgba(84, 82, 95, 0.14)", "borderRadius": "0.375rem", "padding": "1em", "marginBottom": "2em"}}>
|
||||
<VStack>
|
||||
<Heading sx={{"fontSize": "3em"}}>
|
||||
{`BlurMe`}
|
||||
</Heading>
|
||||
<Text>
|
||||
{`Dodaj zdjęcia, które chcesz zanonimizować!`}
|
||||
</Text>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{"position": "fixed", "right": "1.5em", "top": "1.5em", "zIndex": "500"}}>
|
||||
<Menu>
|
||||
<MenuButton sx={{"width": "3em", "height": "3em", "backgroundColor": "white", "border": "1px solid #F4F3F6", "borderRadius": "0.375rem"}}>
|
||||
<HamburgerIcon sx={{"size": "4em", "color": "black"}}/>
|
||||
</MenuButton>
|
||||
<MenuList>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/`} sx={{"width": "100%"}}>
|
||||
{`Strona główna`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/dashboard`} sx={{"width": "100%"}}>
|
||||
{`Dashboard`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`/settings`} sx={{"width": "100%"}}>
|
||||
{`Settings`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuDivider/>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`https://github.com/reflex-dev`} sx={{"width": "100%"}}>
|
||||
{`About`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem sx={{"_hover": {"bg": "#d9a2ca"}}}>
|
||||
<Link as={NextLink} href={`mailto:founders@=reflex.dev`} sx={{"width": "100%"}}>
|
||||
{`Contact`}
|
||||
</Link>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Box>
|
||||
</HStack>
|
||||
<NextHead>
|
||||
<title>
|
||||
{`Settings`}
|
||||
</title>
|
||||
<meta content={`A Reflex app.`} name={`description`}/>
|
||||
<meta content={`favicon.ico`} property={`og:image`}/>
|
||||
<meta content={`width=device-width, shrink-to-fit=no, initial-scale=1`} name={`viewport`}/>
|
||||
</NextHead>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
6
.web/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
BIN
.web/public/favicon.ico
Normal file
After Width: | Height: | Size: 11 KiB |
10
.web/public/github.svg
Normal file
@ -0,0 +1,10 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Github" clip-path="url(#clip0_469_1929)">
|
||||
<path id="Vector" d="M8.0004 0.587524C3.80139 0.587524 0.400391 3.98851 0.400391 8.1875C0.400391 11.5505 2.57589 14.391 5.59689 15.398C5.97689 15.4645 6.11939 15.2365 6.11939 15.037C6.11939 14.8565 6.10989 14.258 6.10989 13.6215C4.20039 13.973 3.70639 13.156 3.55439 12.7285C3.46889 12.51 3.09839 11.8355 2.77539 11.655C2.50939 11.5125 2.12939 11.161 2.76589 11.1515C3.36439 11.142 3.79189 11.7025 3.93439 11.9305C4.61839 13.08 5.71089 12.757 6.14789 12.5575C6.21439 12.0635 6.41388 11.731 6.6324 11.541C4.94139 11.351 3.17439 10.6955 3.17439 7.7885C3.17439 6.962 3.46889 6.27801 3.95339 5.74601C3.87739 5.55601 3.61139 4.77701 4.02939 3.73201C4.02939 3.73201 4.66589 3.53251 6.11939 4.51101C6.7274 4.34001 7.3734 4.25451 8.0194 4.25451C8.6654 4.25451 9.3114 4.34001 9.9194 4.51101C11.3729 3.52301 12.0094 3.73201 12.0094 3.73201C12.4274 4.77701 12.1614 5.55601 12.0854 5.74601C12.5699 6.27801 12.8644 6.9525 12.8644 7.7885C12.8644 10.705 11.0879 11.351 9.3969 11.541C9.6724 11.7785 9.9099 12.2345 9.9099 12.947C9.9099 13.9635 9.9004 14.7805 9.9004 15.037C9.9004 15.2365 10.0429 15.474 10.4229 15.398C13.5165 14.3536 15.5996 11.4527 15.6004 8.1875C15.6004 3.98851 12.1994 0.587524 8.0004 0.587524Z" fill="#494369"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_469_1929">
|
||||
<rect width="16" height="16" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
.web/public/icon.png
Normal file
After Width: | Height: | Size: 95 KiB |
BIN
.web/public/logo.png
Normal file
After Width: | Height: | Size: 93 KiB |
13
.web/public/paneleft.svg
Normal file
@ -0,0 +1,13 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="PaneLeft" clip-path="url(#clip0_469_1942)">
|
||||
<g id="Vector">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.80217 0.525009C7.34654 0.525009 6.97717 0.894373 6.97717 1.35001V10.65C6.97717 11.1056 7.34654 11.475 7.80217 11.475H10.6522C11.1078 11.475 11.4772 11.1056 11.4772 10.65V1.35001C11.4772 0.894373 11.1078 0.525009 10.6522 0.525009H7.80217ZM8.02717 10.425V1.57501H10.4272V10.425H8.02717Z" fill="#494369"/>
|
||||
<path d="M3.78215 8.14502L2.16213 6.525H5.92717V5.475H2.16213L3.78215 3.85498L3.03969 3.11252L0.523438 5.62877V6.37123L3.03969 8.88748L3.78215 8.14502Z" fill="#494369"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_469_1942">
|
||||
<rect width="12" height="12" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 807 B |
1
.web/reflex.json
Normal file
@ -0,0 +1 @@
|
||||
{"version": "0.3.2", "project_hash": 99276045562630266666819420787377202120}
|
1015
.web/styles/code/prism.js
Normal file
1
.web/styles/styles.css
Normal file
@ -0,0 +1 @@
|
||||
@import url('./tailwind.css');
|
3
.web/styles/tailwind.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
7
.web/tailwind.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./pages/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: null,
|
||||
plugins: [
|
||||
],
|
||||
};
|
36
.web/utils/client_side_routing.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
/**
|
||||
* React hook for use in /404 page to enable client-side routing.
|
||||
*
|
||||
* Uses the next/router to redirect to the provided URL when loading
|
||||
* the 404 page (for example as a fallback in static hosting situations).
|
||||
*
|
||||
* @returns {boolean} routeNotFound - true if the current route is an actual 404
|
||||
*/
|
||||
export const useClientSideRouting = () => {
|
||||
const [routeNotFound, setRouteNotFound] = useState(false)
|
||||
const didRedirect = useRef(false)
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
if (
|
||||
router.isReady &&
|
||||
!didRedirect.current // have not tried redirecting yet
|
||||
) {
|
||||
didRedirect.current = true // never redirect twice to avoid "Hard Navigate" error
|
||||
// attempt to redirect to the route in the browser address bar once
|
||||
router.replace({
|
||||
pathname: window.location.pathname,
|
||||
query: window.location.search.slice(1),
|
||||
})
|
||||
.catch((e) => {
|
||||
setRouteNotFound(true) // navigation failed, so this is a real 404
|
||||
})
|
||||
}
|
||||
}, [router.isReady]);
|
||||
|
||||
// Return the reactive bool, to avoid flashing 404 page until we know for sure
|
||||
// the route is not found.
|
||||
return routeNotFound
|
||||
}
|
5
.web/utils/components.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { memo } from "react"
|
||||
import { E, isTrue } from "/utils/state"
|
||||
|
||||
|
||||
|
30
.web/utils/context.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { createContext, useState } from "react"
|
||||
import { Event, hydrateClientStorage, useEventLoop } from "/utils/state.js"
|
||||
|
||||
export const initialState = {"is_hydrated": false, "router": {"session": {"client_token": "", "client_ip": "", "session_id": ""}, "headers": {"host": "", "origin": "", "upgrade": "", "connection": "", "pragma": "", "cache_control": "", "user_agent": "", "sec_websocket_version": "", "sec_websocket_key": "", "sec_websocket_extensions": "", "accept_encoding": "", "accept_language": ""}, "page": {"host": "", "path": "", "raw_path": "", "full_path": "", "full_raw_path": "", "params": {}}}}
|
||||
|
||||
export const ColorModeContext = createContext(null);
|
||||
export const StateContext = createContext(null);
|
||||
export const EventLoopContext = createContext(null);
|
||||
export const clientStorage = {"cookies": {}, "local_storage": {}}
|
||||
|
||||
export const initialEvents = () => [
|
||||
Event('state.hydrate', hydrateClientStorage(clientStorage)),
|
||||
]
|
||||
|
||||
export const isDevMode = true
|
||||
|
||||
export function EventLoopProvider({ children }) {
|
||||
const [state, addEvents, connectError] = useEventLoop(
|
||||
initialState,
|
||||
initialEvents,
|
||||
clientStorage,
|
||||
)
|
||||
return (
|
||||
<EventLoopContext.Provider value={[addEvents, connectError]}>
|
||||
<StateContext.Provider value={state}>
|
||||
{children}
|
||||
</StateContext.Provider>
|
||||
</EventLoopContext.Provider>
|
||||
)
|
||||
}
|
67
.web/utils/helpers/dataeditor.js
Normal file
@ -0,0 +1,67 @@
|
||||
import { GridCellKind } from "@glideapps/glide-data-grid"
|
||||
|
||||
export function getDEColumn(columns, col) {
|
||||
let c = columns[col];
|
||||
c.pos = col;
|
||||
return c;
|
||||
}
|
||||
|
||||
export function getDERow(data, row) {
|
||||
return data[row];
|
||||
}
|
||||
|
||||
export function locateCell(row, column) {
|
||||
if (Array.isArray(row)) {
|
||||
return row[column.pos];
|
||||
} else {
|
||||
return row[column.id];
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCell(value, column) {
|
||||
const editable = column.editable || true
|
||||
switch (column.type) {
|
||||
case "int":
|
||||
case "float":
|
||||
return {
|
||||
kind: GridCellKind.Number,
|
||||
data: value,
|
||||
displayData: value + "",
|
||||
readonly: !editable,
|
||||
allowOverlay: editable,
|
||||
}
|
||||
case "datetime":
|
||||
// value = moment format?
|
||||
case "str":
|
||||
return {
|
||||
kind: GridCellKind.Text,
|
||||
data: value,
|
||||
displayData: value,
|
||||
readonly: !editable,
|
||||
allowOverlay: editable,
|
||||
}
|
||||
case "bool":
|
||||
return {
|
||||
kind: GridCellKind.Boolean,
|
||||
data: value,
|
||||
readonly: !editable,
|
||||
}
|
||||
default:
|
||||
console.log("Warning: column.type is undefined for column.title=" + column.title)
|
||||
return {
|
||||
kind: GridCellKind.Text,
|
||||
data: value,
|
||||
displayData: column.type
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export function formatDataEditorCells(col, row, columns, data) {
|
||||
if (row < data.length && col < columns.length) {
|
||||
const column = getDEColumn(columns, col);
|
||||
const rowData = getDERow(data, row);
|
||||
const cellData = locateCell(rowData, column);
|
||||
return formatCell(cellData, column);
|
||||
}
|
||||
return { kind: GridCellKind.Loading };
|
||||
}
|
589
.web/utils/state.js
Normal file
@ -0,0 +1,589 @@
|
||||
// State management for Reflex web apps.
|
||||
import axios from "axios";
|
||||
import io from "socket.io-client";
|
||||
import JSON5 from "json5";
|
||||
import env from "env.json";
|
||||
import Cookies from "universal-cookie";
|
||||
import { useEffect, useReducer, useRef, useState } from "react";
|
||||
import Router, { useRouter } from "next/router";
|
||||
import { initialEvents } from "utils/context.js"
|
||||
|
||||
// Endpoint URLs.
|
||||
const EVENTURL = env.EVENT
|
||||
const UPLOADURL = env.UPLOAD
|
||||
|
||||
// Global variable to hold the token.
|
||||
let token;
|
||||
|
||||
// Key for the token in the session storage.
|
||||
const TOKEN_KEY = "token";
|
||||
|
||||
// create cookie instance
|
||||
const cookies = new Cookies();
|
||||
|
||||
// Dictionary holding component references.
|
||||
export const refs = {};
|
||||
|
||||
// Flag ensures that only one event is processing on the backend concurrently.
|
||||
let event_processing = false
|
||||
// Array holding pending events to be processed.
|
||||
const event_queue = [];
|
||||
|
||||
/**
|
||||
* Generate a UUID (Used for session tokens).
|
||||
* Taken from: https://stackoverflow.com/questions/105034/how-do-i-create-a-guid-uuid
|
||||
* @returns A UUID.
|
||||
*/
|
||||
const generateUUID = () => {
|
||||
let d = new Date().getTime(),
|
||||
d2 = (performance && performance.now && performance.now() * 1000) || 0;
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
let r = Math.random() * 16;
|
||||
if (d > 0) {
|
||||
r = (d + r) % 16 | 0;
|
||||
d = Math.floor(d / 16);
|
||||
} else {
|
||||
r = (d2 + r) % 16 | 0;
|
||||
d2 = Math.floor(d2 / 16);
|
||||
}
|
||||
return (c == "x" ? r : (r & 0x7) | 0x8).toString(16);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the token for the current session.
|
||||
* @returns The token.
|
||||
*/
|
||||
export const getToken = () => {
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
if (window) {
|
||||
if (!window.sessionStorage.getItem(TOKEN_KEY)) {
|
||||
window.sessionStorage.setItem(TOKEN_KEY, generateUUID());
|
||||
}
|
||||
token = window.sessionStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the URL for the websocket connection
|
||||
* @returns The websocket URL object.
|
||||
*/
|
||||
export const getEventURL = () => {
|
||||
// Get backend URL object from the endpoint.
|
||||
const endpoint = new URL(EVENTURL);
|
||||
if (endpoint.hostname === "localhost") {
|
||||
// If the backend URL references localhost, and the frontend is not on localhost,
|
||||
// then use the frontend host.
|
||||
const frontend_hostname = window.location.hostname;
|
||||
if (frontend_hostname !== "localhost") {
|
||||
endpoint.hostname = frontend_hostname;
|
||||
}
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a delta to the state.
|
||||
* @param state The state to apply the delta to.
|
||||
* @param delta The delta to apply.
|
||||
*/
|
||||
export const applyDelta = (state, delta) => {
|
||||
const new_state = { ...state }
|
||||
for (const substate in delta) {
|
||||
let s = new_state;
|
||||
const path = substate.split(".").slice(1);
|
||||
while (path.length > 0) {
|
||||
s = s[path.shift()];
|
||||
}
|
||||
for (const key in delta[substate]) {
|
||||
s[key] = delta[substate][key];
|
||||
}
|
||||
}
|
||||
return new_state
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get all local storage items in a key-value object.
|
||||
* @returns object of items in local storage.
|
||||
*/
|
||||
export const getAllLocalStorageItems = () => {
|
||||
var localStorageItems = {};
|
||||
|
||||
for (var i = 0, len = localStorage.length; i < len; i++) {
|
||||
var key = localStorage.key(i);
|
||||
localStorageItems[key] = localStorage.getItem(key);
|
||||
}
|
||||
|
||||
return localStorageItems;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle frontend event or send the event to the backend via Websocket.
|
||||
* @param event The event to send.
|
||||
* @param socket The socket object to send the event on.
|
||||
*
|
||||
* @returns True if the event was sent, false if it was handled locally.
|
||||
*/
|
||||
export const applyEvent = async (event, socket) => {
|
||||
// Handle special events
|
||||
if (event.name == "_redirect") {
|
||||
if (event.payload.external)
|
||||
window.open(event.payload.path, "_blank");
|
||||
else
|
||||
Router.push(event.payload.path);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_console") {
|
||||
console.log(event.payload.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_remove_cookie") {
|
||||
cookies.remove(event.payload.key, { ...event.payload.options })
|
||||
queueEvents(initialEvents(), socket)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_clear_local_storage") {
|
||||
localStorage.clear();
|
||||
queueEvents(initialEvents(), socket)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_remove_local_storage") {
|
||||
localStorage.removeItem(event.payload.key);
|
||||
queueEvents(initialEvents(), socket)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_set_clipboard") {
|
||||
const content = event.payload.content;
|
||||
navigator.clipboard.writeText(content);
|
||||
return false;
|
||||
}
|
||||
if (event.name == "_download") {
|
||||
const a = document.createElement('a');
|
||||
a.hidden = true;
|
||||
a.href = event.payload.url;
|
||||
if (event.payload.filename)
|
||||
a.download = event.payload.filename;
|
||||
a.click();
|
||||
a.remove();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_alert") {
|
||||
alert(event.payload.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_set_focus") {
|
||||
const ref =
|
||||
event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
|
||||
ref.current.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_set_value") {
|
||||
const ref =
|
||||
event.payload.ref in refs ? refs[event.payload.ref] : event.payload.ref;
|
||||
ref.current.value = event.payload.value;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (event.name == "_call_script") {
|
||||
try {
|
||||
const eval_result = eval(event.payload.javascript_code);
|
||||
if (event.payload.callback) {
|
||||
if (!!eval_result && typeof eval_result.then === 'function') {
|
||||
eval(event.payload.callback)(await eval_result)
|
||||
} else {
|
||||
eval(event.payload.callback)(eval_result)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("_call_script", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update token and router data (if missing).
|
||||
event.token = getToken()
|
||||
if (event.router_data === undefined || Object.keys(event.router_data).length === 0) {
|
||||
event.router_data = (({ pathname, query, asPath }) => ({ pathname, query, asPath }))(Router)
|
||||
}
|
||||
|
||||
// Send the event to the server.
|
||||
if (socket) {
|
||||
socket.emit("event", JSON.stringify(event, (k, v) => v === undefined ? null : v));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Send an event to the server via REST.
|
||||
* @param event The current event.
|
||||
* @param state The state with the event queue.
|
||||
*
|
||||
* @returns Whether the event was sent.
|
||||
*/
|
||||
export const applyRestEvent = async (event) => {
|
||||
let eventSent = false;
|
||||
if (event.handler == "uploadFiles") {
|
||||
eventSent = await uploadFiles(event.name, event.payload.files);
|
||||
}
|
||||
return eventSent;
|
||||
};
|
||||
|
||||
/**
|
||||
* Queue events to be processed and trigger processing of queue.
|
||||
* @param events Array of events to queue.
|
||||
* @param socket The socket object to send the event on.
|
||||
*/
|
||||
export const queueEvents = async (events, socket) => {
|
||||
event_queue.push(...events)
|
||||
await processEvent(socket.current)
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an event off the event queue.
|
||||
* @param socket The socket object to send the event on.
|
||||
*/
|
||||
export const processEvent = async (
|
||||
socket
|
||||
) => {
|
||||
// Only proceed if the socket is up, otherwise we throw the event into the void
|
||||
if (!socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only proceed if we're not already processing an event.
|
||||
if (event_queue.length === 0 || event_processing) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set processing to true to block other events from being processed.
|
||||
event_processing = true
|
||||
|
||||
// Apply the next event in the queue.
|
||||
const event = event_queue.shift();
|
||||
|
||||
let eventSent = false
|
||||
// Process events with handlers via REST and all others via websockets.
|
||||
if (event.handler) {
|
||||
eventSent = await applyRestEvent(event);
|
||||
} else {
|
||||
eventSent = await applyEvent(event, socket);
|
||||
}
|
||||
// If no event was sent, set processing to false.
|
||||
if (!eventSent) {
|
||||
event_processing = false;
|
||||
// recursively call processEvent to drain the queue, since there is
|
||||
// no state update to trigger the useEffect event loop.
|
||||
await processEvent(socket)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a websocket and set the handlers.
|
||||
* @param socket The socket object to connect.
|
||||
* @param dispatch The function to queue state update
|
||||
* @param transports The transports to use.
|
||||
* @param setConnectError The function to update connection error value.
|
||||
* @param client_storage The client storage object from context.js
|
||||
*/
|
||||
export const connect = async (
|
||||
socket,
|
||||
dispatch,
|
||||
transports,
|
||||
setConnectError,
|
||||
client_storage = {},
|
||||
) => {
|
||||
// Get backend URL object from the endpoint.
|
||||
const endpoint = getEventURL()
|
||||
|
||||
// Create the socket.
|
||||
socket.current = io(endpoint.href, {
|
||||
path: endpoint["pathname"],
|
||||
transports: transports,
|
||||
autoUnref: false,
|
||||
});
|
||||
|
||||
// Once the socket is open, hydrate the page.
|
||||
socket.current.on("connect", () => {
|
||||
setConnectError(null)
|
||||
});
|
||||
|
||||
socket.current.on('connect_error', (error) => {
|
||||
setConnectError(error)
|
||||
});
|
||||
|
||||
// On each received message, queue the updates and events.
|
||||
socket.current.on("event", message => {
|
||||
const update = JSON5.parse(message)
|
||||
dispatch(update.delta)
|
||||
applyClientStorageDelta(client_storage, update.delta)
|
||||
event_processing = !update.final
|
||||
if (update.events) {
|
||||
queueEvents(update.events, socket)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Upload files to the server.
|
||||
*
|
||||
* @param state The state to apply the delta to.
|
||||
* @param handler The handler to use.
|
||||
*
|
||||
* @returns Whether the files were uploaded.
|
||||
*/
|
||||
export const uploadFiles = async (handler, files) => {
|
||||
// return if there's no file to upload
|
||||
if (files.length == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
"Content-Type": files[0].type,
|
||||
};
|
||||
const formdata = new FormData();
|
||||
|
||||
// Add the token and handler to the file name.
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
formdata.append(
|
||||
"files",
|
||||
files[i],
|
||||
getToken() + ":" + handler + ":" + files[i].name
|
||||
);
|
||||
}
|
||||
|
||||
// Send the file to the server.
|
||||
await axios.post(UPLOADURL, formdata, headers)
|
||||
.then(() => { return true; })
|
||||
.catch(
|
||||
error => {
|
||||
if (error.response) {
|
||||
// The request was made and the server responded with a status code
|
||||
// that falls out of the range of 2xx
|
||||
console.log(error.response.data);
|
||||
} else if (error.request) {
|
||||
// The request was made but no response was received
|
||||
// `error.request` is an instance of XMLHttpRequest in the browser and an instance of
|
||||
// http.ClientRequest in node.js
|
||||
console.log(error.request);
|
||||
} else {
|
||||
// Something happened in setting up the request that triggered an Error
|
||||
console.log(error.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an event object.
|
||||
* @param name The name of the event.
|
||||
* @param payload The payload of the event.
|
||||
* @param handler The client handler to process event.
|
||||
* @returns The event object.
|
||||
*/
|
||||
export const Event = (name, payload = {}, handler = null) => {
|
||||
return { name, payload, handler };
|
||||
};
|
||||
|
||||
/**
|
||||
* Package client-side storage values as payload to send to the
|
||||
* backend with the hydrate event
|
||||
* @param client_storage The client storage object from context.js
|
||||
* @returns payload dict of client storage values
|
||||
*/
|
||||
export const hydrateClientStorage = (client_storage) => {
|
||||
const client_storage_values = {
|
||||
"cookies": {},
|
||||
"local_storage": {}
|
||||
}
|
||||
if (client_storage.cookies) {
|
||||
for (const state_key in client_storage.cookies) {
|
||||
const cookie_options = client_storage.cookies[state_key]
|
||||
const cookie_name = cookie_options.name || state_key
|
||||
const cookie_value = cookies.get(cookie_name)
|
||||
if (cookie_value !== undefined) {
|
||||
client_storage_values.cookies[state_key] = cookies.get(cookie_name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (client_storage.local_storage && (typeof window !== 'undefined')) {
|
||||
for (const state_key in client_storage.local_storage) {
|
||||
const options = client_storage.local_storage[state_key]
|
||||
const local_storage_value = localStorage.getItem(options.name || state_key)
|
||||
if (local_storage_value !== null) {
|
||||
client_storage_values.local_storage[state_key] = local_storage_value
|
||||
}
|
||||
}
|
||||
}
|
||||
if (client_storage.cookies || client_storage.local_storage) {
|
||||
return client_storage_values
|
||||
}
|
||||
return {}
|
||||
};
|
||||
|
||||
/**
|
||||
* Update client storage values based on backend state delta.
|
||||
* @param client_storage The client storage object from context.js
|
||||
* @param delta The state update from the backend
|
||||
*/
|
||||
const applyClientStorageDelta = (client_storage, delta) => {
|
||||
// find the main state and check for is_hydrated
|
||||
const unqualified_states = Object.keys(delta).filter((key) => key.split(".").length === 1);
|
||||
if (unqualified_states.length === 1) {
|
||||
const main_state = delta[unqualified_states[0]]
|
||||
if (main_state.is_hydrated !== undefined && !main_state.is_hydrated) {
|
||||
// skip if the state is not hydrated yet, since all client storage
|
||||
// values are sent in the hydrate event
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Save known client storage values to cookies and localStorage.
|
||||
for (const substate in delta) {
|
||||
for (const key in delta[substate]) {
|
||||
const state_key = `${substate}.${key}`
|
||||
if (client_storage.cookies && state_key in client_storage.cookies) {
|
||||
const cookie_options = { ...client_storage.cookies[state_key] }
|
||||
const cookie_name = cookie_options.name || state_key
|
||||
delete cookie_options.name // name is not a valid cookie option
|
||||
cookies.set(cookie_name, delta[substate][key], cookie_options);
|
||||
} else if (client_storage.local_storage && state_key in client_storage.local_storage && (typeof window !== 'undefined')) {
|
||||
const options = client_storage.local_storage[state_key]
|
||||
localStorage.setItem(options.name || state_key, delta[substate][key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Establish websocket event loop for a NextJS page.
|
||||
* @param initial_state The initial app state.
|
||||
* @param initial_events Function that returns the initial app events.
|
||||
* @param client_storage The client storage object from context.js
|
||||
*
|
||||
* @returns [state, addEvents, connectError] -
|
||||
* state is a reactive dict,
|
||||
* addEvents is used to queue an event, and
|
||||
* connectError is a reactive js error from the websocket connection (or null if connected).
|
||||
*/
|
||||
export const useEventLoop = (
|
||||
initial_state = {},
|
||||
initial_events = () => [],
|
||||
client_storage = {},
|
||||
) => {
|
||||
const socket = useRef(null)
|
||||
const router = useRouter()
|
||||
const [state, dispatch] = useReducer(applyDelta, initial_state)
|
||||
const [connectError, setConnectError] = useState(null)
|
||||
|
||||
// Function to add new events to the event queue.
|
||||
const addEvents = (events, _e, event_actions) => {
|
||||
if (event_actions?.preventDefault && _e?.preventDefault) {
|
||||
_e.preventDefault();
|
||||
}
|
||||
if (event_actions?.stopPropagation && _e?.stopPropagation) {
|
||||
_e.stopPropagation();
|
||||
}
|
||||
queueEvents(events, socket)
|
||||
}
|
||||
|
||||
const sentHydrate = useRef(false); // Avoid double-hydrate due to React strict-mode
|
||||
// initial state hydrate
|
||||
useEffect(() => {
|
||||
if (router.isReady && !sentHydrate.current) {
|
||||
addEvents(initial_events())
|
||||
sentHydrate.current = true
|
||||
}
|
||||
}, [router.isReady])
|
||||
|
||||
// Main event loop.
|
||||
useEffect(() => {
|
||||
// Skip if the router is not ready.
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
}
|
||||
// only use websockets if state is present
|
||||
if (Object.keys(state).length > 0) {
|
||||
// Initialize the websocket connection.
|
||||
if (!socket.current) {
|
||||
connect(socket, dispatch, ['websocket', 'polling'], setConnectError, client_storage)
|
||||
}
|
||||
(async () => {
|
||||
// Process all outstanding events.
|
||||
while (event_queue.length > 0 && !event_processing) {
|
||||
await processEvent(socket.current)
|
||||
}
|
||||
})()
|
||||
}
|
||||
})
|
||||
return [state, addEvents, connectError]
|
||||
}
|
||||
|
||||
/***
|
||||
* Check if a value is truthy in python.
|
||||
* @param val The value to check.
|
||||
* @returns True if the value is truthy, false otherwise.
|
||||
*/
|
||||
export const isTrue = (val) => {
|
||||
return Array.isArray(val) ? val.length > 0 : !!val;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the value from a ref.
|
||||
* @param ref The ref to get the value from.
|
||||
* @returns The value.
|
||||
*/
|
||||
export const getRefValue = (ref) => {
|
||||
if (!ref || !ref.current) {
|
||||
return;
|
||||
}
|
||||
if (ref.current.type == "checkbox") {
|
||||
return ref.current.checked;
|
||||
} else {
|
||||
//querySelector(":checked") is needed to get value from radio_group
|
||||
return ref.current.value || (ref.current.querySelector(':checked') && ref.current.querySelector(':checked').value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the values from a ref array.
|
||||
* @param refs The refs to get the values from.
|
||||
* @returns The values array.
|
||||
*/
|
||||
export const getRefValues = (refs) => {
|
||||
if (!refs) {
|
||||
return;
|
||||
}
|
||||
// getAttribute is used by RangeSlider because it doesn't assign value
|
||||
return refs.map((ref) => ref.current.value || ref.current.getAttribute("aria-valuenow"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Spread two arrays or two objects.
|
||||
* @param first The first array or object.
|
||||
* @param second The second array or object.
|
||||
* @returns The final merged array or object.
|
||||
*/
|
||||
export const spreadArraysOrObjects = (first, second) => {
|
||||
if (Array.isArray(first) && Array.isArray(second)) {
|
||||
return [...first, ...second];
|
||||
} else if (typeof first === 'object' && typeof second === 'object') {
|
||||
return { ...first, ...second };
|
||||
} else {
|
||||
throw new Error('Both parameters must be either arrays or objects.');
|
||||
}
|
||||
}
|
1
.web/utils/theme.js
Normal file
@ -0,0 +1 @@
|
||||
export default {"styles": {"global": {"body": {}}}}
|
42
README.md
@ -1,21 +1,27 @@
|
||||
# BlurMe docs
|
||||
# Witaj w BlurMe!
|
||||
Dlaczego Blurme?
|
||||
|
||||
Prywatność: Nasza aplikacja zapewnia pełną ochronę prywatności, eliminując ryzyko identyfikacji osób niezwiązanych z anonimizacją.
|
||||
Prost obsługa: Intuicyjny interfejs użytkownika sprawia, że korzystanie z Blurme jest łatwe dla każdego.
|
||||
Elastyczność: Wybierz, która twarz na zdjęciu ma pozostać nieruszoną, a my zadbasz o resztę. Indywidualizuj swoje zdjęcia zgodnie z własnymi potrzebami.
|
||||
|
||||
Jak to działa?
|
||||
|
||||
Wgraj Zdjęcie: Prosty proces rozpoczyna się od dodania zdjęcia, które chcesz anonimizować.
|
||||
Zaznacz Twarz do Ochrony: Oznacz twarz, którą chcesz zachować.
|
||||
Anonimizuj Resztę: Naciśnij przycisk, a Blurme automatycznie zastosuje efekt rozmycia do wszystkich obszarów poza zaznaczoną twarzą.
|
||||
Pobierz i Podziel Się: Gotowe! Pobierz anonimizowane zdjęcie i dziel się nim bez obaw o prywatność.
|
||||
|
||||
Do Czego Może Ci Się Przydać?
|
||||
|
||||
Ochrona Tożsamości: Idealne do usuwania twarzy nieznajomych lub przypadkowych przechodniów z tła.
|
||||
Zachowanie Prywatności: Przydatne w sytuacjach, gdzie chcesz udostępnić zdjęcia publicznie, ale z zachowaniem prywatności pewnych osób.
|
||||
Kreatywność: Wyraź swoją kreatywność, zachowując jednocześnie istotne elementy na zdjęciach.
|
||||
Blurme to więcej niż tylko narzędzie - to rozwiązanie, które pozwala Ci kontrolować, jak prezentujesz swoje zdjęcia online, zachowując jednocześnie pełną prywatność. Przekształć swoje obrazy już teraz!
|
||||
|
||||
|
||||
## Przygotowanie aplikacji po raz pierwszy
|
||||
In this template, the base state handles the toggle for the sidebar.
|
||||
|
||||
```
|
||||
apt install ffmeg libsm6 libxext6
|
||||
pip install -r requirements.txt
|
||||
reflex init
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Uruchamianie aplikacji (localhost:3000)
|
||||
|
||||
```
|
||||
reflex run
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
Jeżeli jest problem z uruchomieniem aplikacji, usunąć folder `.web` i ponownie wykonać `reflex init`, powinno wystarczyć.
|
||||
As your app grows, we recommend using [substates](https://reflex.dev/docs/state/substates/)
|
||||
to organize your state. You can either define substates in their own files, or if the state is
|
||||
specific to a page, you can define it in the page file itself.
|
||||
|
@ -1,5 +0,0 @@
|
||||
<application>
|
||||
<component name="LafManager">
|
||||
<laf class-name="com.intellij.ide.ui.laf.darcula.DarculaLaf" />
|
||||
</component>
|
||||
</application>
|
Before Width: | Height: | Size: 903 B |
Before Width: | Height: | Size: 761 B |
BIN
assets/about.png
Before Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 732 B |
Before Width: | Height: | Size: 529 B |
Before Width: | Height: | Size: 126 KiB |
BIN
assets/data.png
Before Width: | Height: | Size: 159 KiB |
Before Width: | Height: | Size: 598 B |
Before Width: | Height: | Size: 477 B |
BIN
assets/icon.png
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 95 KiB |
Before Width: | Height: | Size: 820 B |
Before Width: | Height: | Size: 660 B |
Before Width: | Height: | Size: 112 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 760 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 235 KiB |
Before Width: | Height: | Size: 51 KiB |
@ -8,6 +8,5 @@ from blurme.pages import *
|
||||
import reflex as rx
|
||||
|
||||
# Create the app and compile it.
|
||||
app = rx.App()
|
||||
#style=styles.base_style
|
||||
app = rx.App(style=styles.base_style)
|
||||
app.compile()
|
||||
|
@ -2,31 +2,47 @@
|
||||
|
||||
from blurme import styles
|
||||
from blurme.state import State
|
||||
from reflex.page import get_decorated_pages
|
||||
from blurme.templates import template
|
||||
|
||||
|
||||
import reflex as rx
|
||||
|
||||
|
||||
def sidebar_header() -> rx.Component:
|
||||
"""Returns:
|
||||
The sidebar header component.
|
||||
"""
|
||||
return rx.hstack(
|
||||
# The logo.
|
||||
rx.image(
|
||||
src="/icon.png",
|
||||
height="5em",
|
||||
height="4.5em",
|
||||
),
|
||||
rx.spacer(),
|
||||
width="100%",
|
||||
border_bottom=styles.border,
|
||||
padding="1em",
|
||||
align = 'center',
|
||||
)
|
||||
|
||||
def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
|
||||
|
||||
#def sidebar_footer() -> rx.Component:
|
||||
"""Sidebar footer.
|
||||
|
||||
Returns:
|
||||
The sidebar footer component.
|
||||
"""
|
||||
# return rx.hstack(
|
||||
# rx.spacer(),
|
||||
# rx.link(
|
||||
# rx.text("Docs"),
|
||||
# href="https://reflex.dev/docs/getting-started/introduction/",
|
||||
# ),
|
||||
# rx.link(
|
||||
# rx.text("Blog"),
|
||||
# href="https://reflex.dev/blog/",
|
||||
# ),
|
||||
# width="100%",
|
||||
# border_top=styles.border,
|
||||
# padding="1em",
|
||||
# )
|
||||
|
||||
def sidebar_item(text: str, url: str) -> rx.Component:
|
||||
#icon: str,
|
||||
"""Sidebar item.
|
||||
|
||||
Args:
|
||||
@ -38,46 +54,38 @@ def sidebar_item(text: str, icon: str, url: str) -> rx.Component:
|
||||
rx.Component: The sidebar item component.
|
||||
"""
|
||||
# Whether the item is active.
|
||||
active = State.router.page.path == url
|
||||
|
||||
default_dark_icon = "/image-icon-dark.png"
|
||||
|
||||
#final_dark_icon = dark_icon if dark_icon else default_dark_icon
|
||||
|
||||
#active = (State.router.page.path == f"/{text.lower()}") | (
|
||||
# (State.router.page.path == "/") & text == "Strona główna"
|
||||
#)
|
||||
active = (State.router.page.path == f"/{text.lower()}") | (
|
||||
(State.router.page.path == "/") & text == "Strona główna"
|
||||
)
|
||||
|
||||
return rx.link(
|
||||
rx.hstack(
|
||||
rx.image(
|
||||
src=icon,
|
||||
height="2.5em",
|
||||
padding="0.5em",
|
||||
),
|
||||
#rx.image(
|
||||
# src=icon,
|
||||
# height="2.5em",
|
||||
# padding="0.5em",
|
||||
#),
|
||||
rx.text(
|
||||
text,
|
||||
|
||||
),
|
||||
bg=rx.cond(
|
||||
active,
|
||||
styles.accent_color,
|
||||
"transparent",
|
||||
),
|
||||
#color=rx.cond(
|
||||
# active,
|
||||
# styles.accent_text_color,
|
||||
# styles.text_color,
|
||||
#),
|
||||
color=rx.cond(
|
||||
active,
|
||||
styles.accent_text_color,
|
||||
styles.text_color,
|
||||
),
|
||||
border_radius=styles.border_radius,
|
||||
box_shadow=styles.box_shadow,
|
||||
width="100%",
|
||||
padding_x="1.3em",
|
||||
height = "2.5em"
|
||||
padding_x="1em",
|
||||
height = "2em"
|
||||
),
|
||||
href=url,
|
||||
width="100%",
|
||||
|
||||
)
|
||||
|
||||
|
||||
@ -87,6 +95,7 @@ def sidebar() -> rx.Component:
|
||||
Returns:
|
||||
The sidebar component.
|
||||
"""
|
||||
# Get all the decorated pages and add them to the sidebar.
|
||||
from reflex.page import get_decorated_pages
|
||||
|
||||
return rx.box(
|
||||
@ -96,20 +105,19 @@ def sidebar() -> rx.Component:
|
||||
*[
|
||||
sidebar_item(
|
||||
text=page.get("title", page["route"].strip("/").capitalize()),
|
||||
icon=page.get("image",page["route"].strip("/").capitalize()),
|
||||
#icon=page.get("image", "/github.svg"),
|
||||
url=page["route"],
|
||||
)
|
||||
for page in get_decorated_pages()
|
||||
],
|
||||
rx.spacer(),
|
||||
width="100%",
|
||||
overflow_y="auto",
|
||||
align_items="flex-start",
|
||||
padding="1em",
|
||||
),
|
||||
rx.spacer(),
|
||||
height="100em",
|
||||
|
||||
#sidebar_footer(),
|
||||
height="100%",
|
||||
),
|
||||
display=["none", "none", "block"],
|
||||
min_width=styles.sidebar_width,
|
||||
|
@ -1,4 +1,3 @@
|
||||
from .dashboard import dashboard
|
||||
from .index import index
|
||||
from .settings import settings
|
||||
from .about import about
|
||||
|
@ -1,40 +0,0 @@
|
||||
from blurme.templates import template
|
||||
|
||||
import reflex as rx
|
||||
|
||||
@template(route="/about", title="O autorach", image = "/about-icon.png")
|
||||
def about() -> rx.Component:
|
||||
return rx.vstack(rx.heading("O autorach", font_size="3em", margin_bottom="10px", background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", padding = "10px"),
|
||||
|
||||
rx.box(
|
||||
rx.text("Jesteśmy grupą studentek kierunku Przetwarzania i Analizy Danych, które postanowiły połączyć swoje różnorodne umiejętności w ramach wspólnego projektu. Zależy nam nie tylko na zdobywaniu wiedzy na uczelni, ale także na praktycznym wykorzystaniu naszych umiejętności w realnych projektach.", margin_right="5%", margin_left="5%", margin_bottom = "10px"),
|
||||
rx.wrap(rx.hstack(
|
||||
rx.vstack(rx.image(src= "/cooperation.png", height = "8em", align = "center"),
|
||||
rx.text("Współpraca",as_="b",font_size="1.4em"),
|
||||
rx.text("W naszym zespole każda z nas wnosi unikalne perspektywy i pomysły, co sprawia, że nasza współpraca jest inspirująca.", text_align="center"),),
|
||||
rx.vstack(rx.image(src= "/innovation.png", height = "8em", align = "center"),
|
||||
rx.text("Innowacyjność",as_="b",font_size="1.4em"),
|
||||
rx.text("Naszym głównym celem jest stworzenie innowacyjnego rozwiązania, które nie tylko będzie spełniać potrzeby dzisiejszych czasów, ale również przyczyni się do ochrony prywatności w kontekście współdzielenia zdjęć online.", text_align="center"),),
|
||||
rx.vstack(rx.image(src= "/data.png", height = "8em", align = "center"),
|
||||
rx.text("Analiza danych", as_="b",font_size="1.4em"),
|
||||
rx.text("Nasza pasja do analizy i zrozumienia dziedziny przetwarzania danych stanowią fundamenty naszego projektu.", text_align="center"),),
|
||||
style={"align-self": "center", "align-items": "flex-start"}),style={"align-self": "center", "align-items": "flex-start"}, margin_right = "5%", margin_left = "5%"),
|
||||
|
||||
rx.text("Dzięki zaufaniu naszych użytkowników oraz naszemu zangażowaniu, wierzymy, że nasz projekt przyczyni się do tworzenia bezpieczniejszej przestrzeni dla współdzielenia zdjęć w sieci. Cieszymy się, że możemy być częścią tej fascynującej podróży!", margin_right="5%", margin_left="5%", margin_top="10px"), text_align = "center",),
|
||||
rx.list(
|
||||
rx.hstack(rx.list_item(rx.text("Nasza Misja",padding = "10px", text_align="center",background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", font_size="2em", font_weight="bold",),
|
||||
rx.text("Nasza grupa powstała z fascynacji technologią oraz z troską o prywatność w dzisiejszym świecie online. Świadomi wyzwań związanych z udostępnianiem fotografii publicznie, postanowiliśmy stworzyć aplikację, która umożliwi użytkownikom ochronę swojej prywatności, jednocześnie pozwalając na swobodne udostępnianie chwil z życia." ), margin_right = "30px"),
|
||||
rx.image(src="/technology.png", height = "11em"), margin_top="30px", margin_bottom="5px"),
|
||||
rx.hstack(rx.image(src="/about.png", height = "13em", margin_right = "30px"),
|
||||
rx.list_item(rx.text("Jak Powstała Nasza Grupa",padding = "10px", text_align="center",background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", font_size="2em", font_weight="bold", margin_top="30px", margin_bottom="5px"),
|
||||
rx.text("Nasza grupa skupia się na różnorodnych dziedzinach, takich jak sztuczna inteligencja, przetwarzanie obrazów i programowanie. Nasza wspólna pasja do technologii oraz zaangażowanie w projekt zrodziły kreatywną współpracę. Spotkaliśmy się na naszych studiach, gdzie zauważyliśmy potrzebę stworzenia narzędzia, które połączy technologię z ochroną prywatności. Dzięki naszym umiejętnościom oraz współpracy zdobyliśmy doświadczenie w tworzeniu zaawansowanych algorytmów przetwarzania obrazów, które pozwalają na automatyczne wykrywanie twarzy i rejestracji samochodowych na zdjęciach.")),margin_top="30px", margin_bottom="5px"),
|
||||
|
||||
|
||||
rx.hstack(rx.list_item(rx.text("Dlaczego BlurMe?",padding = "10px", text_align="center",background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", font_size="2em", font_weight="bold"),
|
||||
rx.text("BlurMe to wynik naszej wspólnej wizji, aby uczynić proces ochrony prywatności prostym i dostępnym dla każdego. Chcemy, aby ludzie czuli się pewnie dzieląc się zdjęciami, nie martwiąc się o potencjalne naruszenia prywatności.",), margin_right = "30px"),
|
||||
rx.image(src="/privacy.png", height = "11em"), margin_top="30px", margin_bottom="5px"),
|
||||
text_align = "center"), margin_right= "65px", margin_left = "65px")
|
||||
|
||||
#rx.list_item(rx.text("Kontakt",padding = "10px", text_align="center",background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", font_size="2em", font_weight="bold", margin_top="30px", margin_bottom="5px"),
|
||||
# rx.text("Jeśli masz pytania, sugestie lub chciałbyś dowiedzieć się więcej o naszej grupie, skontaktuj się z nami poprzez naszą stronę kontaktową. Jesteśmy otwarci na współpracę i ciekawe pomysły!"),margin_left = "40%"),
|
||||
# text_align = "center"), margin_right= "65px", margin_left = "65px")
|
@ -3,41 +3,15 @@ from blurme.templates import template
|
||||
|
||||
import reflex as rx
|
||||
|
||||
ICON_EMAIL = "assets/email-icon.png"
|
||||
ICON_PHONE = "assets/phone-icon.png"
|
||||
|
||||
@template(route="/dashboard", title="Kontakt", image="/contact-icon.png")
|
||||
@template(route="/dashboard", title="Dashboard")
|
||||
def dashboard() -> rx.Component:
|
||||
"""The dashboard page.
|
||||
|
||||
Returns:
|
||||
The UI for the dashboard page.
|
||||
"""
|
||||
container_style = {"background-color": "#f5f5f5", "padding": "10px", "margin-bottom": "20px", "display": "flex", "align-items": "center"}
|
||||
icon_style = {"width": "20px", "height": "30px", "margin-right": "10px"}
|
||||
heading_style = {"font-weight": "bold", "font-family": "Arial, sans-serif"}
|
||||
|
||||
|
||||
return rx.wrap(
|
||||
rx.hstack(
|
||||
rx.vstack(rx.hstack(rx.image(src="/sparkle-left.png",height="3em"),
|
||||
rx.heading("Skontaktuj się!", font_size="2.6em", margin_bottom="10px", background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", padding = "5px", text_align = "center"),
|
||||
rx.image(src="/sparkle-right.png",height="3em"), style={"align-self": "center"}),
|
||||
rx.text("Witaj na stronie Kontaktowej w aplikacji BlurMe!", font_size="1.2em", text_align = "center"),
|
||||
rx.text("Jesteśmy dostępni dla Ciebie na różnych platformach. "
|
||||
"Skontaktuj się z nami, gdy tylko masz pytania, sugestie lub "
|
||||
"po prostu chcesz porozmawiać o naszej aplikacji.", margin_bottom="25px", text_align = "center"),
|
||||
style={"align-self": "flex-start","margin-left": "50px", "margin-top": "50px", "align-self": "flex-start", "margin-bottom": "25px"}, width = "50%"),
|
||||
rx.wrap(rx.vstack(rx.vstack(rx.fragment(
|
||||
rx.hstack(rx.icon(tag="email",style=icon_style),
|
||||
rx.heading("E-mail", level=3, style=heading_style), margin_top="20px"),
|
||||
rx.text("Zapraszamy do napisania do nas na adres: contact@blurme.com"),
|
||||
style=container_style,text_align = "center")),
|
||||
rx.vstack(rx.fragment(
|
||||
rx.hstack(rx.icon(tag="phone", style=icon_style),rx.heading("Telefon", level=3, style=heading_style), margin_top="20px"),
|
||||
rx.text("Możesz także zadzwonić pod numer: +48 123 456 789"),
|
||||
style=container_style, text_align = "center"),),
|
||||
),style={"align-self": "flex-center"}),))
|
||||
|
||||
|
||||
|
||||
#return rx.vstack(
|
||||
# rx.heading("BlurMe", font_size="3em"),
|
||||
# rx.text("Dodaj zdjęcia, które chcesz zanonimizować!"),
|
||||
#)
|
||||
|
@ -5,57 +5,34 @@ from blurme.templates import template
|
||||
|
||||
import reflex as rx
|
||||
|
||||
@template(route="/", title="Strona główna", image = "/home-icon.png")
|
||||
@template(route="/", title="Strona główna", image="/github.svg")
|
||||
def index() -> rx.Component:
|
||||
return rx.vstack(
|
||||
|
||||
rx.heading("Witaj w BlurMe!", font_size="3em", margin_bottom="25px", background_image="linear-gradient(271.68deg, #7566fe 0.75%, #f96caf 88.52%)", background_clip="text", padding = "10px"),
|
||||
rx.wrap(
|
||||
rx.hstack(
|
||||
rx.vstack(rx.hstack(rx.image(src="sparkle-left.png",height="3em"),
|
||||
rx.heading("Witaj w BlurMe!", font_size="2.6em", margin_bottom="10px", background_image="linear-gradient( 64.5deg, rgba(245,116,185,1) 14.7%, rgba(89,97,223,1) 88.7% )", background_clip="text", padding = "5px", text_align = "center"),
|
||||
rx.image(src="sparkle-right.png",height="3em"), style={"align-self": "center"}),
|
||||
rx.text("Usunięcie wybranych informacji identyfikujących ze zdjęć z wydarzeń i miejsc publicznych nigdy nie było tak proste!", font_size="1.2em", text_align = "center"),
|
||||
rx.text("Dołącz do naszej społeczności użytkowników, którzy cieszą się swobodą dzielenia się chwilami bez obaw o bezpieczeństwo swoich danych osobowych. Zacznij tworzyć bezpieczne wspomnienia już dziś!", margin_bottom="25px", text_align = "center"),
|
||||
rx.text("", pading = "25px"),
|
||||
rx.link(rx.button("Zacznij anonimizować"), href="settings",align="center",color="rgba(117,102,254,255)",button=True,padding="50xp",font_size="1.2em"),
|
||||
style={"align-self": "flex-start","margin-left": "50px", "margin-top": "50px", "align-self": "flex-start", "margin-bottom": "25px"}, width = "50%"),
|
||||
rx.image(src="obraz1.png",height="25em",padding="0.5em",style={"margin-right": "60px"}),
|
||||
style={"align-self": "flex-start"}, max_width='100%'),),
|
||||
|
||||
rx.text(" ", height = "20px"),
|
||||
|
||||
rx.vstack(rx.text("Jak to działa?", color = "rgba(117,102,253,255)", font_size="2em", font_weight="bold", margin_top="15px", margin_bottom="5px"),
|
||||
rx.list(
|
||||
rx.list_item(rx.text("1. Wgraj Zdjęcie ",as_="b",font_size="1.2em"),
|
||||
rx.text("Prosty proces rozpoczyna się od dodania zdjęcia, które chcesz anonimizować.", ),margin_right = "50%"),
|
||||
rx.list_item(rx.text("2. Zaznacz Twarz do Ochrony ",as_="b", font_size="1.2em"),
|
||||
rx.text("Oznacz twarz, którą chcesz zachować.", ),margin_left = "50%"),
|
||||
rx.list_item(rx.text("3. Anonimizuj Resztę ",as_="b", font_size="1.2em"),
|
||||
rx.text("Naciśnij przycisk, a Blurme automatycznie zastosuje efekt rozmycia do wszystkich obszarów poza zaznaczoną twarzą.", ),margin_right ="50%"),
|
||||
rx.list_item(rx.text("4. Pobierz i Podziel Się ",as_="b", font_size="1.2em"),
|
||||
rx.text("Gotowe! Pobierz anonimizowane zdjęcie i dziel się nim bez obaw o prywatność.",), margin_left = "50%"),
|
||||
), margin_right= "40px", margin_left = "40px"),
|
||||
|
||||
rx.vstack(rx.text("Dlaczego Blurme?", color = "rgba(251,108,174,255)", font_size="2em", font_weight="bold", margin_top="25px", margin_bottom="5px"),
|
||||
rx.flex(
|
||||
rx.box(rx.text("Prywatność",as_="b",font_size="1.3em"),
|
||||
rx.text("Nasza aplikacja zapewnia pełną ochronę prywatności, eliminując ryzyko identyfikacji osób niezwiązanych z anonimizacją.", text_align="left",),margin_left= "40px", width="315px", padding = "5px"),
|
||||
rx.box(rx.text("Elastyczność",as_="b",font_size="1.3em"),
|
||||
rx.text("Wybierz, która twarz na zdjęciu ma pozostać niezmieniona, a my zadbamy o resztę.", text_align="left", ),width="315px",padding = "5px" ),
|
||||
rx.box(rx.text("Prosta obsługa",as_="b",font_size="1.3em"),
|
||||
rx.text("Intuicyjny interfejs użytkownika sprawia, że korzystanie z Blurme jest łatwe dla każdego.",text_align="left", ),margin_right= "40px", width="315px",padding = "5px"),
|
||||
align = "top"), text_align = "center"),
|
||||
|
||||
rx.vstack(rx.text("Do Czego?", color = "rgba(183,105,215,255)", font_size="2em", font_weight="bold", margin_top="25px", margin_bottom="5px"),
|
||||
rx.list(rx.hstack(
|
||||
rx.list_item(rx.text("Ochrona Tożsamości",as_="b",font_size="1.3em"),
|
||||
rx.text("Idealne do usuwania twarzy nieznajomych lub przypadkowych przechodniów z tła.", text_align="left"),),
|
||||
rx.list_item(rx.text("Zachowanie Prywatności", as_="b",font_size="1.3em"),
|
||||
rx.text("Przydatne w sytuacjach, gdy chcesz udostępnić zdjęcia publicznie, ale z zachowaniem prywatności pewnych osób.", text_align="left"),),
|
||||
rx.list_item(rx.text("Kreatywność",as_="b",font_size="1.3em"),
|
||||
rx.text("Wyraź swoją kreatywność, zachowując jednocześnie istotne elementy na zdjęciach.", text_align="left"),),
|
||||
margin_right = "40px", margin_left = "40px"), ),text_align = "center", margin_bottom = "50px"),
|
||||
|
||||
|
||||
# rx.text("Blurme to więcej niż tylko narzędzie - to rozwiązanie, które pozwala Ci kontrolować, jak prezentujesz swoje zdjęcia online, zachowując jednocześnie pełną prywatność. Przekształć swoje obrazy już teraz!", text_align = "left"),
|
||||
)#Indywidualizuj swoje zdjęcia zgodnie z własnymi potrzebami
|
||||
rx.wrap_item(rx.text("Aplikacja do anonimizacji zdjęć: usunięcie wybranych informacji identyfikujących ze zdjęć z wydarzeń i miejsc publicznych nigdy nie było tak proste!", font_size="1.2em"),
|
||||
w="45%",style={"flex-direction": "column"}),
|
||||
rx.wrap_item(rx.text("Tu będzie obrazek"),)
|
||||
),
|
||||
rx.link(rx.button("Zacznij anonimizować"),href= "settings",color="rgb(107,99,246)",button=True, padding = "50xp", font_size="1.2em"),
|
||||
rx.text("Dlaczego Blurme?", font_size="1.5em", font_weight="bold", margin_top="10px", margin_bottom="10px"),
|
||||
rx.list(
|
||||
rx.list_item(rx.text("Prywatność: ",as_="b"), "Nasza aplikacja zapewnia pełną ochronę prywatności, eliminując ryzyko identyfikacji osób niezwiązanych z anonimizacją."),
|
||||
rx.list_item(rx.text("Prosta obsługa: ",as_="b"),"Intuicyjny interfejs użytkownika sprawia, że korzystanie z Blurme jest łatwe dla każdego."),
|
||||
rx.list_item(rx.text("Elastyczność: ",as_="b"),"Wybierz, która twarz na zdjęciu ma pozostać niezmieniona, a my zadbamy o resztę. Indywidualizuj swoje zdjęcia zgodnie z własnymi potrzebami."),
|
||||
text_align="left"),
|
||||
rx.text("Jak to działa?", font_size="1.5em", font_weight="bold", margin_top="20px", margin_bottom="10px"),
|
||||
rx.ordered_list(
|
||||
rx.list_item(rx.text("Wgraj Zdjęcie: ",as_="b"), "Prosty proces rozpoczyna się od dodania zdjęcia, które chcesz anonimizować."),
|
||||
rx.list_item(rx.text("Zaznacz Twarz do Ochrony: ",as_="b"), "Oznacz twarz, którą chcesz zachować."),
|
||||
rx.list_item(rx.text("Anonimizuj Resztę: ",as_="b"), "Naciśnij przycisk, a Blurme automatycznie zastosuje efekt rozmycia do wszystkich obszarów poza zaznaczoną twarzą."),
|
||||
rx.list_item(rx.text("Pobierz i Podziel Się: ",as_="b"), "Gotowe! Pobierz anonimizowane zdjęcie i dziel się nim bez obaw o prywatność.")
|
||||
),
|
||||
rx.text("Do Czego Może Ci Się Przydać?", font_size="1.5em", font_weight="bold", margin_top="20px", margin_bottom="10px"),
|
||||
rx.list(
|
||||
rx.list_item(rx.text("Ochrona Tożsamości: ",as_="b"),"Idealne do usuwania twarzy nieznajomych lub przypadkowych przechodniów z tła.", text_align="left"),
|
||||
rx.list_item(rx.text("Zachowanie Prywatności: ",as_="b"),"Przydatne w sytuacjach, gdzie chcesz udostępnić zdjęcia publicznie, ale z zachowaniem prywatności pewnych osób.", text_align="left"),
|
||||
rx.list_item(rx.text("Kreatywność: ",as_="b"),"Wyraź swoją kreatywność, zachowując jednocześnie istotne elementy na zdjęciach.", text_align="left"),
|
||||
),
|
||||
rx.text("Blurme to więcej niż tylko narzędzie - to rozwiązanie, które pozwala Ci kontrolować, jak prezentujesz swoje zdjęcia online, zachowując jednocześnie pełną prywatność. Przekształć swoje obrazy już teraz!", text_align = "center"),
|
||||
)
|
||||
|
@ -1,106 +1,22 @@
|
||||
"""The settings page."""
|
||||
import asyncio
|
||||
|
||||
from blurme.templates import template
|
||||
from blurme.state import State
|
||||
|
||||
import reflex as rx
|
||||
|
||||
|
||||
color = "rgb(107,99,246)"
|
||||
|
||||
@template(route="/settings", title="Zdjęcie", image = "/image-icon.png")
|
||||
@template(route="/settings", title="Settings")
|
||||
def settings() -> rx.Component:
|
||||
icon_style = {"width": "20px", "height": "30px", "margin-right": "10px"}
|
||||
"""The settings page.
|
||||
|
||||
Returns:
|
||||
The UI for the settings page.
|
||||
"""
|
||||
return rx.vstack(
|
||||
rx.wrap(
|
||||
rx.hstack(
|
||||
rx.vstack(
|
||||
rx.image(src="/brush-logo.png", height="5.2em", margin_bottom="15px"),
|
||||
rx.heading("Anonimizuj już teraz!", font_size="2em", margin_bottom="15px",
|
||||
background_image="linear-gradient(271.68deg, #7566fe 0.75%, #f96caf 88.52%)",
|
||||
background_clip="text", padding="5px"),
|
||||
rx.text("Dodaj zdjęcie, wybierz obszar i ukryj tożsamość: Anonimizacja zdjęć w mgnieniu oka – szybko, łatwo, bezpłatnie!",
|
||||
font_size="1.2em", text_align="center"), width="50%", margin_left="50px", margin_right="30px"),
|
||||
rx.vstack(
|
||||
rx.upload(
|
||||
rx.vstack(
|
||||
rx.image(src="/upload-icon.png", height="5em"),
|
||||
rx.text("Przeciągnij, aby wybrać pliki", font_size="1em"),
|
||||
rx.text("albo", font_size="0.8em"),
|
||||
rx.button("Kliknij", font_size="1em", color="rgba(117,102,254,255)"),
|
||||
rx.text(" "),
|
||||
rx.text("Dostępne rozszerzenia plików: png, jpg, webp, tiff", font_size="0.8em"),
|
||||
),
|
||||
border=f"1px dotted {color}",
|
||||
padding="1em",
|
||||
width="120%",
|
||||
max_width = "120%",
|
||||
multiple=True,
|
||||
margin_top="50px",
|
||||
style={"align-self": "center"},
|
||||
spacing="2em",
|
||||
),
|
||||
rx.foreach(rx.selected_files, rx.text),
|
||||
rx.box(
|
||||
rx.button("Załaduj zdjęcie",
|
||||
on_click=lambda: State.handle_upload(rx.upload_files()),
|
||||
color="rgba(47, 187, 74,255)"),
|
||||
rx.alert_dialog(
|
||||
rx.alert_dialog_overlay(
|
||||
rx.alert_dialog_content(
|
||||
rx.alert_dialog_header("Przekroczono limit plików"),
|
||||
rx.alert_dialog_body("Nie można przesłać więcej niż 5 plików."),
|
||||
rx.alert_dialog_footer(
|
||||
rx.button("Zamknij", color="rgba(255, 75, 42,255)",
|
||||
on_click=State.toggle_show(),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
is_open=State.show,
|
||||
),
|
||||
rx.alert_dialog(
|
||||
rx.alert_dialog_overlay(
|
||||
rx.alert_dialog_content(
|
||||
rx.alert_dialog_header("Nieprawidłowe rozszerzenie pliku"),
|
||||
rx.alert_dialog_body("Dozwolone rozszerzenia plików to: png, jpg, webp, tiff."),
|
||||
rx.alert_dialog_footer(
|
||||
rx.button("Zamknij", color="rgba(255, 75, 42,255)",
|
||||
on_click=State.toggle_extension_alert(),
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
is_open=State.show_extension_alert,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
rx.responsive_grid(
|
||||
rx.foreach(
|
||||
State.img,
|
||||
lambda img: rx.vstack(
|
||||
rx.image(src=f'/{img}', max_height="450px"),
|
||||
rx.text(img, font_size="15px"),
|
||||
rx.hstack(
|
||||
rx.button(rx.icon(tag="delete", margin_right="8px"), " Usuń",
|
||||
on_click=lambda img_name=img: State.delete_image(img_name),
|
||||
color="rgba(255, 75, 42,255)", width="100px"),
|
||||
rx.button(rx.icon(tag="check", margin_right="8px"), " Wykrywaj",
|
||||
on_click=lambda img_name=img: State.image_analysis(img_name),
|
||||
width="130px"),
|
||||
rx.button(rx.icon(tag="edit", margin_right="8px"), " Anonimizuj",
|
||||
on_click=lambda img_name=img: State.image_anonymization(img_name),
|
||||
width="130px"),
|
||||
rx.button(rx.icon(tag="download", margin_right="8px"), " Pobierz",
|
||||
on_click=lambda img_name=img: State.download_image(img_name),
|
||||
width="100px"),
|
||||
padding="1em"
|
||||
),
|
||||
),
|
||||
),
|
||||
columns=[1],
|
||||
spacing="5px",
|
||||
),
|
||||
rx.heading("BlurMe", font_size="3em"),
|
||||
rx.text("Dodaj zdjęcia, które chcesz zanonimizować!"),
|
||||
)
|
||||
# rx.text(
|
||||
# "You can edit this page in ",
|
||||
# rx.code("{your_app}/pages/settings.py"),
|
||||
# ),
|
||||
|
@ -1,95 +1,12 @@
|
||||
"""Base state for the app."""
|
||||
|
||||
import reflex as rx
|
||||
import os
|
||||
import stat
|
||||
#from blurme import styles
|
||||
import asyncio
|
||||
from graphics.image_modification import blur_boxes_on_image
|
||||
from graphics.image_modification import show_image_with_boxes
|
||||
from ml.element_detection import BoundBox, detect
|
||||
|
||||
|
||||
class State(rx.State):
|
||||
"""The app state."""
|
||||
"""Base state for the app.
|
||||
|
||||
# The images to show.
|
||||
img: list[str] = []
|
||||
The base state is used to store general vars used throughout the app.
|
||||
"""
|
||||
|
||||
show: bool = False
|
||||
def toggle_show(self):
|
||||
self.show = not self.show
|
||||
|
||||
show_extension_alert: bool = False
|
||||
def toggle_extension_alert(self):
|
||||
self.show_extension_alert = not self.show_extension_alert
|
||||
|
||||
def change(self):
|
||||
self.show = not (self.show)
|
||||
|
||||
|
||||
async def handle_upload(self, files: list[rx.UploadFile]):
|
||||
"""Handle the upload of file(s).
|
||||
Args:
|
||||
files: The uploaded files.
|
||||
"""
|
||||
max_files_limit = 5
|
||||
acceptable_extensions = {"png", "jpg", "jpeg", "webp", "tif", "tiff"}
|
||||
|
||||
if len(self.img) + len(files) > max_files_limit:
|
||||
self.toggle_show()
|
||||
return
|
||||
|
||||
for file in files:
|
||||
upload_data = await file.read()
|
||||
outfile = rx.get_asset_path(file.filename)
|
||||
|
||||
extension = file.filename.split(".")[-1]
|
||||
|
||||
if extension.lower() not in acceptable_extensions:
|
||||
self.toggle_extension_alert()
|
||||
return
|
||||
|
||||
# Save the file.
|
||||
with open(outfile, "wb") as file_object:
|
||||
file_object.write(upload_data)
|
||||
|
||||
# Update the img var.
|
||||
self.img.append(file.filename)
|
||||
|
||||
self.img = self.img
|
||||
|
||||
|
||||
def delete_image(self, img_name: str):
|
||||
if img_name in self.img:
|
||||
self.img.remove(img_name)
|
||||
self.img=self.img
|
||||
|
||||
def download_image(self, img_name):
|
||||
print(self.img)
|
||||
return rx.download(url=f'/{img_name}', filename=img_name)
|
||||
|
||||
async def image_analysis(self, img_name: str):
|
||||
if img_name in self.img:
|
||||
stripped_image_name = str(img_name).lstrip("analysis-").lstrip("anonim-")
|
||||
image_path = rx.get_asset_path(stripped_image_name)
|
||||
#timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
|
||||
new_image_name = f"analysis-{stripped_image_name}"
|
||||
new_image_path = rx.get_asset_path(new_image_name)
|
||||
show_image_with_boxes(image_path, detect(image_path), new_image_path)
|
||||
upload_file = rx.UploadFile(file=open(new_image_path, 'rb'), filename=new_image_name)
|
||||
self.delete_image(img_name)
|
||||
await self.handle_upload([upload_file])
|
||||
self.img=self.img
|
||||
|
||||
async def image_anonymization(self, img_name: str):
|
||||
if img_name in self.img:
|
||||
stripped_image_name = str(img_name).lstrip("analysis-").lstrip("anonim-")
|
||||
image_path = rx.get_asset_path(stripped_image_name)
|
||||
#timestamp = datetime.now().strftime("%Y%m%d%H%M%S%f")
|
||||
new_image_name = f"anonim-{stripped_image_name}"
|
||||
new_image_path = rx.get_asset_path(new_image_name)
|
||||
blur_boxes_on_image(image_path, detect(image_path), new_image_path)
|
||||
upload_file = rx.UploadFile(file=open(new_image_path, 'rb'), filename=new_image_name)
|
||||
self.delete_image(img_name)
|
||||
await self.handle_upload([upload_file])
|
||||
self.img=self.img
|
||||
pass
|
||||
|
@ -1,18 +1,13 @@
|
||||
"""Styles for the app."""
|
||||
|
||||
import reflex as rx
|
||||
from blurme.state import State
|
||||
|
||||
border_radius = "0.375rem"
|
||||
box_shadow = "0px 0px 0px 1px rgba(84, 82, 95, 0.14)"
|
||||
border = "1px solid #F4F3F6"
|
||||
text_color = "black"
|
||||
#dark_mode_state = State.toggle_dark_mode()
|
||||
#text_color = "white" if dark_mode_state == True else "purple"
|
||||
text_color_light = "black"
|
||||
text_color_dark = "white"
|
||||
accent_text_color = "black"
|
||||
accent_color = "linear-gradient(linear-gradient(43deg, rgba(201,238,248,1) 18%, rgba(253,210,227,0.7792366946778712) 86%)"
|
||||
accent_text_color = "#1A1060"
|
||||
accent_color = "#d9a2ca"
|
||||
hover_accent_color = {"_hover": {"color": accent_color}}
|
||||
hover_accent_bg = {"_hover": {"bg": accent_color}}
|
||||
content_width_vw = "90vw"
|
||||
@ -40,6 +35,15 @@ overlapping_button_style = {
|
||||
"border_radius": border_radius,
|
||||
}
|
||||
|
||||
base_style = {
|
||||
rx.MenuButton: {
|
||||
"width": "3em",
|
||||
"height": "3em",
|
||||
**overlapping_button_style,
|
||||
},
|
||||
rx.MenuItem: hover_accent_bg,
|
||||
}
|
||||
|
||||
markdown_style = {
|
||||
"code": lambda text: rx.code(text, color="#1F1944", bg="#EAE4FD", margin="10px 0"),
|
||||
"a": lambda text, **props: rx.link(
|
||||
|
@ -1,2 +1 @@
|
||||
from .template import template
|
||||
|
||||
|
@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
from blurme import styles
|
||||
from blurme.components.sidebar import sidebar
|
||||
from typing import Callable
|
||||
from blurme.state import State
|
||||
|
||||
import reflex as rx
|
||||
|
||||
@ -17,27 +16,55 @@ default_meta = [
|
||||
},
|
||||
]
|
||||
|
||||
def dark_mode() -> rx.Component:
|
||||
|
||||
def menu_button() -> rx.Component:
|
||||
"""The menu button on the top right of the page.
|
||||
|
||||
Returns:
|
||||
The menu button component.
|
||||
"""
|
||||
from reflex.page import get_decorated_pages
|
||||
|
||||
return rx.box(
|
||||
rx.button(
|
||||
rx.menu(
|
||||
rx.menu_button(
|
||||
rx.icon(
|
||||
tag="moon",
|
||||
tag="hamburger",
|
||||
size="4em",
|
||||
color=styles.text_color,
|
||||
),
|
||||
on_click= rx.toggle_color_mode
|
||||
#State.toggle_dark_mode()
|
||||
),
|
||||
rx.menu_list(
|
||||
*[
|
||||
rx.menu_item(
|
||||
rx.link(
|
||||
page["title"],
|
||||
href=page["route"],
|
||||
width="100%",
|
||||
)
|
||||
)
|
||||
for page in get_decorated_pages()
|
||||
],
|
||||
rx.menu_divider(),
|
||||
rx.menu_item(
|
||||
rx.link("About", href="https://github.com/reflex-dev", width="100%")
|
||||
),
|
||||
rx.menu_item(
|
||||
rx.link("Contact", href="mailto:founders@=reflex.dev", width="100%")
|
||||
),
|
||||
),
|
||||
),
|
||||
position="fixed",
|
||||
right="1.5em",
|
||||
top="1.5em",
|
||||
z_index="500",)
|
||||
z_index="500",
|
||||
)
|
||||
|
||||
|
||||
def template(
|
||||
route: str | None = None,
|
||||
title: str | None = None,
|
||||
image: str | None = None,
|
||||
image_dark: str | None = None,
|
||||
description: str | None = None,
|
||||
meta: str | None = None,
|
||||
script_tags: list[rx.Component] | None = None,
|
||||
@ -49,10 +76,9 @@ def template(
|
||||
route: The route to reach the page.
|
||||
title: The title of the page.
|
||||
image: The favicon of the page.
|
||||
image_dark: The dark mode favicon of the page.
|
||||
description: The description of the page.
|
||||
meta: Additional meta to add to the page.
|
||||
on_load: The event handler(s) called when the page loads.
|
||||
meta: Additionnal meta to add to the page.
|
||||
on_load: The event handler(s) called when the page load.
|
||||
script_tags: Scripts to attach to the page.
|
||||
|
||||
Returns:
|
||||
@ -90,7 +116,7 @@ def template(
|
||||
),
|
||||
**styles.template_page_style,
|
||||
),
|
||||
dark_mode(),
|
||||
menu_button(),
|
||||
align_items="flex-start",
|
||||
transition="left 0.5s, width 0.5s",
|
||||
position="relative",
|
||||
|
@ -1 +0,0 @@
|
||||
<code_scheme name="Default" version="173" />
|
@ -1,5 +0,0 @@
|
||||
<application>
|
||||
<component name="EditorColorsManagerImpl">
|
||||
<global_color_scheme name="_@user_Darcula" />
|
||||
</component>
|
||||
</application>
|
@ -1,6 +0,0 @@
|
||||
<application>
|
||||
<component name="XDebuggerSettings">
|
||||
<data-views />
|
||||
<general />
|
||||
</component>
|
||||
</application>
|
@ -1,6 +0,0 @@
|
||||
<application>
|
||||
<component name="DefaultFont">
|
||||
<option name="FONT_SIZE" value="18" />
|
||||
<option name="FONT_SCALE" value="1.5" />
|
||||
</component>
|
||||
</application>
|
@ -1,7 +0,0 @@
|
||||
<application>
|
||||
<component name="FileTypeManager" version="17">
|
||||
<extensionMap>
|
||||
<mapping ext="tex" type="PLAIN_TEXT" />
|
||||
</extensionMap>
|
||||
</component>
|
||||
</application>
|
@ -1,48 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import List
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
from PIL.Image import composite
|
||||
|
||||
from ml.element_detection import BoundBox
|
||||
|
||||
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
def show_image_with_boxes(
|
||||
in_image_path: str, bounding_boxes: List[BoundBox], out_image_path: str = None
|
||||
):
|
||||
img = Image.open(in_image_path)
|
||||
draw = ImageDraw.Draw(img)
|
||||
font_path = DIR_PATH + "/assets/fonts/arial.ttf"
|
||||
font = ImageFont.truetype(font_path, 25)
|
||||
for i, box in enumerate(bounding_boxes):
|
||||
draw.rectangle(box.get_params(), outline="red", width=2, fill=None)
|
||||
draw.text((box.x1 + 5, box.y1 + 5), str(i + 1), fill="red", font=font)
|
||||
if not out_image_path:
|
||||
out_image_path = (
|
||||
in_image_path.split(".")[0] + "_out." + in_image_path.split(".")[1]
|
||||
)
|
||||
img.save(out_image_path)
|
||||
|
||||
|
||||
def blur_boxes_on_image(
|
||||
in_image_path: str, bounding_boxes: List[BoundBox], out_image_path: str = None
|
||||
):
|
||||
img = Image.open(in_image_path)
|
||||
blur_img = img.filter(ImageFilter.GaussianBlur(radius=13)) # to increase the blur, increase the radius
|
||||
mask = Image.new("L", img.size, 255)
|
||||
draw_mask = ImageDraw.Draw(mask)
|
||||
for box in bounding_boxes:
|
||||
if box.selected:
|
||||
if box.object == "plate":
|
||||
draw_mask.rectangle(box.get_params(), fill=0)
|
||||
elif box.object == "face":
|
||||
draw_mask.ellipse(box.get_params(), fill=0)
|
||||
mask = mask.filter(ImageFilter.GaussianBlur(radius=3))
|
||||
img = composite(img, blur_img, mask)
|
||||
if not out_image_path:
|
||||
out_image_path = (
|
||||
in_image_path.split(".")[0] + "_blurred." + in_image_path.split(".")[1]
|
||||
)
|
||||
img.save(out_image_path)
|
Before Width: | Height: | Size: 48 KiB |
@ -1,56 +0,0 @@
|
||||
import os
|
||||
from typing import List, Tuple
|
||||
|
||||
import torch
|
||||
|
||||
from ultralytics import YOLO
|
||||
|
||||
DIR_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
FACES_PATH = os.path.join(DIR_PATH, "assets/weights/yolov8n-face.pt")
|
||||
PLATES_PATH = os.path.join(DIR_PATH, "assets/weights/yolov8-plate.pt")
|
||||
FACES_MODEL = YOLO(FACES_PATH)
|
||||
PLATES_MODEL = YOLO(PLATES_PATH)
|
||||
CONF_THRESH = 0.3
|
||||
IOU_THRESH = 0.5
|
||||
|
||||
|
||||
class BoundBox:
|
||||
def __init__(self, x1, y1, x2, y2, object=None):
|
||||
self.x1, self.y1, self.x2, self.y2 = x1, y1, x2, y2
|
||||
self.selected = True
|
||||
if object not in ["face", "plate"]:
|
||||
raise ValueError("object must be either 'face' or 'plate'")
|
||||
self.object = object
|
||||
|
||||
def select(self):
|
||||
self.selected = True
|
||||
|
||||
def unselect(self):
|
||||
self.selected = False
|
||||
|
||||
def get_params(self) -> Tuple[int, int, int, int]:
|
||||
return self.x1, self.y1, self.x2, self.y2
|
||||
|
||||
|
||||
def detect(image_path: str) -> List[BoundBox]:
|
||||
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
faces = FACES_MODEL.predict(
|
||||
source=image_path, conf=CONF_THRESH, iou=IOU_THRESH, device=device
|
||||
)
|
||||
faces = faces[0].cpu().numpy().boxes
|
||||
plates = PLATES_MODEL.predict(
|
||||
source=image_path, conf=CONF_THRESH, iou=IOU_THRESH, device=device
|
||||
)
|
||||
plates = plates[0].cpu().numpy().boxes
|
||||
bounding_boxes = []
|
||||
for boxes, tag in zip([faces, plates], ["face", "plate"]):
|
||||
for box in boxes:
|
||||
xyxyn = box.xyxy[0]
|
||||
x1 = int(xyxyn[0])
|
||||
y1 = int(xyxyn[1])
|
||||
x2 = int(xyxyn[2])
|
||||
y2 = int(xyxyn[3])
|
||||
bounding_boxes.append(BoundBox(x1, y1, x2, y2, tag))
|
||||
return bounding_boxes
|
||||
|
||||
|
59
ml/test.py
@ -1,59 +0,0 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock, patch
|
||||
from element_detection import detect, BoundBox
|
||||
from graphics.image_modification import show_image_with_boxes
|
||||
|
||||
|
||||
class TestYourModule(unittest.TestCase):
|
||||
|
||||
@patch('element_detection.FACES_MODEL.predict')
|
||||
@patch('element_detection.PLATES_MODEL.predict')
|
||||
def test_detect(self, mock_plates_predict, mock_faces_predict):
|
||||
# Przygotuj dane testowe
|
||||
image_path = 'blurme/ml/MicrosoftTeams-image.png'
|
||||
mock_faces_predict.__iter__.return_value = [Mock(cpu=Mock(return_value=Mock(numpy=Mock(boxes=[Mock(xyxy=[1, 2, 3, 4])]))))]
|
||||
mock_plates_predict.__iter__.return_value = [Mock(cpu=Mock(return_value=Mock(numpy=Mock(boxes=[Mock(xyxy=[5, 6, 7, 8])]))))]
|
||||
|
||||
# Wywołaj funkcję
|
||||
result = detect(image_path)
|
||||
|
||||
# Sprawdź, czy otrzymałeś oczekiwany wynik
|
||||
expected_result = [BoundBox(1, 2, 3, 4), BoundBox(5, 6, 7, 8)]
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
@patch('element_detection.ImageDraw.Draw')
|
||||
@patch('element_detection.ImageFont.truetype')
|
||||
#@patch('element_detection.Image.save')
|
||||
@patch('element_detection.Image.open')
|
||||
def test_show_image_with_boxes(self, mock_image_open,
|
||||
#mock_image_save,
|
||||
mock_truetype, mock_draw):
|
||||
# Przygotuj dane testowe
|
||||
in_image_path = 'blurme/ml/MicrosoftTeams-image.png'
|
||||
bounding_boxes = [BoundBox(1, 2, 3, 4), BoundBox(5, 6, 7, 8)]
|
||||
|
||||
# Wywołaj funkcję
|
||||
show_image_with_boxes(in_image_path, bounding_boxes)
|
||||
|
||||
# Sprawdź, czy odpowiednie metody zostały wywołane z oczekiwanymi argumentami
|
||||
mock_image_open.assert_called_once_with(in_image_path)
|
||||
mock_draw.assert_called_once()
|
||||
# mock_image_save.assert_called_once()
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
def test_detect_with_stub(self, mock_plates_predict, mock_faces_predict):
|
||||
# Przygotuj dane testowe
|
||||
image_path = 'blurme/ml/MicrosoftTeams-image.png'
|
||||
mock_faces_predict.__iter__.return_value = [Mock(cpu=Mock(return_value=Mock(numpy=Mock(boxes=[Mock(xyxy=[1, 2, 3, 4])]))))]
|
||||
mock_plates_predict.__iter__.return_value = [Mock(cpu=Mock(return_value=Mock(numpy=Mock(boxes=[Mock(xyxy=[5, 6, 7, 8])]))))]
|
||||
|
||||
# Użyj stuba zamiast rzeczywistego BoundBox
|
||||
with patch('element_detection.BoundBox', StubBoundBox):
|
||||
# Wywołaj funkcję
|
||||
result = detect(image_path)
|
||||
|
||||
# Sprawdź, czy otrzymałeś oczekiwany wynik
|
||||
expected_result = [StubBoundBox(1, 2, 3, 4), StubBoundBox(5, 6, 7, 8)]
|
||||
self.assertEqual(result, expected_result)
|
@ -1,5 +1,2 @@
|
||||
Pillow==10.1.0
|
||||
|
||||
reflex==0.3.2
|
||||
torch==2.1.1
|
||||
torchvision==0.16.1
|
||||
ultralytics==8.0.212
|
@ -1,5 +0,0 @@
|
||||
<application>
|
||||
<component name="UISettings">
|
||||
<option name="FONT_SCALE" value="1.5" />
|
||||
</component>
|
||||
</application>
|