18 Commits

Author SHA1 Message Date
paul
fff10097fd updated after time 2025-04-01 20:47:25 +05:30
paul
d10928bb5a fixed padx/y and ipadx/y 2025-04-01 20:42:14 +05:30
paul
0243b3b9e8 added license comment 2025-04-01 19:00:20 +05:30
paul
66d7dfc45f added webpack and fixed ImageLabel 2025-04-01 18:50:14 +05:30
paul
a38cd90c16 working on label image resize fix 2025-03-31 20:38:25 +05:30
paul
bdd3bab3a5 corrected grid weight and grid config position in toolbar 2025-03-30 05:46:03 +05:30
paul
53aaa8a670 fix: fixed grid row col on load 2025-03-30 05:23:18 +05:30
paul
64631caaaa fix: fixed setState warning 2025-03-29 21:03:02 +05:30
paul
8e1f042350 fix: fixed parentLayout taking place on changing layout 2025-03-29 21:00:45 +05:30
paul
128a7c49b9 updated readme 2025-03-28 18:48:41 +05:30
paul
e0fa421459 Merge branch 'customtk-fixes' 2025-03-28 17:14:51 +05:30
paul
0438480620 fixes: corrected tk to ctk in customtk 2025-03-28 17:14:03 +05:30
Art/Paul
d64f87847c Merge pull request #12 from PaulleDemon/customtk-fixes
fixing customtk layout
2025-03-28 17:13:21 +05:30
paul
c6fd4a8275 fixing customtk layout 2025-03-28 15:56:17 +05:30
paul
bfeb1c55b9 added discord invite 2025-03-28 11:17:48 +05:30
paul
9949fb8335 updated manifest 2025-03-27 18:43:56 +05:30
paul
86f84ef998 updated premium message 2025-03-27 17:18:30 +05:30
paul
fc68d407b7 added discord invite 2025-03-27 15:31:27 +05:30
28 changed files with 6325 additions and 2126 deletions

1
.gitignore vendored
View File

@@ -7,6 +7,7 @@
.env-cmdrc.json
build
dist
python-tests/

View File

@@ -30,7 +30,7 @@ https://github.com/user-attachments/assets/ac91aa98-843d-4578-b646-88e66bc113de
<sub>**Don't like background music? fell free to mute it**</sub>
## Try PyUIBuilder
Try [PyUIBuilder](https://pyuibuilder.pages.dev/)
Try [PyUIBuilder](https://pyuibuilder.com)
## Table of contents
@@ -53,7 +53,7 @@ Try [PyUIBuilder](https://pyuibuilder.pages.dev/)
## Docs - Getting started
Read the docs on the [Docs page](https://pyuibuilder-docs.pages.dev/)
Read the docs on the [Docs page](https://docs.pyuibuilder.com/)
## Example app
@@ -109,7 +109,7 @@ While there are a lot of features, here are few you need to know.
* Framework agnostic - Can outputs code in multiple frameworks.
* Pre-built UI widgets
* Plugins to extend 3rd party UI libraries
* Supports layout managers, such as flex, grid and absolute positioning [read docs](https://pyuibuilder-docs.pages.dev/)
* Supports layout managers, such as flex, grid and absolute positioning [read docs](https://docs.pyuibuilder.com/)
* Generates python Code.
* Support to upload local assets.
* Generates requirements.txt file when needed

View File

@@ -249,7 +249,7 @@
</ul>
<a
href="https://pyuibuilder.pages.dev/"
href="https://pyuibuilder.com"
target="_blank"
rel="noreferrer noopener"
class=" !tw-bg-[#0F1727] tw-duration-[0.3s] hover:tw-transition-transform hover:tw-scale-[1.01] !tw-mt-auto !tw-text-white tw-gap-2 tw-text-lg tw-rounded-md tw-w-full tw-flex tw-place-content-center tw-p-2 tw-mx-2"

6532
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,8 +35,8 @@
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "GENERATE_SOURCEMAP=false react-scripts build",
"start": "env-cmd -e development cross-env NODE_ENV=development webpack serve",
"build": "env-cmd -e production cross-env NODE_ENV=production webpack",
"build:local": "GENERATE_SOURCEMAP=false env-cmd -e production react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
@@ -63,8 +63,28 @@
]
},
"devDependencies": {
"@babel/core": "^7.26.10",
"@babel/preset-env": "^7.26.9",
"@babel/preset-react": "^7.26.3",
"@svgr/webpack": "^8.1.0",
"ajv": "^7.2.4",
"babel-loader": "^10.0.0",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2",
"css-minimizer-webpack-plugin": "^7.0.2",
"docsify-cli": "^4.4.4",
"typescript": "^4.9.5"
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.3",
"mini-css-extract-plugin": "^2.9.2",
"raw-loader": "^4.0.2",
"react-app-rewired": "^2.2.1",
"sass-loader": "^16.0.5",
"style-loader": "^4.0.0",
"terser-webpack-plugin": "^5.3.14",
"typescript": "^4.9.5",
"webpack": "^5.98.0",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.1"
}
}

View File

@@ -2,31 +2,31 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" href="favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="A python GUI builder. Create tkinter, Customtk, Kivy and PySide using GUI builder"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="apple-touch-icon" href="logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
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="manifest" href="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" />
<script async src="https://tally.so/widgets/embed.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=%REACT_APP_ANALYTICS_SCRIPT_ID%"></script>
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= REACT_APP_ANALYTICS_SCRIPT_ID %>"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '%REACT_APP_ANALYTICS_SCRIPT_ID%');
gtag('config', '<%= REACT_APP_ANALYTICS_SCRIPT_ID %>');
</script>
<!--
Notice the use of %PUBLIC_URL% in the tags above.

View File

@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "PyUiBuilder",
"name": "PyUiBuilder - Python GUI Builder",
"icons": [
{
"src": "favicon.ico",

View File

@@ -61,21 +61,23 @@ function App() {
useEffect(() => {
if (shownNotChromiumAlert){
return
if (!shownNotChromiumAlert){
// this modal may rerender twice only in dev mode because of how react works
isChromium().then((isChrome) => {
if (!isChrome){
Modal.warning({
title: "Use Chromium browser",
onOk: () => setShownNotChromiumAlert(true),
content: (<span>We recommend using Chromium based browser such as Chrome, Brave, Edge etc for best results.
<br />
Join us on
<a href="https://discord.gg/dHXjrrCA7G" target='_blank' rel='noreferrer noopener'> Discord</a> for help and updates</span>)
})
}
})
}
isChromium().then((isChrome) => {
if (!isChrome){
Modal.warning({
title: "Not Chromium browser",
content: "We recommend using Chromium based browser such as Chrome, Brave, Edge etc."
})
setShownNotChromiumAlert(true)
}
})
setShownNotChromiumAlert(true)
}, [shownNotChromiumAlert])
@@ -123,7 +125,6 @@ function App() {
const widgetCenterX = (TkMainWindow.initialSize.width - canvasBoundingBox.left) / 2
const widgetCenterY = (TkMainWindow.initialSize.height - canvasBoundingBox.top) / 2
canvasRef?.current?.createWidget(TkMainWindow, {x: canvasCenterX - widgetCenterX, y: canvasCenterY - widgetCenterY}, ({id, widgetRef}) => {
// center the widget when adding to canvas
@@ -140,7 +141,12 @@ function App() {
})
}else if (UIFramework === FrameWorks.CUSTOMTK){
canvasRef?.current?.createWidget(CTkMainWindow, ({id, widgetRef}) => {
const widgetCenterX = (TkMainWindow.initialSize.width - canvasBoundingBox.left) / 2
const widgetCenterY = (TkMainWindow.initialSize.height - canvasBoundingBox.top) / 2
canvasRef?.current?.createWidget(CTkMainWindow, {x: canvasCenterX - widgetCenterX, y: canvasCenterY - widgetCenterY}, ({id, widgetRef}) => {
// center the widget when adding to canvas
if (!widgetRef.current){
@@ -152,7 +158,7 @@ function App() {
const widgetCenterY = (widgetBoundingBox.height - widgetBoundingBox.top) / 2
widgetRef.current?.setPos(canvasCenterX-widgetCenterX, canvasCenterY-widgetCenterY)
// widgetRef.current?.setPos(canvasCenterX-widgetCenterX, canvasCenterY-widgetCenterY)
})
}

View File

@@ -19,7 +19,8 @@ import { removeDuplicateObjects } from "../utils/common"
// import DotsBackground from "../assets/background/dots.svg"
import { ReactComponent as DotsBackground } from "../assets/background/dots.svg"
// import { ReactComponent as DotsBackground } from "../assets/background/dots.svg"
import DotsBackground from "../assets/background/dots.svg";
import DroppableWrapper from "../components/draggable/droppable"

View File

@@ -41,6 +41,8 @@ class Widget extends React.Component {
static requirements = [] // requirements for the widgets (libraries) eg: tkvideoplayer, tktimepicker
static requiredImports = [] // import statements
static requiredCustomPyFiles = [] // custom widgets inside of pythonWidgets don't add .py at the end
// static contextType = ActiveWidgetContext
constructor(props) {
@@ -185,6 +187,7 @@ class Widget extends React.Component {
this.getImports = this.getImports.bind(this)
this.getRequirements = this.getRequirements.bind(this)
this.getRequiredCustomPyFiles = this.getRequiredCustomPyFiles.bind(this)
// this.openRenaming = this.openRenaming.bind(this)
@@ -215,7 +218,6 @@ class Widget extends React.Component {
this.stateUpdateCallback = null // allowing other components such as toolbar to subscribe to changes in this widget
this.resizeObserver = null
}
componentDidMount() {
@@ -427,6 +429,10 @@ class Widget extends React.Component {
return this.constructor.requiredImports
}
getRequiredCustomPyFiles(){
return this.constructor.requiredCustomPyFiles
}
generateCode(){
throw new NotImplementedError("generateCode() must be implemented by the subclass")
}
@@ -704,8 +710,7 @@ class Widget extends React.Component {
}
getLayout(){
return this.getAttrValue("layout") || Layouts.PLACE
return this.getAttrValue("layout") || {layout: Layouts.PLACE}
}
setLayout(value) {
@@ -918,6 +923,8 @@ class Widget extends React.Component {
pos
}
const {layout} = attrs
this.setState(newData, () => {
// Updates attrs
let newAttrs = { ...this.state.attrs }
@@ -949,6 +956,10 @@ class Widget extends React.Component {
if (selected){
this.select()
}
if (layout){
this.setLayout(layout)
}
})

View File

@@ -2,6 +2,7 @@
export const Tkinter_TO_WEB_CURSOR_MAPPING = {
"": "",
"arrow": "default",
"circle": "wait",
"clock": "wait",

View File

@@ -20,5 +20,25 @@ export const ANCHOR = [
"s",
"e",
"w",
"center"
]
"center",
"ne",
"se",
"sw",
"nw",
]
export const GRID_STICKY = {
N: "n",
S: "s",
E: "e",
W: "w",
WE: "we",
NS: "ns",
NW: "nw",
NE: "ne",
SW: "sw",
SE: "se",
NEWS: "news",
NONE: "",
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,5 @@
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import Widget from "../../../canvas/widgets/base"
import { CustomTkBase } from "./base"
@@ -17,7 +19,99 @@ class Frame extends CustomTkBase{
this.state = {
...this.state,
fitContent: {width: true, height: true},
widgetName: "Frame"
widgetName: "Frame",
attrs: {
...this.state.attrs,
padding: {
label: "padding",
padX: {
label: "Pad X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
// this.setWidgetInnerStyle("paddingLeft", `${value}px`)
// this.setWidgetInnerStyle("paddingRight", `${value}px`)
// const widgetStyle = {
// }
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingLeft: `${value}px`,
paddingRight: `${value}px`
}
}))
this.setAttrValue("padding.padX", value)
}
},
padY: {
label: "Pad Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.setState((prevState) => ({
widgetInnerStyling: {
...prevState.widgetInnerStyling,
paddingTop: `${value}px`,
paddingBottom: `${value}px`
}
}))
// this.setState({
// widgetInnerStyling: widgetStyle
// })
this.setAttrValue("padding.padY", value)
}
},
},
margin: {
label: "Margin",
marginX: {
label: "Margin X",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginLeft: `${value}px`,
marginRight: `${value}px`
},
}))
this.setAttrValue("margin.marginX", value)
}
},
marginY: {
label: "Margin Y",
tool: Tools.NUMBER_INPUT,
toolProps: {min: 0, max: 140},
value: null,
onChange: (value) => {
this.updateState((prev) => ({
widgetOuterStyling: {
...prev.widgetOuterStyling,
marginTop: `${value}px`,
marginBottom: `${value}px`
},
}))
this.setAttrValue("margin.marginY", value)
}
},
},
}
}
}
@@ -27,6 +121,34 @@ class Frame extends CustomTkBase{
this.setAttrValue("styling.backgroundColor", "#EDECEC")
}
getConfigCode(){
const bg = this.getAttrValue("styling.backgroundColor")
const fitWidth = this.state.fitContent.width
const fitHeight = this.state.fitContent.height
const {width, height} = this.getSize()
const {layout} = this.getParentLayout()
const config = {
bg: `"${bg}"`
}
if (layout !== Layouts.PLACE){
if (!fitWidth){
config['width'] = width
}
if (!fitHeight){
config['height'] = height
}
}
return config
}
generateCode(variableName, parent){
const bg = this.getAttrValue("styling.backgroundColor")
@@ -34,12 +156,26 @@ class Frame extends CustomTkBase{
return [
`${variableName} = ctk.CTkFrame(master=${parent})`,
`${variableName}.configure(fg_color="${bg}")`,
`${variableName}.${this.getLayoutCode()}`
`${variableName}.${this.getLayoutCode()}`,
...this.getGridLayoutConfigurationCode(variableName)
]
}
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
padding: this.state.attrs.padding,
margin: this.state.attrs.margin,
layout,
gridConfig,
gridWeights
})
}
renderContent(){
// console.log("bounding rect: ", this.getBoundingRect())
@@ -49,7 +185,7 @@ class Frame extends CustomTkBase{
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
{this.props.children}
{this.renderTkinterLayout()}
</div>
</div>
)

View File

@@ -116,6 +116,22 @@ class Label extends CustomTkWidgetBase{
})
}
getAnchorStyle = (anchor) => {
const anchorStyles = {
n: { justifyContent: 'center', alignItems: 'flex-start' },
s: { justifyContent: 'center', alignItems: 'flex-end' },
e: { justifyContent: 'flex-end', alignItems: 'center' },
w: { justifyContent: 'flex-start', alignItems: 'center' },
ne: { justifyContent: 'flex-end', alignItems: 'flex-start' },
se: { justifyContent: 'flex-end', alignItems: 'flex-end' },
nw: { justifyContent: 'flex-start', alignItems: 'flex-start' },
sw: { justifyContent: 'flex-start', alignItems: 'flex-end' },
center: { justifyContent: 'center', alignItems: 'center' }
}
return anchorStyles[anchor] || anchorStyles["w"];
}
renderContent(){
const image = this.getAttrValue("imageUpload")
@@ -137,7 +153,10 @@ class Label extends CustomTkWidgetBase{
<img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" />
)
}
<div className="" style={{color: this.getAttrValue("styling.foregroundColor")}}>
<div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")}
</div>
</div>

View File

@@ -1,6 +1,7 @@
import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools"
import { CustomTkBase } from "./base"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
class MainWindow extends CustomTkBase{
@@ -8,6 +9,13 @@ class MainWindow extends CustomTkBase{
static widgetType = "main_window"
static displayName = "Main Window"
static initialSize = {
width: 700,
height: 400
}
constructor(props) {
super(props)
@@ -27,6 +35,13 @@ class MainWindow extends CustomTkBase{
toolProps: {placeholder: "Window title", maxLength: 40},
value: "Main Window",
onChange: (value) => this.setAttrValue("title", value)
},
logo: {
label: "Window Logo",
tool: Tools.UPLOADED_LIST,
toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
value: "",
onChange: (value) => this.setAttrValue("logo", value)
}
}
@@ -43,12 +58,48 @@ class MainWindow extends CustomTkBase{
generateCode(variableName, parent){
const backgroundColor = this.getAttrValue("styling.backgroundColor")
const logo = this.getAttrValue("logo")
return [
`${variableName} = ctk.CTk()`,
`${variableName}.configure(fg_color="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`
]
const {width, height} = this.getSize()
const code = [
`${variableName} = ctk.CTk()`,
`${variableName}.configure(fg_color="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`,
`${variableName}.geometry("${width}x${height}")`,
...this.getGridLayoutConfigurationCode(variableName)
]
if (logo?.name){
// code.push(`\n`)
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
// code.push("\n")
}
return code
}
getImports(){
const imports = super.getImports()
if (this.getAttrValue("logo"))
imports.push("import os", "from PIL import Image, ImageTk", )
return imports
}
getRequirements(){
const requirements = super.getRequirements()
if (this.getAttrValue("logo"))
requirements.push("pillow")
return requirements
}
getToolbarAttrs(){
@@ -58,8 +109,8 @@ class MainWindow extends CustomTkBase{
id: this.__id,
widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title,
logo: this.state.attrs.logo,
size: toolBarAttrs.size,
...this.state.attrs,
})
@@ -83,7 +134,7 @@ class MainWindow extends CustomTkBase{
<div className="tw-p-2 tw-w-full tw-relative tw-h-full tw-overflow-hidden tw-content-start"
ref={this.styleAreaRef}
style={this.state.widgetInnerStyling}>
{this.props.children}
{this.renderTkinterLayout()}
</div>
</div>
)

View File

@@ -1,8 +1,10 @@
import Widget from "../../../canvas/widgets/base"
import Tools from "../../../canvas/constants/tools"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
import { CustomTkBase } from "./base"
class TopLevel extends Widget{
class TopLevel extends CustomTkBase{
static widgetType = "toplevel"
static displayName = "Top Level"
@@ -27,6 +29,13 @@ class TopLevel extends Widget{
toolProps: {placeholder: "Window title", maxLength: 40},
value: "Top level",
onChange: (value) => this.setAttrValue("title", value)
},
logo: {
label: "Toplevel Logo",
tool: Tools.UPLOADED_LIST,
toolProps: {filterOptions: ["image/jpg", "image/jpeg", "image/png"]},
value: "",
onChange: (value) => this.setAttrValue("logo", value)
}
}
@@ -42,11 +51,49 @@ class TopLevel extends Widget{
const backgroundColor = this.getAttrValue("styling.backgroundColor")
return [
`${variableName} = ctk.CTkToplevel(master=${parent})`,
`${variableName}.configure(fg_color="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`
]
const logo = this.getAttrValue("logo")
const {width, height} = this.getSize()
const code = [
`${variableName} = ctk.CTkToplevel(master=${parent})`,
`${variableName}.configure(fg_color="${backgroundColor}")`,
`${variableName}.title("${this.getAttrValue("title")}")`,
`${variableName}.geometry("${width}x${height}")`,
...this.getGridLayoutConfigurationCode(variableName)
]
if (logo?.name){
// code.push(`\n`)
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(logo.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
code.push(`${variableName}.iconphoto(False, ${variableName}_img)`)
// code.push("\n")
}
return code
}
getImports(){
const imports = super.getImports()
if (this.getAttrValue("logo"))
imports.push("import os", "from PIL import Image, ImageTk", )
return imports
}
getRequirements(){
const requirements = super.getRequirements()
if (this.getAttrValue("logo"))
requirements.push("pillow")
return requirements
}
getToolbarAttrs(){
@@ -55,8 +102,8 @@ class TopLevel extends Widget{
return ({
widgetName: toolBarAttrs.widgetName,
title: this.state.attrs.title,
logo: this.state.attrs.logo,
size: toolBarAttrs.size,
...this.state.attrs,
})
@@ -80,7 +127,7 @@ class TopLevel extends Widget{
<div className="tw-p-2 tw-w-full tw-h-full tw-content-start"
ref={this.styleAreaRef}
style={this.state.widgetInnerStyling}>
{this.props.children}
{this.renderTkinterLayout()}
</div>
</div>
)

View File

@@ -2,6 +2,7 @@
export const Tkinter_TO_WEB_CURSOR_MAPPING = {
"": "",
"arrow": "default",
"circle": "wait",
"clock": "wait",

View File

@@ -4,6 +4,8 @@ import MainWindow from "../widgets/mainWindow"
import { message } from "antd"
import TopLevel from "../widgets/toplevel"
const pythonFiles = require.context("../pythonWidgets", false, /\.py$/)
// FIXME: if the toplevel comes first, before the MainWindow in widgetlist the root may become null
// Recursive function to generate the code list, imports, requirements, and track mainVariable
@@ -13,6 +15,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
let requirements = new Set([])
let code = []
let customPythonWidgets = new Set([])
for (let widget of widgetList) {
const widgetRef = widgetRefs[widget.id].current
let varName = widgetRef.getVariableName()
@@ -20,6 +24,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Add imports and requirements to sets
widgetRef.getImports().forEach(importItem => imports.add(importItem))
widgetRef.getRequirements().forEach(requirementItem => requirements.add(requirementItem))
widgetRef.getRequiredCustomPyFiles().forEach(customFile => customPythonWidgets.add(customFile))
// Set main variable if the widget is MainWindow
if (widget.widgetType === MainWindow) {
@@ -68,6 +73,8 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
// Merge child imports, requirements, and code
imports = new Set([...imports, ...childResult.imports])
requirements = new Set([...requirements, ...childResult.requirements])
customPythonWidgets = new Set([...customPythonWidgets, ...childResult.customPythonWidgets])
code.push(...childResult.code)
mainVariable = childResult.mainVariable || mainVariable // the main variable is the main window variable
@@ -78,6 +85,7 @@ function generateTkinterCodeList(widgetList = [], widgetRefs = [], parentVariabl
imports: Array.from(imports),
code: code,
requirements: Array.from(requirements),
customPythonWidgets: Array.from(customPythonWidgets),
mainVariable
}
}
@@ -113,9 +121,13 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
const generatedObject = generateTkinterCodeList(filteredWidgetList, widgetRefs.current, "", "")
const {code: codeLines, imports, requirements, mainVariable} = generatedObject
const {code: codeLines, imports, requirements, mainVariable, customPythonWidgets} = generatedObject
console.log("custom python widgets: ", customPythonWidgets)
// TODO: avoid adding \n inside the list instead rewrite using code.join("\n")
// TODO: import customWidgets
const code = [
"# This code is generated by PyUIbuilder: https://pyuibuilder.com",
"\n\n",
@@ -147,6 +159,32 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
})
}
for (let customWidget of customPythonWidgets){
let [fileName, extension] = customWidget.split(".")
if (!extension){
fileName = `${fileName}.py`
}
const fileContent = pythonFiles(`./${fileName}`).default
createFileList.push({
fileData: new Blob([fileContent], { type: "text/plain" }),
fileName: fileName,
folder: "customWidgets"
})
}
if (customPythonWidgets.length > 0){
createFileList.push({
fileData: new Blob([''], { type: "text/plain" }),
fileName: '__init__.py',
folder: "customWidgets"
})
}
for (let asset of assetFiles){
if (asset.fileType === "image"){

View File

@@ -0,0 +1,92 @@
# Author: Paul: https://github.com/PaulleDemon
# Made using PyUibuilder: https://pyuibuilder.com
# MIT License - keep the copy of this license
# By default Label grows to fit the image, which isn't ideal for many use cases (image should grow to fit/cover the image instead)
import tkinter as tk
from PIL import Image, ImageTk
class ImageLabel(tk.Label):
def __init__(self, master, image_path=None, mode="cover", width=100, height=100, *args, **kwargs):
"""
mode:
- "fit" -> Keeps aspect ratio, fits inside label
- "cover" -> Covers label fully, cropping excess
"""
super().__init__(master, width=width, height=height, *args, **kwargs)
self.parent = master
self.image_path = image_path
self.mode = mode
self.original_image = None
self.photo = None
self.resize_job = None # Debounce job reference
if mode not in ['fit', 'cover']:
raise Exception("Mode can only be fit or cover.")
if image_path:
try:
self.original_image = Image.open(image_path)
self.photo = ImageTk.PhotoImage(self.original_image)
self.config(image=self.photo)
self.force_resize()
except Exception as e:
print(f"Error loading image: {e}")
self.after(100, self.init_events)
def init_events(self):
self.parent.bind("<Configure>", self.on_resize)
def on_resize(self, event=None):
"""Debounce resizing to prevent rapid execution."""
if self.resize_job:
self.after_cancel(self.resize_job)
self.resize_job = self.after(1, self.force_resize) # Debounce
def force_resize(self):
"""Resize image using actual widget size."""
if self.original_image is None:
return # Do nothing if no image is loaded
width = self.winfo_width()
height = self.winfo_height()
if width < 5 or height < 5:
return
aspect_ratio = self.original_image.width / self.original_image.height
if self.mode == "fit":
if width / height > aspect_ratio:
new_width = int(height * aspect_ratio)
new_height = height
else:
new_width = width
new_height = int(width / aspect_ratio)
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
elif self.mode == "cover":
if width / height > aspect_ratio:
new_width = width
new_height = int(width / aspect_ratio)
else:
new_width = int(height * aspect_ratio)
new_height = height
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
# Crop excess
left = (new_width - width) // 2
top = (new_height - height) // 2
right = left + width
bottom = top + height
resized = resized.crop((left, top, right, bottom))
# Update image
self.photo = ImageTk.PhotoImage(resized)
self.config(image=self.photo)

View File

@@ -0,0 +1 @@
# contains python coded custom widgets

View File

@@ -59,13 +59,34 @@ export class TkinterBase extends Widget {
const absolutePositioning = this.getAttrValue("positioning")
let layoutManager = `pack()`
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
let config = {}
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
if (parentLayout === Layouts.PLACE || absolutePositioning){
const config = {
x: Math.trunc(this.state.pos.x),
y: Math.trunc(this.state.pos.y),
}
config['x'] = Math.trunc(this.state.pos.x)
config['y'] = Math.trunc(this.state.pos.y)
config["width"] = Math.trunc(this.state.size.width)
config["height"] = Math.trunc(this.state.size.height)
@@ -85,14 +106,6 @@ export class TkinterBase extends Widget {
const packSide = this.getAttrValue("flexManager.side")
const marginX = this.getAttrValue("margin.marginX")
const marginY = this.getAttrValue("margin.marginY")
const paddingX = this.getAttrValue("padding.padX")
const paddingY = this.getAttrValue("padding.padY")
const config = {}
if (packSide === "" || packSide === "top"){
config['side'] = `tk.TOP`
@@ -114,21 +127,6 @@ export class TkinterBase extends Widget {
// config["pady"] = gap
// }
if (marginX){
config["padx"] = marginX
}
if (marginY){
config["pady"] = marginY
}
if (paddingX){
config["ipadx"] = paddingX
}
if (paddingY){
config["ipady"] = paddingY
}
// if (align === "start"){
// config["anchor"] = "'nw'"
@@ -168,10 +166,8 @@ export class TkinterBase extends Widget {
const sticky = this.getAttrValue("gridManager.sticky")
const config = {
row: row-1, // unlike css grid tkinter grid starts from 0
column: column-1, // unlike css grid tkinter grid starts from 0
}
config['row'] = row - 1 // unlike css grid tkinter grid starts from 0
config['column'] = column - 1 // unlike css grid tkinter grid starts from 0
if (rowSpan > 1){
config['rowspan'] = rowSpan
@@ -561,7 +557,8 @@ export class TkinterBase extends Widget {
const { side = "top", expand = false, anchor } = widgetRef.getPackAttrs() || {};
// console.log("rerendering:", side, expand);
const directionMap = {
top: "column",
bottom: "column-reverse",
@@ -570,17 +567,29 @@ export class TkinterBase extends Widget {
}
const currentWidgetDirection = directionMap[side] || "column"; // Default to "column"
const isSameSide = lastSide === side;
const isVertical = ["top", "bottom"].includes(side);
let expandValue = expand ? (isSameSide ? previousExpandValue : widgets.length - index) : 1;
if ((expand && previousExpandValue === 0) || (expand && expandValue === 0)){
const isSameSide = lastSide === side
const isOppositeSide = ((lastSide === "top" && side === "bottom") || (lastSide === "left" && side === "right"))
const isDiagonal = (!isSameSide && !isOppositeSide && lastSide !== "") // bottom and right, top and left
let expandValue = expand ? ((isSameSide || isOppositeSide) ? previousExpandValue : (widgets.length - index) + 1) : (previousExpandValue > 0) ? 0 : 1
if (expand && expandValue === 0){
expandValue = 1 // if there is expand it should expand
}
if (expand && !isSameSide) previousExpandValue = expandValue;
if (expand && isDiagonal){
expandValue = 1
}
// TODO: if the child widget as fillx or y use flex grow
if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){
previousExpandValue = expandValue;
}
lastSide = side; // Update last side for recursion
const anchorStyles = {
@@ -598,7 +607,10 @@ export class TkinterBase extends Widget {
const stretchClass = isVertical ? "tw-flex-grow" : "tw-h-full"; // Allow only horizontal growth for top/bottom
// TODO: if previous expand value is greater than 0 and current doesn't have expand then it should be 0
// const fill = this.getAttrValue("flexManager.fillX") || this.getAttrValue("flexManager.fillY")
if (isSameSide) {
return (
<>
@@ -627,11 +639,12 @@ export class TkinterBase extends Widget {
return (
<div
data-pack-container={side}
className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
className={`tw-flex tw-flex-auto`}
// className={`tw-flex ${isVertical ? "!tw-h-full" : "!tw-w-full"}`}
style={{
display: "flex",
flexDirection: currentWidgetDirection,
flexGrow: expand ? expandValue : 1, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexGrow: expandValue, //((widgets.length - 1) === index) ? 1 : 0, // last index will always have flex-grow 1
flexShrink: expand ? 0 : 1,
flexBasis: "auto",
minWidth: isVertical ? "0" : "auto",
@@ -659,7 +672,7 @@ export class TkinterBase extends Widget {
{this.renderPackWidgetsRecursively(widgets, index + 1, side, previousExpandValue)}
</div>
);
};
}
@@ -687,9 +700,13 @@ export class TkinterBase extends Widget {
setLayout(value) {
const { layout, direction, grid = { rows: 1, cols: 1 }, gap = 10, align } = value
const gridRow = this.getAttrValue("gridConfig.noOfRows") || 3 // suppose its loaded using this.load, we need this
const gridCol = this.getAttrValue("gridConfig.noOfCols") || 3
if (layout === Layouts.GRID){
const rowWeight = this.getAttrValue("gridWeights.rowWeights") || undefined // suppose its loaded via this.load
const colWeight = this.getAttrValue("gridWeights.colWeights") || undefined // suppose its loaded via this.load
const {...restAttrs} = this.state.attrs
@@ -703,7 +720,7 @@ export class TkinterBase extends Widget {
label: "No of rows",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "no of rows", max: 1000, min: 1 },
value: 3,
value: gridRow,
onChange: (value) => {
this.setAttrValue("gridConfig.noOfRows", value)
@@ -729,7 +746,7 @@ export class TkinterBase extends Widget {
label: "No of cols",
tool: Tools.NUMBER_INPUT,
toolProps: { placeholder: "no of cols", max: 1000, min: 1 },
value: 3,
value: gridCol,
onChange: (value) => {
this.setAttrValue("gridConfig.noOfCols", value)
@@ -753,7 +770,7 @@ export class TkinterBase extends Widget {
// placeholder: "weight",
// defaultWeightMapping: this.getAttrValue("gridWeights.rowWeights"),
},
value: undefined,
value: rowWeight,
Component: DynamicGridWeightInput,
onChange: (value) => {
@@ -781,7 +798,7 @@ export class TkinterBase extends Widget {
// placeholder: "weight",
// defaultWeightMapping: {0: {weight: 0, gridNo: 0}}
},
value: undefined,
value: colWeight,
Component: DynamicGridWeightInput,
onChange: (value) => {
@@ -812,8 +829,6 @@ export class TkinterBase extends Widget {
}else if (layout === Layouts.FLEX){
const {gridConfig, gridWeights, ...restAttrs} = this.state.attrs
console.log("Flex: ", restAttrs)
this.updateState((prevState) => ({attrs: {...restAttrs}}))
}
@@ -827,8 +842,8 @@ export class TkinterBase extends Widget {
flexDirection: "column",
// flexDirection: direction,
gap: `${gap}px`,
gridTemplateColumns: "repeat(3, max-content)",
gridTemplateRows: "repeat(3, max-content)",
gridTemplateColumns: `repeat(${gridRow}, max-content)`,
gridTemplateRows: `repeat(${gridCol}, max-content)`,
// gridTemplateColumns: "repeat(auto-fill, minmax(100px, auto))",
// gridTemplateRows: "repeat(auto-fill, minmax(100px, auto))",
}
@@ -842,18 +857,6 @@ export class TkinterBase extends Widget {
const {layout: parentLayout, direction, gap} = this.getParentLayout() || {}
// if (parentLayout === Layouts.FLEX){
// const fillX = this.getAttrValue("flexManager.fillX")
// const fillY = this.getAttrValue("flexManager.fillY")
// // This is needed if fillX or fillY is true, as the parent is applied flex-grow
// if (fillX || fillY){
// width = "100%"
// height = "100%"
// }
// }
const styling = {
...this.state.widgetInnerStyling,
@@ -900,6 +903,19 @@ export class TkinterBase extends Widget {
}
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
layout,
gridConfig,
gridWeights
})
}
serialize(){
return ({
...super.serialize(),
@@ -950,8 +966,10 @@ export class TkinterBase extends Widget {
...layoutUpdates,
pos
}
const {layout} = attrs
this.setState(newData, () => {
let layoutAttrs = this.setParentLayout(parentLayout).attrs || {}
@@ -977,15 +995,21 @@ export class TkinterBase extends Widget {
if (newAttrs?.styling?.backgroundColor){
// TODO: find a better way to apply innerStyles
// TODO: find a better way to apply innerStyles (we may not need this anymore)
this.setWidgetInnerStyle("backgroundColor", newAttrs.styling.backgroundColor.value)
}
this.updateState({ attrs: newAttrs }, callback)
// FIXME: when changing layouts all the widgets are being selected
if (selected){
this.select()
}
}
if (layout){
this.setLayout(layout)
}
})
@@ -1201,23 +1225,23 @@ export class TkinterWidgetBase extends TkinterBase{
if (this.getAttrValue("cursor"))
config["cursor"] = `"${this.getAttrValue("cursor")}"`
if (this.getAttrValue("padding.padX")){
// inner padding
config["ipadx"] = this.getAttrValue("padding.padX")
}
// if (this.getAttrValue("padding.padX")){
// // inner padding
// config["ipadx"] = this.getAttrValue("padding.padX")
// }
if (this.getAttrValue("padding.padY")){
config["ipady"] = this.getAttrValue("padding.padY")
}
// if (this.getAttrValue("padding.padY")){
// config["ipady"] = this.getAttrValue("padding.padY")
// }
if (this.getAttrValue("margin.marginX")){
config["padx"] = this.getAttrValue("margin.marginX")
}
// if (this.getAttrValue("margin.marginX")){
// config["padx"] = this.getAttrValue("margin.marginX")
// }
if (this.getAttrValue("margin.marginY")){
config["pady"] = this.getAttrValue("margin.marginY")
}
// if (this.getAttrValue("margin.marginY")){
// config["pady"] = this.getAttrValue("margin.marginY")
// }
// FIXME: add width and height, the scales may not be correct as the width and height are based on characters in label and grid not pixels
// if (!this.state.fitContent.width){

View File

@@ -161,7 +161,20 @@ class Frame extends TkinterBase{
]
}
getToolbarAttrs(){
const {layout, gridConfig, gridWeights, ...toolBarAttrs} = super.getToolbarAttrs()
// places layout at the end
return ({
id: this.__id,
...toolBarAttrs,
padding: this.state.attrs.padding,
margin: this.state.attrs.margin,
layout,
gridConfig,
gridWeights
})
}
renderContent(){
// console.log("bounding rect: ", this.getBoundingRect())

View File

@@ -1,3 +1,5 @@
import { useEffect, useState } from "react"
import { Layouts } from "../../../canvas/constants/layouts"
import Tools from "../../../canvas/constants/tools"
import { convertObjectToKeyValueString } from "../../../utils/common"
import { getPythonAssetPath } from "../../utils/pythonFilePath"
@@ -10,6 +12,7 @@ class Label extends TkinterWidgetBase{
static widgetType = "label"
static displayName = "Label"
// static requiredCustomPyFiles = ["imageLabel"]
constructor(props) {
super(props)
@@ -55,6 +58,34 @@ class Label extends TkinterWidgetBase{
value: "",
onChange: (value) => this.setAttrValue("imageUpload", value)
},
imageSize: {
label: "Image size",
display: "horizontal",
// width: {
// label: "Width",
// tool: Tools.NUMBER_INPUT, // the tool to display, can be either HTML ELement or a constant string
// toolProps: { placeholder: "width", max: 3000, min: 1 },
// value: 150,
// onChange: (value) => this.setWidgetSize(value, null)
// },
// height: {
// label: "Height",
// tool: Tools.NUMBER_INPUT,
// toolProps: { placeholder: "height", max: 3000, min: 1 },
// value: 150,
// onChange: (value) => this.setWidgetSize(null, value)
// },
mode: {
label: "Image mode",
tool: Tools.SELECT_DROPDOWN,
options: ["fit", "cover"].map((val) => ({value: val, label: val})),
value: "cover",
onChange: (value) => {
this.setAttrValue("imageSize.mode", value)
}
}
},
}
}
@@ -73,11 +104,20 @@ class Label extends TkinterWidgetBase{
const imports = super.getImports()
if (this.getAttrValue("imageUpload"))
imports.push("import os", "from PIL import Image, ImageTk", )
imports.push("import os", "from PIL import Image, ImageTk", "from customWidgets.imageLabel import ImageLabel")
return imports
}
getRequiredCustomPyFiles(){
const requiredCustomFiles = super.getRequiredCustomPyFiles()
if (this.getAttrValue("imageUpload"))
requiredCustomFiles.push("imageLabel")
return requiredCustomFiles
}
getRequirements(){
const requirements = super.getRequirements()
@@ -92,10 +132,28 @@ class Label extends TkinterWidgetBase{
const config = super.getConfigCode()
const anchor = this.getAttrValue("styling.anchor")
const fitWidth = this.state.fitContent.width
const fitHeight = this.state.fitContent.height
const {width, height} = this.getSize()
const {layout} = this.getParentLayout()
if (anchor)
config['anchor'] = `"${anchor}"`
// LABEL width and height are not pixel based instead its character based
// if (layout !== Layouts.PLACE){
// if (!fitWidth){
// config['width'] = width
// }
// if (!fitHeight){
// config['height'] = height
// }
// }
return config
}
@@ -112,10 +170,10 @@ class Label extends TkinterWidgetBase{
const code = []
if (image?.name){
code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`)
code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
// code.push(`${variableName}_img = Image.open(${getPythonAssetPath(image.name, "image")})`)
// code.push(`${variableName}_img = ImageTk.PhotoImage(${variableName}_img)`)
// code.push("\n")
labelInitialization = `${variableName} = tk.Label(master=${parent}, image=${variableName}_img, text="${labelText}", compound=tk.TOP)`
labelInitialization = `${variableName} = ImageLabel(master=${parent}, image_path=${getPythonAssetPath(image.name, "image")}, text="${labelText}", compound=tk.TOP, mode="${this.getAttrValue("imageSize.mode")}")`
}
// code.push("\n")
@@ -155,36 +213,53 @@ class Label extends TkinterWidgetBase{
center: { justifyContent: 'center', alignItems: 'center' }
}
return anchorStyles[anchor] || anchorStyles["w"];
return anchorStyles[anchor] || anchorStyles["center"];
}
renderContent(){
//FIXME: label image causing issues
const image = this.getAttrValue("imageUpload")
const imageMode = this.getAttrValue("imageSize.mode") || "cover"
const imgClassName = imageMode === "fit" ? "tw-object-contain" : (imageMode === "cover" ? "tw-object-cover" : "")
return (
<div className="tw-w-flex tw-flex-col tw-w-full tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden"
style={{
flexGrow: 1, // Ensure the content grows to fill the parent
minWidth: '100%', // Force the width to 100% of the parent
minHeight: '100%', // Force the height to 100% of the parent
}}
<div className="tw-flex tw-flex-col tw-w-full tw-relative tw-content-start tw-h-full tw-rounded-md tw-overflow-hidden"
// style={{
// flexGrow: 1, // Ensure the content grows to fill the parent
// minWidth: '100%', // Force the width to 100% of the parent
// minHeight: '100%', // Force the height to 100% of the parent
// }}
>
<div className="tw-p-2 tw-w-full tw-h-full tw-flex tw-place-content-center tw-place-items-center "
<div className="tw-p-2 tw-w-full tw-h-full tw-overflow-hidden tw-flex tw-place-content-center tw-place-items-center"
ref={this.styleAreaRef}
style={this.getInnerRenderStyling()}>
{/* {this.props.children} */}
{
image && (
<img src={image.previewUrl} className="tw-bg-contain tw-w-full tw-h-full" />
)
}
<div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")}
</div>
{/* {this.props.children} */}
{
image ? (
<div className="tw-relative tw-w-full tw-h-full tw-overflow-hidden">
<img src={image.previewUrl}
className={`${imgClassName}`}
alt={this.getAttrValue("widgetName")}
style={{
width: "100%",
height: "100%",
position: "absolute",
top: '0px',
left: '0px',
}}
// className="tw-object-cover"
/>
</div>
) : null
}
<div className={`tw-flex ${!image ? "tw-w-full tw-h-full" : ""}`} style={{
color: this.getAttrValue("styling.foregroundColor"),
...this.getAnchorStyle(this.getAttrValue("styling.anchor"))
}}>
{this.getAttrValue("labelWidget")}
</div>
</div>
</div>
)
@@ -193,4 +268,44 @@ class Label extends TkinterWidgetBase{
}
function LabelImage({imageSrc, alt, styleAreaRef}){
const [size, setSize] = useState({
width: 0,
height: 0
})
useEffect(() => {
if (!styleAreaRef.current) return;
// Function to update size
const updateSize = () => {
const boundingBox = styleAreaRef.current.getBoundingClientRect();
setSize({ width: boundingBox.width, height: boundingBox.height });
};
// Initial size update
updateSize();
// Observe size changes
const resizeObserver = new ResizeObserver(updateSize);
resizeObserver.observe(styleAreaRef.current);
return () => resizeObserver.disconnect();
}, [styleAreaRef])
return (
<img src={imageSrc} alt={alt} className="tw-object-cover"
style={{
position: "absolute",
top: '0px',
left: '0px',
width: "100%", // Prevent overflow
height: "100%", // Prevent overflow
}}
/>
)
}
export default Label

View File

@@ -14,6 +14,9 @@ import "./styles/index.css";
import { FileUploadProvider } from "./contexts/fileUploadContext";
window.React = React
const originalSetItem = localStorage.setItem;
// triggers itemsChaned event whenever the item in localstorage is chanegd.
localStorage.setItem = function (key, value) {

View File

@@ -4,7 +4,7 @@
*/
import { useEffect, useRef, useMemo, useState } from "react";
import { BookOutlined, CloseCircleFilled, CrownFilled, GithubFilled, ShareAltOutlined } from "@ant-design/icons";
import { BookOutlined, CloseCircleFilled, CrownFilled, DiscordFilled, GithubFilled, ShareAltOutlined } from "@ant-design/icons";
import KO_FI from "../assets/logo/ko-fi.png"
import Premium from "./utils/premium";
@@ -88,6 +88,11 @@ function Sidebar({tabs}){
}
<div className="tw-flex tw-flex-col tw-place-content-items tw-place-items-center tw-gap-3 tw-mt-auto">
<a href="https://discord.gg/dHXjrrCA7G" target="_blank"
title="discord invite"
rel="noopener noreferrer" className="tw-text-3xl tw-cursor-pointer tw-text-[#5562EA]">
<DiscordFilled />
</a>
<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
@@ -107,9 +112,9 @@ function Sidebar({tabs}){
rel="noopener noreferrer" className="tw-text-2xl tw-cursor-pointer tw-text-black">
<GithubFilled />
</a>
<a href="https://ko-fi.com/artpaul" className="tw-cursor-pointer ">
{/* <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>
</a> */}
</div>
</div>

View File

@@ -38,11 +38,7 @@ function Premium({ children, className = "" }) {
If you find this tool useful and want to fund and support it's development, consider buying a <b>one time license</b>.
<br />
<br />
I am working on PYUI builder full time to help you easily build GUI's in python. However as a solo-dev, its super hard
to keep working on it without enough funding.
<br />
<br />
By buying pre-order license, I get to complete this faster and you get discounted price, advance features, priority support, early access, upcoming features, and &nbsp;
By pre-ordering license, you get discounted price, advance features, priority support, early access, upcoming features, and &nbsp;
<a
href="https://github.com/PaulleDemon/PyUIBuilder/blob/main/roadmap.md"
target="_blank"
@@ -51,6 +47,10 @@ function Premium({ children, className = "" }) {
>
more.
</a>
<br />
<br />
Premium features will start rolling out phase wise from mid of April, after which there would be a price increase.
</div>
@@ -145,7 +145,7 @@ function Premium({ children, className = "" }) {
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">
<ul className="tw-mt-4 tw-flex tw-flex-col tw-gap-3 tw-text-xl tw-text-black">
<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>

110
webpack.config.js Normal file
View File

@@ -0,0 +1,110 @@
const webpack = require('webpack');
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
const dotenv = require('dotenv');
const fs = require('fs');
const isProduction = process.env.NODE_ENV === "production";
// Load .env-cmdrc.json based on NODE_ENV
const envFile = `.env-cmdrc.json`;
const envConfig = JSON.parse(fs.readFileSync(envFile, 'utf8'))[process.env.NODE_ENV] || {};
// Convert JSON to a format Webpack understands
const envKeys = Object.keys(envConfig).reduce((prev, next) => {
prev[`process.env.${next}`] = JSON.stringify(envConfig[next]);
return prev;
}, {});
module.exports = {
mode: isProduction ? "production" : "development",
watch: !isProduction,
watchOptions: {
ignored: /node_modules/,
},
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: isProduction ? "js/[name].[contenthash].js" : "js/[name].js",
publicPath: "/",
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env", "@babel/preset-react"],
},
},
},
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader",],
},
{
test: /\.py$/,
use: "raw-loader", // Load Python files as raw text
},
{
test: /\.svg$/,
use: ["@svgr/webpack"], // Enables importing SVGs as React components
},
{
test: /\.(png|jpe?g|gif|ico)$/,
type: "asset/resource", // Handles image files
},
],
},
plugins: [
new webpack.DefinePlugin(envKeys),
new HtmlWebpackPlugin({
template: "./public/index.html",
minify: isProduction,
inject: true,
templateParameters: envConfig
}),
(isProduction ? new MiniCssExtractPlugin({ filename: 'styles.[contenthash].css' }) : new MiniCssExtractPlugin()),
new CopyWebpackPlugin({
patterns: [
{
from: path.resolve(__dirname, "src/assets"),
to: "assets", // Copies to `dist/assets`
noErrorOnMissing: true, // Prevents errors if the folder is missing
},
{ from: 'public', to: '', noErrorOnMissing: true, globOptions: { ignore: ['**/index.html'] }}, // Copies everything else from public to dist
],
}),
new webpack.EnvironmentPlugin({
REACT_APP_ANALYTICS_SCRIPT_ID: process.env.REACT_APP_ANALYTICS_SCRIPT_ID || '', // Default empty value
API_URL: 'https://default.api.com'
})
],
optimization: isProduction
? {
minimize: true,
minimizer: [new CssMinimizerPlugin()],
}
: {},
devServer: {
static: path.join(__dirname, "public"),
port: 3000,
hot: true,
historyApiFallback: true,
},
};