Merge pull request #1 from PaulleDemon/widget-drag-and-drop

Merging Drag and drop features
This commit is contained in:
Art/Paul
2024-09-22 15:40:26 +05:30
committed by GitHub
36 changed files with 2567 additions and 565 deletions

50
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,50 @@
name: Bug report
description: Create a report to help us improve
title: "[BUG]: "
labels: ["bug"]
assignees:
- PaulleDemon
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to fill out this bug report!
- type: input
id: template-name
attributes:
label: Template name
description: Please tell us which template is the issue related to
placeholder: Template name
value: "Template name"
validations:
required: true
- type: input
id: browser
attributes:
label: Browser & version
description: Please tell us which browser and version of the browser you are using
placeholder: Browser name & version
value: "Browser name"
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Also tell us, what did you expect to happen?
placeholder: Tell us what you see!
value: "A bug happened!"
validations:
required: true
- type: checkboxes
id: related-issues
attributes:
label: This is a new issue
description: I have checked for the same issue in the issues panel
options:
- label: "Yes"
required: true

View File

@@ -0,0 +1,46 @@
name: New Feature request
description: Have an idea for new feature? well tell us
title: "[FEATURE REQUEST]: "
labels: ["feature request"]
assignees:
- PaulleDemon
body:
- type: markdown
attributes:
value: |
> [!NOTE]
> Please make sure to check the issues tab before requesting a new feature.
- type: textarea
id: feature-description
attributes:
label: Feature Description
description: "Describe about the feature in brief"
placeholder: "Tell us about the feature request in brief"
value: "Feature description"
validations:
required: true
- type: textarea
id: functionalities
attributes:
label: Functionalities of the Feature
description: "What should the feature do? eg: a bold button to make the text bold"
placeholder: Functions of the feature
value: "Functions of the feature"
validations:
required: true
- type: checkboxes
id: related-issues
attributes:
label: This is a new feature request
description: I acknowledge that I have checked the issues section for similar request and found none.
options:
- label: "Yes"
required: true
- type: markdown
attributes:
value: |
The feature request priority are based on number of reaction, a good number of thumbs up will make it to priority queue

3
.github/funding.yaml vendored Normal file
View File

@@ -0,0 +1,3 @@
ko_fi: artpaul
github: [PaulleDemon]
buy_me_a_coffee: artpaul

23
License.txt Normal file
View File

@@ -0,0 +1,23 @@
License held by paul
Github username: PaulleDemon
1. License Grant
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, publish, and distribute the source code, subject to the following conditions:
2. Derived Work License
Any modifications, derivative works, or copies of the Software must retain this license. All derivative works must be open-sourced under the same license terms. You are not permitted to relicense or sub-license any portion of the Software under a different license without explicit prior written permission from the original author.
3. Distribution Restriction on Executables
You are not permitted to distribute the Software in compiled, executable, or binary form without prior written consent from the original author. This applies to both original and derivative works.
4. Commercial Use Restriction
The Software, in its original or modified form, cannot be used for any commercial purpose without prior written permission from the original author. This includes selling, licensing, or offering the Software as part of a service.
5. Non-Commercial Use
You may freely use, copy, modify, and distribute the source code of the Software for non-commercial purposes, provided that the same terms of this license are maintained in all copies or derivative works.
6. Changes to the License
The original author reserves the right to modify, amend, or update this license at any time. Any changes will apply only to future versions of the Software. Any prior versions of the Software, including derivative works, will continue to be governed by the version of the license in effect at the time the work was created.
7. Disclaimer
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1 +1,18 @@
# TkBuilder
# PyUIBuilder - The only Python GUI builder you'll ever need
## Features
* Framework agnostic - Can outputs code in multiple frameworks.
* Easy to use.
* Plugins to extend 3rd party UI libraries
* Generate Code.
## Roadmap
### All code generated by the builder tools are under MIT license and can be used commercially

1
notes.md Normal file
View File

@@ -0,0 +1 @@
### State management in react is a f*king mess

View File

@@ -15,6 +15,7 @@
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" integrity="sha512-dPXYcDub/aeb08c63jRq/k6GaKccl256JQy/AnOq7CAnEZ9FzSL9wSbcZkMp4R26vBsMLFYH4kQ67/bbV8XaCQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
@@ -24,7 +25,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<title>PyUI builder</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

32
roadmap.md Normal file
View File

@@ -0,0 +1,32 @@
## Road map for PyUIBuilder
Any feature that has 👑 beside it, is meant only for [premium users](./readme.md#license--support)
### 1.0.0
- [x] Create the initial version for UI builder
### 1.2.0
- [ ] UI fixes and enhancement
- [ ] Tree view for elements on the canvas
- [ ] Add text editor to support event handlers
- [ ] Rewrite DND for better feedback - (swappy/react-dnd-kit/ GSAP draggable)
- [ ] Duplicate widgets
### 1.5.0
- [ ] Add canvas support tools (lines, rect etc) (try fabricjs)
- [ ] Initial version for Electron App 👑
- [ ] Save files locally 👑
- [ ] Load UI files 👑
- [ ] Light/Dark theme 👑
- [ ] Run the preview 👑
### 2.0.0
- [ ] Support for more third party plugins
- [ ] Support more templates
- [ ] Support for Kivy
- [ ] Sharable Templates
- [ ] Dark theme 👑
### 3.0.0
- [ ] Support for PySide / PyQt 👑 (commercial license only)

View File

@@ -12,6 +12,9 @@ import WidgetsContainer from './sidebar/widgetsContainer'
import Widget from './canvas/widgets/base'
import { DraggableWidgetCard } from './components/cards'
import { DragProvider } from './components/draggable/draggableContext'
import { ActiveWidgetProvider } from './canvas/activeWidgetContext'
import TkinterSidebar from './frameworks/tkinter/sidebarWidgets'
function App() {
@@ -28,7 +31,7 @@ function App() {
const [dropAnimation, setDropAnimation] = useState(null)
const [sidebarWidgets, setSidebarWidgets] = useState([])
const [sidebarWidgets, setSidebarWidgets] = useState(TkinterSidebar || [])
const [canvasWidgets, setCanvasWidgets] = useState([]) // contains the reference to the widgets inside the canvas
const [activeSidebarWidget, setActiveSidebarWidget] = useState(null) // helps with the dnd overlay
@@ -41,7 +44,7 @@ function App() {
{
name: "Widgets",
icon: <LayoutFilled />,
content: <WidgetsContainer onWidgetsUpdate={(widgets) => setSidebarWidgets(widgets)}/>
content: <WidgetsContainer sidebarContent={TkinterSidebar} onWidgetsUpdate={(widgets) => setSidebarWidgets(widgets)}/>
},
{
name: "Plugins",
@@ -95,15 +98,6 @@ function App() {
return
}
setDropAnimation(null)
// FIXME: drop offset is not correct
// Calculate the dragged item's bounding rectangle
const itemRect = activeItemElement.getBoundingClientRect();
const itemCenterX = itemRect.left + itemRect.width / 2
const itemCenterY = itemRect.top + itemRect.height / 2
console.log("widget overlay: ", delta, itemRect)
// Get widget dimensions (assuming you have a way to get these)
const widgetWidth = activeItemElement.offsetWidth; // Adjust this based on how you get widget size
@@ -114,11 +108,6 @@ function App() {
const canvasTranslate = canvasRef.current.getCanvasTranslation()
const zoom = canvasRef.current.getZoom()
// let finalPosition = {
// x: (delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom,
// y: (delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom,
// }
let finalPosition = {
x: (initialPosition.x + delta.x - canvasContainerRect.x - canvasTranslate.x) / zoom - (widgetWidth / 2),
y: (initialPosition.y + delta.y - canvasContainerRect.y - canvasTranslate.y) / zoom - (widgetHeight / 2),
@@ -151,32 +140,18 @@ function App() {
<div className="tw-w-full tw-h-[100vh] tw-flex tw-flex-col tw-bg-primaryBg">
<Header className="tw-h-[6vh]"/>
<DndContext
modifiers={[snapCenterToCursor]}
collisionDetection={rectIntersection}
onDragStart={handleDragStart}
onDragMove={handleDragMove}
onDragEnd={handleDragEnd}
>
<DragProvider>
<div className="tw-w-full tw-h-[94vh] tw-flex">
<Sidebar tabs={sidebarTabs}/>
{/* <ActiveWidgetProvider> */}
<Canvas ref={canvasRef} widgets={canvasWidgets} onWidgetAdded={handleWidgetAddedToCanvas}/>
{/* </ActiveWidgetProvider> */}
</div>
{/* dragOverlay (dnd-kit) helps move items from one container to another */}
<DragOverlay dropAnimation={dropAnimation}>
{activeSidebarWidget ? (
<DraggableWidgetCard name={activeSidebarWidget.name}
img={activeSidebarWidget.img}
url={activeSidebarWidget.link}
innerRef={widgetOverlayRef}
/>
):
null}
</DragOverlay>
</DndContext>
</DragProvider>
</div>
)
}

View File

@@ -1,7 +1,7 @@
<svg width="100%" height="100%">
<pattern id="pattern-circles" x="0" y="0" width="50" height="50" patternUnits="userSpaceOnUse" patternContentUnits="userSpaceOnUse">
<circle id="pattern-circle" cx="10" cy="10" r="1.6257413380501518" fill="#000"></circle>
<pattern id="pattern-circles" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse" patternContentUnits="userSpaceOnUse">
<circle id="pattern-circle" cx="10" cy="10" r="1.6257413380501518" fill="#4d4f4e"></circle>
</pattern>
<rect id="rect" x="0" y="0" width="100%" height="100%" fill="url(#pattern-circles)"></rect>

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 390 B

52
src/assets/logo/bmc.svg Normal file
View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.0" id="katman_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 600 450" style="enable-background:new 0 0 600 450;" xml:space="preserve">
<style type="text/css">
.st0{fill:#010202;}
.st1{fill:#FFDD06;}
</style>
<path class="st0" d="M390.1,137.3l-0.2-0.1l-0.5-0.2C389.5,137.2,389.8,137.3,390.1,137.3z"/>
<path class="st0" d="M393.4,160.9l-0.3,0.1L393.4,160.9z"/>
<path class="st0" d="M390.1,137.2C390.1,137.2,390.1,137.2,390.1,137.2C390,137.2,390,137.2,390.1,137.2
C390.1,137.2,390.1,137.2,390.1,137.2z"/>
<path class="st0" d="M393.1,160.7l0.4-0.2l0.1-0.1l0.1-0.1C393.5,160.4,393.3,160.6,393.1,160.7z"/>
<path class="st0" d="M390.7,137.8l-0.4-0.4l-0.3-0.1C390.2,137.5,390.4,137.7,390.7,137.8z"/>
<path class="st0" d="M297,366.2c-0.3,0.1-0.6,0.3-0.8,0.6l0.2-0.2C296.7,366.4,296.9,366.3,297,366.2z"/>
<path class="st0" d="M351.5,355.4c0-0.3-0.2-0.3-0.1,0.9c0-0.1,0-0.2,0.1-0.3C351.4,355.9,351.4,355.7,351.5,355.4z"/>
<path class="st0" d="M345.8,366.2c-0.3,0.1-0.6,0.3-0.8,0.6l0.2-0.2C345.5,366.4,345.7,366.3,345.8,366.2z"/>
<path class="st0" d="M258.7,368.7c-0.2-0.2-0.5-0.3-0.8-0.4c0.2,0.1,0.5,0.2,0.6,0.3L258.7,368.7z"/>
<path class="st0" d="M250.2,360.5c0-0.3-0.1-0.7-0.3-1C250,359.8,250.1,360.1,250.2,360.5L250.2,360.5z"/>
<path class="st1" d="M308,212.8c-11.8,5.1-25.3,10.8-42.7,10.8c-7.3,0-14.5-1-21.5-3l12,123.6c0.4,5.2,2.8,10,6.6,13.5
c3.8,3.5,8.8,5.5,14,5.5c0,0,17.1,0.9,22.8,0.9c6.1,0,24.5-0.9,24.5-0.9c5.2,0,10.2-1.9,14-5.5c3.8-3.5,6.2-8.3,6.6-13.5l12.9-136.5
c-5.8-2-11.6-3.3-18.1-3.3C327.7,204.4,318.6,208.3,308,212.8z"/>
<path class="st0" d="M206.5,160.2l0.2,0.2l0.1,0.1C206.8,160.3,206.7,160.2,206.5,160.2z"/>
<path class="st0" d="M412.8,148.7l-1.8-9.1c-1.6-8.2-5.3-16-13.7-18.9c-2.7-0.9-5.8-1.4-7.8-3.3c-2.1-2-2.7-5-3.2-7.8
c-0.9-5.2-1.7-10.4-2.6-15.6c-0.8-4.5-1.4-9.5-3.4-13.5c-2.7-5.5-8.2-8.7-13.7-10.8c-2.8-1-5.7-1.9-8.6-2.7
c-13.7-3.6-28.1-4.9-42.2-5.7c-16.9-0.9-33.9-0.7-50.8,0.8c-12.6,1.1-25.8,2.5-37.7,6.9c-4.4,1.6-8.9,3.5-12.2,6.9
c-4.1,4.1-5.4,10.6-2.4,15.7c2.1,3.7,5.7,6.3,9.5,8c4.9,2.2,10.1,3.9,15.4,5c14.8,3.3,30,4.5,45.1,5.1c16.7,0.7,33.4,0.1,50.1-1.6
c4.1-0.5,8.2-1,12.3-1.6c4.8-0.7,7.9-7.1,6.5-11.4c-1.7-5.3-6.3-7.3-11.4-6.5c-0.8,0.1-1.5,0.2-2.3,0.3l-0.5,0.1
c-1.8,0.2-3.5,0.4-5.3,0.6c-3.6,0.4-7.2,0.7-10.9,1c-8.1,0.6-16.3,0.8-24.5,0.8c-8,0-16-0.2-24-0.8c-3.7-0.2-7.3-0.5-10.9-0.9
c-1.7-0.2-3.3-0.4-4.9-0.6l-1.6-0.2l-0.3,0l-1.6-0.2c-3.3-0.5-6.6-1.1-9.9-1.8c-0.3-0.1-0.6-0.3-0.8-0.5s-0.3-0.6-0.3-0.9
c0-0.3,0.1-0.7,0.3-0.9s0.5-0.4,0.8-0.5h0.1c2.8-0.6,5.7-1.1,8.6-1.6c1-0.2,1.9-0.3,2.9-0.4h0c1.8-0.1,3.6-0.4,5.4-0.7
c15.6-1.6,31.3-2.2,47-1.7c7.6,0.2,15.2,0.7,22.8,1.4c1.6,0.2,3.3,0.3,4.9,0.5c0.6,0.1,1.2,0.2,1.9,0.2l1.3,0.2
c3.7,0.5,7.3,1.2,11,2c5.4,1.2,12.3,1.6,14.7,7.4c0.8,1.9,1.1,3.9,1.5,5.9l0.5,2.5c0,0,0,0.1,0,0.1c1.3,5.9,2.5,11.8,3.8,17.7
c0.1,0.4,0.1,0.9,0,1.3c-0.1,0.4-0.3,0.9-0.5,1.2c-0.3,0.4-0.6,0.7-1,0.9c-0.4,0.2-0.8,0.4-1.2,0.4h0l-0.8,0.1l-0.8,0.1
c-2.4,0.3-4.9,0.6-7.3,0.9c-4.8,0.5-9.6,1-14.4,1.4c-9.6,0.8-19.1,1.3-28.7,1.6c-4.9,0.1-9.8,0.2-14.7,0.2
c-19.5,0-38.9-1.1-58.2-3.4c-2.1-0.2-4.2-0.5-6.3-0.8c1.6,0.2-1.2-0.2-1.7-0.2c-1.3-0.2-2.7-0.4-4-0.6c-4.5-0.7-8.9-1.5-13.4-2.2
c-5.4-0.9-10.5-0.4-15.4,2.2c-4,2.2-7.2,5.5-9.3,9.6c-2.1,4.3-2.7,9.1-3.7,13.7c-0.9,4.7-2.4,9.7-1.8,14.5
c1.2,10.3,8.4,18.7,18.8,20.6c9.8,1.8,19.6,3.2,29.5,4.4c38.7,4.7,77.9,5.3,116.7,1.7c3.2-0.3,6.3-0.6,9.5-1c1-0.1,2,0,2.9,0.3
c0.9,0.3,1.8,0.9,2.5,1.6c0.7,0.7,1.2,1.5,1.6,2.5c0.3,0.9,0.5,1.9,0.4,2.9l-1,9.6c-2,19.3-4,38.6-5.9,58
c-2.1,20.3-4.1,40.6-6.2,60.9c-0.6,5.7-1.2,11.4-1.8,17.1c-0.6,5.6-0.6,11.4-1.7,17c-1.7,8.7-7.6,14.1-16.2,16.1
c-7.9,1.8-16,2.7-24.1,2.8c-9,0-18-0.3-27-0.3c-9.6,0.1-21.4-0.8-28.8-8c-6.5-6.3-7.4-16.1-8.3-24.6c-1.2-11.2-2.4-22.5-3.5-33.7
l-6.5-62.5l-4.2-40.5c-0.1-0.7-0.1-1.3-0.2-2c-0.5-4.8-3.9-9.5-9.3-9.3c-4.6,0.2-9.8,4.1-9.3,9.3l3.1,30l6.5,62
c1.8,17.6,3.7,35.2,5.5,52.9c0.4,3.4,0.7,6.8,1.1,10.1c2,18.5,16.1,28.4,33.6,31.2c10.2,1.6,20.6,2,31,2.1
c13.3,0.2,26.7,0.7,39.7-1.7c19.3-3.5,33.8-16.4,35.9-36.5c0.6-5.8,1.2-11.6,1.8-17.3c2-19.1,3.9-38.2,5.9-57.4l6.4-62.5l2.9-28.6
c0.1-1.4,0.7-2.8,1.7-3.8s2.2-1.8,3.6-2c5.5-1.1,10.8-2.9,14.7-7.1C413.8,166.2,415,157.5,412.8,148.7z M205,154.9
c0.1,0-0.1,0.7-0.1,1C204.8,155.4,204.8,154.9,205,154.9z M205.5,159c0,0,0.2,0.1,0.3,0.4C205.6,159.2,205.5,159,205.5,159
L205.5,159z M206,159.7C206.2,160.1,206.3,160.3,206,159.7L206,159.7z M207.1,160.6L207.1,160.6
C207.1,160.6,207.2,160.6,207.1,160.6C207.1,160.6,207.1,160.6,207.1,160.6L207.1,160.6z M392.5,159.3c-2,1.9-5,2.8-7.9,3.2
c-33.1,4.9-66.8,7.4-100.3,6.3c-24-0.8-47.7-3.5-71.5-6.8c-2.3-0.3-4.8-0.8-6.4-2.5c-3-3.2-1.5-9.7-0.7-13.7
c0.7-3.6,2.1-8.4,6.4-8.9c6.6-0.8,14.4,2,20.9,3c7.9,1.2,15.9,2.2,23.8,2.9c34,3.1,68.7,2.6,102.5-1.9c6.2-0.8,12.3-1.8,18.5-2.9
c5.5-1,11.5-2.8,14.8,2.8c2.3,3.9,2.6,9,2.2,13.4C394.8,156.2,393.9,158,392.5,159.3L392.5,159.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
src/assets/logo/ko-fi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,61 @@
import React, { createContext, Component, useContext, useState, createRef, useMemo } from 'react'
// NOTE: using this context provider causes many re-rendering when the canvas is panned the toolbar updates unnecessarily
// Create the Context
export const ActiveWidgetContext = createContext()
// Create the Provider component
export class ActiveWidgetProvider extends Component {
state = {
activeWidgetId: null,
activeWidgetAttrs: {}
}
updateActiveWidget = (widget) => {
this.setState({ activeWidgetId: widget })
}
updateToolAttrs = (widgetAttrs) => {
this.setState({activeWidgetAttrs: widgetAttrs})
}
render() {
return (
<ActiveWidgetContext.Provider
value={{ ...this.state, updateActiveWidget: this.updateActiveWidget, updateToolAttrs: this.updateToolAttrs }}
>
{this.props.children}
</ActiveWidgetContext.Provider>
);
}
}
// Custom hook for function components
export const useActiveWidget = () => {
const context = useContext(ActiveWidgetContext)
if (context === undefined) {
throw new Error('useActiveWidget must be used within an ActiveWidgetProvider')
}
return useMemo(() => context, [context.activeWidgetId, context.activeWidgetAttrs, context.updateToolAttrs, context.updateActiveWidget])
}
// Higher-Order Component for class-based components
export const withActiveWidget = (WrappedComponent) => {
return class extends Component {
render() {
return (
<ActiveWidgetContext.Consumer>
{context => <WrappedComponent {...this.props} {...context} />}
</ActiveWidgetContext.Consumer>
)
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
const WidgetContainer = {
CANVAS: "canvas", // widget is on the canvas
SIDEBAR: "sidebar", // widget is contained inside sidebar
WIDGET: "widget", // widget is contained inside another widget
}
export default WidgetContainer

View File

@@ -1,7 +1,12 @@
const Layouts = {
PACK: "flex",
export const Layouts = {
FLEX: "flex",
GRID: "grid",
PLACE: "absolute"
}
export default Layouts
export const PosType = {
ABSOLUTE: "absolute",
RELATIVE: "relative",
NONE: "unset"
}

View File

@@ -5,6 +5,8 @@ const Tools = {
SELECT_DROPDOWN: "select_dropdown",
COLOR_PICKER: "color_picker",
EVENT_HANDLER: "event_handler", // shows a event handler with all the possible function in the dropdown
LAYOUT_MANAGER: "layout_manager"
}

View File

@@ -4,6 +4,8 @@ import { ColorPicker, Input, InputNumber, Select } from "antd"
import { capitalize } from "../utils/common"
import Tools from "./constants/tools.js"
import { useActiveWidget } from "./activeWidgetContext.js"
import { Layouts } from "./constants/layouts.js"
// FIXME: Maximum recursion error
@@ -14,11 +16,15 @@ import Tools from "./constants/tools.js"
* @param {string} widgetType
* @param {object} attrs - widget attributes
*/
const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
// const { activeWidgetAttrs } = useActiveWidget()
// console.log("active widget context: ", activeWidgetAttrs)
const [toolbarOpen, setToolbarOpen] = useState(isOpen)
const [toolbarAttrs, setToolbarAttrs] = useState(attrs)
useEffect(() => {
setToolbarOpen(isOpen)
}, [isOpen])
@@ -34,6 +40,93 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
}
}
const renderLayoutManager = (val) => {
return (
<div className="tw-flex tw-flex-col tw-gap-2">
<Select
options={[
{ value: Layouts.FLEX, label: "Flex" },
{ value: Layouts.GRID, label: "Grid" },
{ value: Layouts.PLACE, label: "Place" },
]}
showSearch
value={val.value?.layout || ""}
placeholder={`${val.label}`}
size="medium"
onChange={(value) => handleChange({ ...val.value, layout: value }, val.onChange)}
/>
<div className="tw-flex tw-flex-col tw-gap-1">
<span className="tw-text-sm">Direction</span>
<Select
options={[
{ value: "column", label: "Vertical" },
{ value: "row", label: "Horizontal" },
]}
showSearch
value={val.value?.direction || "row"}
placeholder={`${val.label}`}
onChange={(value) => handleChange({ ...val.value, direction: value }, val.onChange)}
/>
</div>
<div className="tw-flex tw-flex-col">
<span className="tw-text-sm">Gap</span>
<InputNumber
max={500}
min={1}
value={val.value?.gap || 10}
size="small"
onChange={(value) => {
handleChange({ ...val.value, gap: value }, val.onChange)
}}
/>
</div>
<div className="tw-flex tw-flex-col">
<span className="tw-text-sm tw-font-medium">Grids</span>
<div className="tw-flex tw-gap-2">
<div className="tw-flex tw-flex-col">
<span className="tw-text-sm">Rows</span>
<InputNumber
max={12}
min={1}
value={val.value?.grid.rows || 1}
size="small"
onChange={(value) => {
let newGrid = {
rows: value,
cols: val.value?.grid.cols
}
handleChange({ ...val.value, grid: newGrid }, val.onChange)
}}
/>
</div>
<div className="tw-flex tw-flex-col">
<span className="tw-text-sm">Columns</span>
<InputNumber
max={12}
min={1}
value={val.value?.grid.cols || 1}
size="small"
onChange={(value) => {
let newGrid = {
rows: val.value?.grid.cols,
cols: value
}
handleChange({ ...val.value, grid: newGrid }, val.onChange)
}}
/>
</div>
</div>
</div>
</div>
)
}
const renderWidgets = (obj, parentKey = "") => {
return Object.entries(obj).map(([key, val], i) => {
const keyName = parentKey ? `${parentKey}.${key}` : key
@@ -42,14 +135,14 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
const isFirstLevel = parentKey === ""
const outerLabelClass = isFirstLevel
? "tw-text-lg tw-text-blue-700 tw-font-medium"
: "tw-text-lg"
? "tw-text-base tw-text-blue-700 tw-font-medium"
: "tw-text-base"
// Render tool widgets
if (typeof val === "object" && val.tool) {
return (
<div key={keyName} className="tw-flex tw-flex-col tw-gap-2">
<div className={`${isFirstLevel ? outerLabelClass : "tw-text-base"}`}>{val.label}</div>
<div className={`${isFirstLevel ? outerLabelClass : "tw-text-sm"}`}>{val.label}</div>
{val.tool === Tools.INPUT && (
<Input
@@ -70,7 +163,8 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
{val.tool === Tools.COLOR_PICKER && (
<ColorPicker
defaultValue={val.value || "#fff"}
// defaultValue={val.value || "#fff"}
value={val.value || "#fff"}
disabledAlpha
arrow={false}
size="middle"
@@ -91,6 +185,13 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
onChange={(value) => handleChange(value, val.onChange)}
/>
)}
{
val.tool === Tools.LAYOUT_MANAGER && (
renderLayoutManager(val)
)
}
</div>
);
}
@@ -116,8 +217,8 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
return (
<div
className={`tw-absolute tw-top-20 tw-right-5 tw-bg-white ${toolbarOpen ? "tw-w-[320px]" : "tw-w-0"
} tw-px-4 tw-p-2 tw-h-[600px] tw-rounded-md tw-z-20 tw-shadow-lg
className={`tw-absolute tw-top-20 tw-right-5 tw-bg-white ${toolbarOpen ? "tw-w-[280px]" : "tw-w-0"
} tw-px-4 tw-p-2 tw-h-[600px] tw-rounded-md tw-z-[1000] tw-shadow-lg
tw-transition-transform tw-duration-75
tw-flex tw-flex-col tw-gap-2 tw-overflow-y-auto`}
>
@@ -125,7 +226,6 @@ const CanvasToolBar = memo(({ isOpen, widgetType, attrs = {} }) => {
{capitalize(`${widgetType || ""}`)}
</h3>
<hr />
<div className="tw-flex tw-flex-col tw-gap-4">{renderWidgets(toolbarAttrs || {})}</div>
</div>
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
import React, { createContext, useContext, useState } from 'react';
const DragWidgetContext = createContext()
export const useDragWidgetContext = () => useContext(DragWidgetContext)
// Provider component to wrap around parts of your app that need drag-and-drop functionality
export const DragWidgetProvider = ({ children }) => {
const [draggedElement, setDraggedElement] = useState(null)
const onDragStart = (element) => {
setDraggedElement(element)
}
const onDragEnd = () => {
setDraggedElement(null)
}
return (
<DragWidgetContext.Provider value={{ draggedElement, onDragStart, onDragEnd }}>
{children}
</DragWidgetContext.Provider>
)
}

View File

@@ -0,0 +1,176 @@
import { memo, useEffect, useRef, useState } from "react"
import { useDragWidgetContext } from "./draggableWidgetContext"
import { useDragContext } from "../../components/draggable/draggableContext"
// FIXME: sometimes even after drag end the showDroppable is visible
/**
* @param {} - widgetRef - the widget ref for your widget
* @param {boolean} - enableDraggable - should the widget be draggable
* @param {string} - dragElementType - the widget type of widget so the droppable knows if the widget can be accepted
* @param {() => void} - onDrop - the widget type of widget so the droppable knows if the widget can be accepted
* @param {string[]} - droppableTags - array of widget that can be dropped on the widget
*
*/
const WidgetDraggable = memo(({ widgetRef, enableDrag=true, dragElementType="widget",
onDragEnter, onDragLeave, onDrop, style={},
droppableTags = ["widget"], ...props }) => {
// const { draggedElement, onDragStart, onDragEnd } = useDragWidgetContext()
const { draggedElement, onDragStart, onDragEnd, overElement, setOverElement } = useDragContext()
// const [dragEnabled, setDragEnabled] = useState(enableDraggable)
const [isDragging, setIsDragging] = useState(false)
const [showDroppable, setShowDroppable] = useState({
show: false,
allow: false
})
const handleDragStart = (e) => {
e.stopPropagation()
setIsDragging(true)
onDragStart(widgetRef?.current || null)
console.log("Drag start: ", widgetRef.current)
// Create custom drag image with full opacity, this will ensure the image isn't taken from part of the canvas
const dragImage = widgetRef?.current.cloneNode(true)
dragImage.style.opacity = '1' // Ensure full opacity
dragImage.style.position = 'absolute'
dragImage.style.top = '-9999px' // Move it out of view
document.body.appendChild(dragImage)
const rect = widgetRef?.current.getBoundingClientRect()
const offsetX = e.clientX - rect.left
const offsetY = e.clientY - rect.top
// Set the custom drag image with correct offset to avoid snapping to the top-left corner
e.dataTransfer.setDragImage(dragImage, offsetX, offsetY)
// Remove the custom drag image after some time to avoid leaving it in the DOM
setTimeout(() => {
document.body.removeChild(dragImage)
}, 0)
}
const handleDragEnter = (e) => {
const dragEleType = draggedElement.getAttribute("data-draggable-type")
// console.log("Drag entering...", overElement === e.currentTarget)
if (draggedElement === widgetRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
setOverElement(e.currentTarget)
let showDrop = {
allow: true,
show: true
}
if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
showDrop = {
allow: true,
show: true
}
} else {
showDrop = {
allow: false,
show: true
}
}
setShowDroppable(showDrop)
if (onDragEnter)
onDragEnter({element: draggedElement, showDrop})
}
const handleDragOver = (e) => {
if (draggedElement === widgetRef.current){
// prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
return
}
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragEleType = draggedElement.getAttribute("data-draggable-type")
if (droppableTags.length === 0 || droppableTags.includes(dragEleType)) {
e.preventDefault() // this is necessary to allow drop to take place
}
}
const handleDropEvent = (e) => {
e.preventDefault()
e.stopPropagation()
console.log("Dropped: ", draggedElement, props.children)
setShowDroppable({
allow: false,
show: false
})
if (onDrop) {
onDrop(e, draggedElement)
}
// if (draggedElement === widgetRef.current){
// // prevent drop on itself, since the widget is invisible when dragging, if dropped on itself, it may consume itself
// return
// }
let currentElement = e.currentTarget
while (currentElement) {
if (currentElement === draggedElement) {
console.log("Dropped into a descendant element, ignoring drop")
return // Exit early to prevent the drop
}
currentElement = currentElement.parentElement // Traverse up to check ancestors
}
}
const handleDragLeave = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowDroppable({
allow: false,
show: false
})
if (onDragLeave)
onDragLeave()
}
}
const handleDragEnd = () => {
onDragEnd()
setIsDragging(false)
}
// TODO: FIXME, currently the draggable div doesn't move with the child, instead only child div moves, simulating childrens movement, add color and check
return (
<div className={`${props.className || ""}`}
onDragOver={handleDragOver}
onDrop={handleDropEvent}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
draggable={enableDrag}
style={{ opacity: isDragging ? 0.3 : 1, ...style}} // hide the initial position when dragging
>
{props.children}
</div>
)
})
export default WidgetDraggable

View File

@@ -1,12 +1,14 @@
import { useEffect, useMemo, useRef } from "react"
import Draggable from "./utils/draggable"
import Draggable from "./utils/draggableDnd"
import { FileImageOutlined, GithubOutlined, GitlabOutlined, LinkOutlined,
AudioOutlined, VideoCameraOutlined,
FileTextOutlined} from "@ant-design/icons"
import DraggableWrapper from "./draggable/draggable"
import { useDragContext } from "./draggable/draggableContext"
export function DraggableWidgetCard({ name, img, url, innerRef}){
export function SidebarWidgetCard({ name, img, url, widgetClass, innerRef}){
const urlIcon = useMemo(() => {
if (url){
@@ -23,26 +25,32 @@ export function DraggableWidgetCard({ name, img, url, innerRef}){
}, [url])
useEffect(() => {
}, [])
return (
<Draggable className="tw-cursor-pointer" id={name}>
<div ref={innerRef} className="tw-select-none tw-h-[240px] tw-w-[280px] tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px] tw-border-[#888] ">
<div className="tw-h-[200px] tw-w-full tw-overflow-hidden">
<img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
</div>
<span className="tw-text-xl">{name}</span>
<div className="tw-flex tw-text-lg tw-justify-between tw-px-4">
// <Draggable className="tw-cursor-pointer" id={name}>
<DraggableWrapper data-container={"sidebar"}
dragElementType={"widget"}
dragWidgetClass={widgetClass}
className="tw-cursor-pointer tw-w-fit tw-h-fit">
<div ref={innerRef} className="tw-select-none tw-h-[200px] tw-w-[230px] tw-flex tw-flex-col
tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px]
tw-border-[#888] ">
<div className="tw-h-[200px] tw-pointer-events-none tw-w-full tw-overflow-hidden">
<img src={img} alt={name} className="tw-object-contain tw-h-full tw-w-full tw-select-none" />
</div>
<span className="tw-text-xl tw-text-center">{name}</span>
<div className="tw-flex tw-text-lg tw-place tw-px-4">
<a href={url} className="tw-text-black" target="_blank" rel="noopener noreferrer">
{urlIcon}
</a>
<a href={url} className="tw-text-black" target="_blank" rel="noopener noreferrer">
{urlIcon}
</a>
</div>
</div>
</div>
</Draggable>
</DraggableWrapper>
// </Draggable>
)
}
@@ -80,7 +88,7 @@ export function DraggableAssetCard({file}){
return (
<Draggable className="tw-cursor-pointer">
<div className="tw-w-full tw-h-[240px] tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
<div className="tw-w-full tw-h-[240px] tw-p-1 tw-flex tw-flex-col tw-rounded-md tw-overflow-hidden
tw-gap-2 tw-text-gray-600 tw-bg-[#ffffff44] tw-border-solid tw-border-[1px] tw-border-[#888] ">
<div className="tw-h-[200px] tw-w-full tw-flex tw-place-content-center tw-p-1 tw-text-3xl tw-overflow-hidden">
{ file.fileType === "image" &&
@@ -105,7 +113,7 @@ export function DraggableAssetCard({file}){
}
</div>
<span className="tw-text-lg">{file.name}</span>
<span className="tw-text-base">{file.name}</span>
</div>
</Draggable>
)

View File

@@ -0,0 +1,49 @@
import { memo, useRef } from "react"
import { useDragContext } from "./draggableContext"
/**
*
* @param {string} - dragElementType - this will set the data-draggable-type which can be accessed on droppable to check if its allowed or not
* @returns
*/
const DraggableWrapper = memo(({dragElementType, dragWidgetClass=null, className, children, ...props}) => {
const { onDragStart, onDragEnd } = useDragContext()
const draggableRef = useRef(null)
/**
*
* @param {DragEvent} event
*/
const handleDragStart = (event) => {
// event.dataTransfer.setData("text/plain", "")
onDragStart(draggableRef?.current, dragWidgetClass)
}
const handleDragEnd = (e) => {
// console.log("Drag end: ", e, e.target.closest('div'))
onDragEnd()
}
return (
<div className={`${className}`}
draggable
data-draggable-type={dragElementType}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
ref={draggableRef}
{...props}
>
{children}
</div>
)
})
export default DraggableWrapper

View File

@@ -0,0 +1,36 @@
import React, { createContext, useContext, useState } from 'react'
import { isSubClassOfWidget } from '../../utils/widget'
// import Widget from '../../canvas/widgets/base'
export const DragContext = createContext()
export const useDragContext = () => useContext(DragContext)
// Provider component to wrap around parts of your app that need drag-and-drop functionality
export const DragProvider = ({ children }) => {
const [draggedElement, setDraggedElement] = useState(null)
const [overElement, setOverElement] = useState(null) // the element the dragged items is over
const [widgetClass, setWidgetClass] = useState(null) // helper to help pass the widget type from sidebar to canvas
const onDragStart = (element, widgetClass=null) => {
setDraggedElement(element)
if (widgetClass && !isSubClassOfWidget(widgetClass))
throw new Error("widgetClass must inherit from the Widget base class")
setWidgetClass(() => widgetClass) // store the class so later it can be passed to the canvas from sidebar
}
const onDragEnd = () => {
setDraggedElement(null)
setWidgetClass(null)
}
return (
<DragContext.Provider value={{ draggedElement, overElement, setOverElement,
widgetClass, onDragStart, onDragEnd }}>
{children}
</DragContext.Provider>
)
}

View File

@@ -0,0 +1,95 @@
import { memo, useState } from "react"
import { useDragContext } from "./draggableContext"
const DroppableWrapper = memo(({onDrop, droppableTags=["widget"], ...props}) => {
const { draggedElement, overElement, setOverElement, widgetClass } = useDragContext()
const [showDroppable, setShowDroppable] = useState({
show: false,
allow: false
})
const handleDragEnter = (e) => {
const dragElementType = draggedElement.getAttribute("data-draggable-type")
// console.log("Current target: ", e.currentTarget)
setOverElement(e.currentTarget)
if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){
setShowDroppable({
allow: true,
show: true
})
}else{
setShowDroppable({
allow: false,
show: true
})
}
}
const handleDragOver = (e) => {
// console.log("Drag over: ", e.dataTransfer.getData("text/plain"), e.dataTransfer)
const dragElementType = draggedElement.getAttribute("data-draggable-type")
if (droppableTags.length === 0 || droppableTags.includes(dragElementType)){
e.preventDefault() // this is necessary to allow drop to take place
}
}
const handleDropEvent = (e) => {
e.stopPropagation()
setShowDroppable({
allow: false,
show: false
})
if(onDrop){
onDrop(e, draggedElement, widgetClass)
}
}
const handleDragLeave = (e) => {
if (!e.currentTarget.contains(e.relatedTarget)) {
setShowDroppable({
allow: false,
show: false
})
}
}
return (
<div className={`${props.className}`}
onDragOver={handleDragOver}
onDrop={handleDropEvent}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
>
{props.children}
{
showDroppable.show &&
<div className={`${showDroppable.allow ? "tw-border-green-600" : "tw-border-red-600"}
tw-absolute tw-top-0 tw-left-0 tw-w-full tw-h-full tw-z-[2]
tw-border-2 tw-border-dashed tw-rounded-lg tw-pointer-events-none
`}>
</div>
}
</div>
)
})
export default DroppableWrapper

View File

@@ -2,6 +2,7 @@ import React from "react"
import {useDraggable} from "@dnd-kit/core"
import { CSS } from "@dnd-kit/utilities"
function Draggable(props) {
const {attributes, listeners, setNodeRef, transform} = useDraggable({
id: props.id,

View File

@@ -1,4 +1,4 @@
// This is only for testing purpose, not really meant to be used
//NOTE: This is only for testing purpose, not really meant to be used
import './polyfills'
import {

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@@ -0,0 +1,41 @@
import Widget from "../../canvas/widgets/base"
import ButtonWidget from "./assets/widgets/button.png"
const TkinterSidebar = [
{
name: "Main window",
img: ButtonWidget,
link: "https://github.com",
widgetClass: Widget
},
{
name: "Top Level",
img: ButtonWidget,
link: "https://github.com",
widgetClass: Widget
},
{
name: "Frame",
img: ButtonWidget,
link: "https://github.com",
widgetClass: Widget
},
{
name: "Button",
img: ButtonWidget,
link: "https://github.com",
widgetClass: Widget
},
{
name: "Input",
img: ButtonWidget,
link: "https://github.com",
widgetClass: Widget
},
]
export default TkinterSidebar

View File

@@ -1,6 +1,11 @@
import { useEffect, useRef, useMemo, useState } from "react";
import { CloseCircleFilled } from "@ant-design/icons";
import { CloseCircleFilled, CrownFilled, GithubFilled, ShareAltOutlined } from "@ant-design/icons";
import KO_FI from "../assets/logo/ko-fi.png"
import Premium from "./utils/premium";
import Share from "./utils/share";
function Sidebar({tabs}){
@@ -44,8 +49,8 @@ function Sidebar({tabs}){
return (
<div className={`tw-relative tw-duration-[0.3s] tw-transition-all
tw-max-w-[400px] tw-flex tw-h-full tw-z-10
${sidebarOpen ? "tw-bg-white tw-min-w-[400px] tw-w-[400px] tw-shadow-lg":
tw-max-w-[350px] tw-flex tw-h-full tw-z-10 tw-shadow-xl
${sidebarOpen ? "tw-bg-white tw-min-w-[350px] tw-w-[350px] tw-shadow-lg":
"tw-bg-primaryBg tw-min-w-[80px] tw-w-[80px]"}
`}
ref={sideBarRef}
@@ -77,6 +82,25 @@ function Sidebar({tabs}){
)
})
}
<div className="tw-flex tw-flex-col tw-place-content-items tw-place-items-center tw-gap-3 tw-mt-auto">
<Premium className="tw-text-2xl tw-bg-purple-700 tw-text-center
tw-w-[35px] tw-h-[35px] tw-rounded-md
tw-cursor-pointer tw-text-white
tw-transition-all
hover:tw-scale-[1.2]">
<CrownFilled />
</Premium>
<Share className="tw-cursor-pointer tw-text-xl">
<ShareAltOutlined />
</Share>
<a href="https://github.com/PaulleDemon/tkbuilder" className="tw-text-2xl tw-cursor-pointer tw-text-black">
<GithubFilled />
</a>
<a href="https://ko-fi.com/artpaul" className="tw-cursor-pointer ">
<img src={KO_FI} alt="ko-fi" className="tw-w-[30px] tw-h-[30px]"/>
</a>
</div>
</div>
<div className="tw-w-full tw-h-full tw-bg-inherit tw-flex tw-flex-col tw-overflow-x-hidden" ref={sideBarExtraRef}>

View File

@@ -0,0 +1,268 @@
import { useState } from "react"
import { Modal } from "antd"
import { CrownFilled } from "@ant-design/icons"
function Premium({ children, className = "" }) {
const [premiumModalOpen, setPremiumModalOpen] = useState(false)
const onClick = () => {
setPremiumModalOpen(true)
}
const onClose = (event) => {
event.stopPropagation()
setPremiumModalOpen(false)
}
// FIXME: the pricing section is not responsive
return (
<div onClick={onClick} className={`${className}`}>
{children}
<Modal
title={<h3 className="tw-text-xl tw-font-medium">Buy Pre-order one Time License</h3>}
style={{ zIndex: 14000, gap: '10px', maxWidth: '80vw', placeItems: "center" }}
onCancel={onClose}
centered
onOk={onClose}
footer={null}
width={'auto'}
open={premiumModalOpen}
>
<div className="tw-mt-5 tw-text-lg tw-max-w-[850px] tw-w-full ">
I am Paul, an open-source dev, funding open-source projects by providing custom works.
If you find this tool useful and want to support its development, consider buying a <b>one time license</b>.
<br />
<br />
By buying pre-order license, you get advance features, priority support, early access, upcoming features, and more.
<a
href="https://github.com/PaulleDemon/tab=readme-ov-file"
target="_blank"
rel="noreferrer noopener"
className="!tw-text-blue-500"
>
more.
</a>
</div>
<section className="tw-mt-1 tw-flex tw-w-full tw-flex-col tw-place-items-center tw-p-[2%] max-lg:tw-p-2" id="pricing">
<h3 className="tw-text-2xl tw-font-medium max-md:tw-text-2xl">Choose your plan</h3>
<div className="tw-mt-10 tw-flex tw-place-content-center tw-gap-8 max-lg:tw-flex-col">
{/* Free Plan */}
<div className="tw-flex tw-w-[380px] tw-border-[1px] tw-border-gray-500 tw-border-solid tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<h3>
<span className="tw-text-5xl tw-font-semibold">$0</span>
</h3>
<p className="tw-mt-3 tw-text-base tw-text-center tw-text-gray-600">
Free to use forever, but for added features and to support open-source development, consider buying a lifetime license.
</p>
<hr />
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to web-based editor</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Commercial use</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Support for PySlide/PyQt</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Preview live</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Save and load files</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Load plugins locally</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Dark theme</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Priority support</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Early access to new features</span>
</li>
</ul>
</div>
{/* Paid Plan */}
<div className="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-2 tw-border-solid
tw-border-blue-500 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<div className="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full">
Limited time offer
</div>
<div className="tw-text-white tw-font-medium tw-p-1 tw-px-3 tw-bg-green-700 tw-rounded-full">
Hobby
</div>
<h3>
<span className="tw-text-5xl tw-font-semibold">
<s className="tw-font-medium tw-text-4xl">$129</s>
<span>$29</span>
</span>
<span className="tw-text-2xl tw-text-gray-600">Forever</span>
</h3>
<p className="tw-mt-3 tw-text-center tw-text-gray-600">
Support open-source development 🚀. Plus, get added benefits.
</p>
<hr />
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to web-based editor</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Preview live</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Save and load files</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Load plugins locally</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Dark theme</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Priority support</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Early access to new features</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Support for PySlide/PyQt</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-x-circle-fill tw-text-red-600 tw-text-base"></i>
<span>Commercial use</span>
</li>
</ul>
<a
href=""
target="_blank"
rel="noreferrer noopener"
className="tw-mt-8 !tw-bg-purple-500 !tw-text-white tw-gap-2 tw-text-lg tw-rounded-md !tw-font-semibold tw-w-full tw-flex tw-place-content-center tw-p-2 tw-mx-2"
>
<span>Buy License</span>
<CrownFilled />
</a>
</div>
{/* Paid Plan */}
<div className="tw-flex tw-w-[380px] tw-flex-col tw-place-items-center tw-gap-2 tw-rounded-lg tw-border-2 tw-border-solid
tw-border-green-600 tw-p-8 tw-shadow-xl max-lg:tw-w-[340px]">
<div className="tw-text-white tw-p-1 tw-px-3 tw-bg-blue-500 tw-rounded-full">
Limited time offer
</div>
<div className="tw-text-white tw-font-medium tw-p-1 tw-px-3 tw-bg-green-700 tw-rounded-full">
Commercial
</div>
<h3>
<span className="tw-text-5xl tw-font-semibold">
<s className="tw-font-medium tw-text-4xl">$180</s>
<span>$49</span>
</span>
<span className="tw-text-2xl tw-text-gray-600">Forever</span>
</h3>
<p className="tw-mt-3 tw-text-center tw-text-gray-600">
Support open-source development 🚀. Plus, get added benefits.
</p>
<hr />
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-gray-600">
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Access to web-based editor</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Downloadable UI builder exe for local development</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Preview live</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Save and load files</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Load plugins locally</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Dark theme</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Priority support</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Early access to new features</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Support for PySlide/PyQt</span>
</li>
<li className="tw-flex tw-place-items-center tw-gap-2">
<i className="bi bi-check-circle-fill tw-text-green-600 tw-text-base"></i>
<span>Commercial use</span>
</li>
</ul>
<a
href=""
target="_blank"
rel="noreferrer noopener"
className="tw-mt-8 !tw-bg-purple-500 !tw-text-white tw-gap-2 tw-text-lg tw-rounded-md !tw-font-semibold tw-w-full tw-flex tw-place-content-center tw-p-2 tw-mx-2"
>
<span>Buy License</span>
<CrownFilled />
</a>
</div>
</div>
</section>
</Modal>
</div>
)
}
export default Premium

110
src/sidebar/utils/share.js Normal file
View File

@@ -0,0 +1,110 @@
import { useMemo, useState } from "react"
import { Modal, message } from "antd"
import { CopyOutlined, FacebookFilled, LinkedinFilled, MediumCircleFilled, RedditCircleFilled, TwitchFilled, TwitterCircleFilled } from "@ant-design/icons"
function Share({children, className=""}){
const [shareModalOpen, setShareModalOpen] = useState(false)
const shareInfo = useMemo(() => {
return {
url: encodeURI("https://github.com/PaulleDemon/font-tester-chrome"),
text: "Check out Framework agnostic GUI builder for python"
}
}, [])
const onClick = () => {
setShareModalOpen(true)
}
const onClose = (event) => {
event.stopPropagation()
setShareModalOpen(false)
}
const onCopy = (event) => {
event.stopPropagation()
navigator.clipboard.writeText(`Check out Font tester: ${shareInfo.url}`).then(function() {
message.success("Link copied to clipboard")
}, function(err) {
message.error("Error copying to clipboard")
})
}
return (
<div onClick={onClick} className={className}>
{children}
<Modal title={<h3 className="tw-text-xl tw-font-medium">Share PyUI Builder with others</h3>}
styles={{wrapper: {zIndex: 14000, gap: "10px"}}}
onCancel={onClose}
onOk={onClose}
footer={null}
open={shareModalOpen}>
<div className="tw-mt-5 tw-flex tw-place-content-center tw-w-full tw-place-items-center">
<a onClick={onCopy}
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"
style={{width: "80px", height: "80px", outline: "none", border: "none", color: "#000",
backgroundColor: "transparent", display: "flex", justifyContent: "center",
padding: "0.5rem 0.75rem", borderRadius: "0.375rem"}}>
<CopyOutlined />
</a>
<a href={`https://www.reddit.com/submit?url=${shareInfo.url}&title=${encodeURIComponent(shareInfo.text)}`}
target="_blank" rel="noopener noreferrer"
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"
style={{width: "80px", height: "80px", outline: "none", border: "none", color: "#000",
backgroundColor: "transparent", display: "flex", justifyContent: "center",
padding: "0.5rem 0.75rem", borderRadius: "0.375rem"}}>
<RedditCircleFilled />
</a>
<a href={`https://www.linkedin.com/shareArticle?mini=true&url=${shareInfo.url}&title=${encodeURIComponent(shareInfo.text)}`}
target="_blank" rel="noopener noreferrer"
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"
style={{width: "80px", height: "80px", outline: "none", border: "none", color: "#000",
backgroundColor: "transparent", display: "flex", justifyContent: "center",
padding: "0.5rem 0.75rem", borderRadius: "0.375rem"}}>
<LinkedinFilled />
</a>
<a href={`https://www.facebook.com/sharer/sharer.php?u=${shareInfo.url}`}
target="_blank" rel="noopener noreferrer"
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"
style={{width: "80px", height: "80px", outline: "none", border: "none", color: "#000",
backgroundColor: "transparent", display: "flex", justifyContent: "center",
padding: "0.5rem 0.75rem", borderRadius: "0.375rem"}}>
<FacebookFilled />
</a>
<a href="https://medium.com/new-story"
target="_blank" rel="noopener noreferrer"
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"
style={{width: "80px", height: "80px", outline: "none", border: "none", color: "#000",
backgroundColor: "transparent", display: "flex", justifyContent: "center",
padding: "0.5rem 0.75rem", borderRadius: "0.375rem"}}>
<MediumCircleFilled />
</a>
<a href={`https://twitter.com/share?url=${shareInfo.url}&text=${encodeURIComponent(shareInfo.text)}`}
target="_blank" rel="noopener noreferrer"
className="hover:!tw-bg-gray-100 hover:!tw-color-black !tw-text-4xl"
style={{width: "80px", height: "80px", outline: "none", border: "none", color: "#000",
backgroundColor: "transparent", display: "flex", justifyContent: "center",
padding: "0.5rem 0.75rem", borderRadius: "0.375rem"}}>
<TwitterCircleFilled />
</a>
</div>
</Modal>
</div>
)
}
export default Share

View File

@@ -2,11 +2,12 @@ import { useEffect, useMemo, useState } from "react"
import { CloseCircleFilled, SearchOutlined } from "@ant-design/icons"
import {DraggableWidgetCard} from "../components/cards"
import {SidebarWidgetCard} from "../components/cards"
import ButtonWidget from "../assets/widgets/button.png"
import { filterObjectListStartingWith } from "../utils/filter"
import Widget from "../canvas/widgets/base"
/**
@@ -14,55 +15,30 @@ import { filterObjectListStartingWith } from "../utils/filter"
* @param {function} onWidgetsUpdate - this is a callback that will be called once the sidebar is populated with widgets
* @returns
*/
function WidgetsContainer({onWidgetsUpdate}){
function WidgetsContainer({sidebarContent, onWidgetsUpdate}){
const widgets = useMemo(() => {
return [
{
name: "TopLevel",
img: ButtonWidget,
link: "https://github.com"
},
{
name: "Frame",
img: ButtonWidget,
link: "https://github.com"
},
{
name: "Button",
img: ButtonWidget,
link: "https://github.com"
},
{
name: "Input",
img: ButtonWidget,
link: "https://github.com"
},
]
}, [])
const [searchValue, setSearchValue] = useState("")
const [widgetData, setWidgetData] = useState(widgets)
const [widgetData, setWidgetData] = useState(sidebarContent)
useEffect(() => {
setWidgetData(widgets)
setWidgetData(sidebarContent)
// if (onWidgetsUpdate){
// onWidgetsUpdate(widgets)
// }
if (onWidgetsUpdate){
onWidgetsUpdate(widgets)
}
}, [widgets])
}, [sidebarContent])
useEffect(() => {
if (searchValue.length > 0){
const searchData = filterObjectListStartingWith(widgets, "name", searchValue)
const searchData = filterObjectListStartingWith(sidebarContent, "name", searchValue)
setWidgetData(searchData)
}else{
setWidgetData(widgets)
setWidgetData(sidebarContent)
}
}, [searchValue])
@@ -94,10 +70,11 @@ function WidgetsContainer({onWidgetsUpdate}){
{
widgetData.map((widget, index) => {
return (
<DraggableWidgetCard key={widget.name}
<SidebarWidgetCard key={widget.name}
name={widget.name}
img={widget.img}
url={widget.link}
widgetClass={widget.widgetClass}
/>
)

View File

@@ -17,6 +17,16 @@ body {
background-size: cover;
}
.stripes-bg {
width: 100%;
height: 100%;
--color: #E1E1E1;
background-color: #F3F3F3;
background-image: linear-gradient(0deg, transparent 24%, var(--color) 25%, var(--color) 26%, transparent 27%,transparent 74%, var(--color) 75%, var(--color) 76%, transparent 77%,transparent),
linear-gradient(90deg, transparent 24%, var(--color) 25%, var(--color) 26%, transparent 27%,transparent 74%, var(--color) 75%, var(--color) 76%, transparent 77%,transparent);
background-size: 55px 55px;
}
.input{
border: 2px solid #e3e5e8;
padding: 2px 8px;

16
src/utils/widget.js Normal file
View File

@@ -0,0 +1,16 @@
import Widget from "../canvas/widgets/base"
// checks if the object is instance is instance of widget class
export const isSubClassOfWidget = (_class) => {
return Widget.isPrototypeOf(_class) || _class === Widget
}
export const isInstanceOfWidget = (_class) => {
console.log("Widget is instance of Object: ", _class)
return _class instanceof Widget
}