added webpack and fixed ImageLabel

This commit is contained in:
paul
2025-04-01 18:50:14 +05:30
parent a38cd90c16
commit 66d7dfc45f
12 changed files with 1705 additions and 734 deletions

1
.gitignore vendored
View File

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

2211
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",
@@ -66,9 +66,11 @@
"@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",

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

@@ -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

@@ -159,7 +159,6 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
})
}
// TODO: add empty __init__ file
for (let customWidget of customPythonWidgets){
let [fileName, extension] = customWidget.split(".")
@@ -169,9 +168,8 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
}
const fileContent = pythonFiles(`./${fileName}`);
const fileContent = pythonFiles(`./${fileName}`).default
console.log("file name: ", fileContent.default, pythonFiles(`./${fileName}`))
createFileList.push({
fileData: new Blob([fileContent], { type: "text/plain" }),
fileName: fileName,
@@ -179,6 +177,14 @@ async function generateTkinterCode(projectName, widgetList=[], widgetRefs=[], as
})
}
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

@@ -1,26 +1,43 @@
# Author: Paul: https://github.com/PaulleDemon
# Made using PyUibuilder: https://pyuibuilder.com
# MIT License - keep the copy of this license
import tkinter as tk
from PIL import Image, ImageTk
class ImageLabel(tk.Label):
def __init__(self, master, image_path, mode="fit", *args, **kwargs):
def __init__(self, master, image_path=None, mode="cover", width=100, height=100, *args, **kwargs):
"""
mode:
mode:
- "fit" -> Keeps aspect ratio, fits inside label
- "cover" -> Covers label fully, cropping excess
"""
super().__init__(master, *args, **kwargs)
self.parent = master # Store parent reference
super().__init__(master, width=width, height=height, *args, **kwargs)
self.parent = master
self.image_path = image_path
self.mode = mode
self.original_image = Image.open(image_path)
self.original_image = None
self.photo = None
self.resize_job = None # Debounce job reference
self.force_resize() # Initial resize
self.after(100, self.init_events) # Delay event binding slightly
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) # Bind resize to parent
self.parent.bind("<Configure>", self.on_resize)
def on_resize(self, event=None):
"""Debounce resizing to prevent rapid execution."""
@@ -30,8 +47,13 @@ class ImageLabel(tk.Label):
def force_resize(self):
"""Resize image using actual widget size."""
width = self.winfo_width()
height = self.winfo_height()
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
@@ -53,6 +75,7 @@ class ImageLabel(tk.Label):
else:
new_width = int(height * aspect_ratio)
new_height = height
resized = self.original_image.resize((new_width, new_height), Image.LANCZOS)
# Crop excess

View File

@@ -561,7 +561,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",
@@ -587,7 +588,7 @@ export class TkinterBase extends Widget {
expandValue = 1
}
// TODO: if previous expand value is greater than 0 and current doesn't have expand then it should be 0
// TODO: if the child widget as fillx or y use flex grow
if ((expand && !isSameSide) || (expand && previousExpandValue === 0)){
previousExpandValue = expandValue;
@@ -610,7 +611,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 (
<>

View File

@@ -12,7 +12,7 @@ class Label extends TkinterWidgetBase{
static widgetType = "label"
static displayName = "Label"
static requiredCustomPyFiles = ["imageLabel"]
// static requiredCustomPyFiles = ["imageLabel"]
constructor(props) {
super(props)
@@ -104,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()
@@ -161,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")

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

@@ -47,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>

View File

@@ -1,11 +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 = {
module: {
rules: [
{
test: /\.py$/,
use: "raw-loader",
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,
},
};