You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
547 lines
14 KiB
547 lines
14 KiB
""" |
|
Vue.js/Quasar project generator for BadGUI. |
|
""" |
|
|
|
import os |
|
import json |
|
import shutil |
|
from typing import List, Dict, Any, Optional |
|
from .core import App, Component |
|
|
|
|
|
class VueGenerator: |
|
"""Generates Vue.js/Quasar project files from BadGUI components.""" |
|
|
|
def __init__(self, app: App): |
|
self.app = app |
|
|
|
def generate_project(self, output_dir: str, project_name: str = "badgui-app", **kwargs): |
|
"""Generate the complete Vue.js/Quasar project.""" |
|
|
|
# Create output directory |
|
os.makedirs(output_dir, exist_ok=True) |
|
|
|
# Generate project structure |
|
self._create_project_structure(output_dir, project_name) |
|
self._generate_package_json(output_dir, project_name) |
|
self._generate_quasar_config(output_dir) |
|
self._generate_main_layout(output_dir) |
|
self._generate_pages(output_dir) |
|
self._generate_router(output_dir) |
|
self._generate_app_vue(output_dir) |
|
self._generate_boot_files(output_dir) |
|
self._generate_css_files(output_dir) |
|
self._copy_static_files(output_dir) |
|
|
|
def _create_project_structure(self, output_dir: str, project_name: str): |
|
"""Create the basic project directory structure.""" |
|
directories = [ |
|
'src', |
|
'src/components', |
|
'src/layouts', |
|
'src/pages', |
|
'src/router', |
|
'src/boot', |
|
'src/css', |
|
'src/assets', |
|
'public' |
|
] |
|
|
|
for directory in directories: |
|
os.makedirs(os.path.join(output_dir, directory), exist_ok=True) |
|
|
|
def _generate_package_json(self, output_dir: str, project_name: str): |
|
"""Generate package.json file.""" |
|
package_json = { |
|
"name": project_name, |
|
"version": "0.0.1", |
|
"description": "A Vue.js/Quasar app generated by BadGUI", |
|
"productName": project_name.title(), |
|
"author": "BadGUI Generator", |
|
"private": True, |
|
"scripts": { |
|
"dev": "quasar dev", |
|
"build": "quasar build", |
|
"build:pwa": "quasar build -m pwa" |
|
}, |
|
"dependencies": { |
|
"@quasar/extras": "^1.16.4", |
|
"quasar": "^2.12.0", |
|
"vue": "^3.3.4", |
|
"vue-router": "^4.2.4" |
|
}, |
|
"devDependencies": { |
|
"@quasar/app-vite": "^1.3.0", |
|
"@types/node": "^20.4.5", |
|
"@typescript-eslint/eslint-plugin": "^6.2.0", |
|
"@typescript-eslint/parser": "^6.2.0", |
|
"autoprefixer": "^10.4.14", |
|
"eslint": "^8.45.0", |
|
"eslint-config-prettier": "^8.8.0", |
|
"eslint-plugin-vue": "^9.15.1", |
|
"prettier": "^3.0.0", |
|
"typescript": "^5.1.6" |
|
}, |
|
"engines": { |
|
"node": "^20 || ^18 || ^16 || ^14.19", |
|
"npm": ">= 6.13.4", |
|
"yarn": ">= 1.21.1" |
|
} |
|
} |
|
|
|
with open(os.path.join(output_dir, 'package.json'), 'w') as f: |
|
json.dump(package_json, f, indent=2) |
|
|
|
def _generate_quasar_config(self, output_dir: str): |
|
"""Generate quasar.config.js file.""" |
|
config_content = '''const { configure } = require('quasar/wrappers'); |
|
|
|
module.exports = configure(function (ctx) { |
|
return { |
|
eslint: { |
|
fix: false, |
|
warnings: false, |
|
errors: false, |
|
}, |
|
|
|
boot: [ |
|
'badgui' |
|
], |
|
|
|
css: [ |
|
'app.scss' |
|
], |
|
|
|
extras: [ |
|
'roboto-font', |
|
'material-icons', |
|
], |
|
|
|
build: { |
|
target: { |
|
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'], |
|
node: 'node16' |
|
}, |
|
|
|
vueRouterMode: 'hash', |
|
}, |
|
|
|
devServer: { |
|
open: true |
|
}, |
|
|
|
framework: { |
|
config: {}, |
|
plugins: [ |
|
'Notify' |
|
] |
|
}, |
|
|
|
animations: [], |
|
|
|
ssr: { |
|
pwa: false, |
|
prodPort: 3000, |
|
}, |
|
|
|
pwa: { |
|
workboxMode: 'injectManifest', |
|
injectManifest: { |
|
swSrc: 'src-pwa/sw.js' |
|
}, |
|
|
|
manifestFilename: 'manifest.json', |
|
useCredentialsForManifestTag: false, |
|
}, |
|
|
|
cordova: {}, |
|
|
|
capacitor: { |
|
hideSplashscreen: true |
|
}, |
|
|
|
electron: { |
|
inspectPort: 5858, |
|
|
|
bundler: 'packager', |
|
|
|
packager: {}, |
|
|
|
builder: { |
|
appId: 'badgui-app' |
|
} |
|
}, |
|
|
|
bex: { |
|
contentScripts: [ |
|
'my-content-script' |
|
], |
|
} |
|
} |
|
}); |
|
''' |
|
with open(os.path.join(output_dir, 'quasar.config.js'), 'w') as f: |
|
f.write(config_content) |
|
|
|
def _generate_main_layout(self, output_dir: str): |
|
"""Generate the main layout file.""" |
|
layout_content = '''<template> |
|
<q-layout view="lHh Lpr lFf"> |
|
<q-page-container> |
|
<router-view /> |
|
</q-page-container> |
|
</q-layout> |
|
</template> |
|
|
|
<script setup> |
|
// Layout setup |
|
</script> |
|
''' |
|
layout_path = os.path.join(output_dir, 'src', 'layouts', 'MainLayout.vue') |
|
with open(layout_path, 'w') as f: |
|
f.write(layout_content) |
|
|
|
def _generate_pages(self, output_dir: str): |
|
"""Generate all page components from BadGUI pages.""" |
|
|
|
# If no pages defined, create default from old components structure |
|
if not self.app.pages: |
|
# Backward compatibility: create default page from root components |
|
if hasattr(self.app, 'components'): |
|
self.app._ensure_default_page() |
|
self.app.current_page.components = getattr(self.app, 'components', []) |
|
|
|
# Generate each page |
|
for path, page in self.app.pages.items(): |
|
self._generate_page_component(output_dir, page) |
|
|
|
def _generate_page_component(self, output_dir: str, page): |
|
"""Generate a single page component.""" |
|
|
|
# Generate the template from page components |
|
template_content = self._generate_vue_template_for_page(page) |
|
|
|
page_content = f'''<template> |
|
<q-page class="row items-center justify-evenly"> |
|
{template_content} |
|
</q-page> |
|
</template> |
|
|
|
<script setup> |
|
import {{ ref }} from 'vue' |
|
|
|
// Component state |
|
const state = ref({{}}) |
|
|
|
// Event handlers |
|
const handleClick = (event) => {{ |
|
console.log('Button clicked:', event) |
|
}} |
|
</script> |
|
|
|
<style scoped> |
|
.row {{ |
|
display: flex; |
|
flex-direction: row; |
|
gap: 16px; |
|
align-items: center; |
|
}} |
|
|
|
.column {{ |
|
display: flex; |
|
flex-direction: column; |
|
gap: 16px; |
|
}} |
|
</style> |
|
''' |
|
|
|
page_path = os.path.join(output_dir, 'src', 'pages', f'{page.name}.vue') |
|
with open(page_path, 'w') as f: |
|
f.write(page_content) |
|
|
|
def _generate_vue_template_for_page(self, page) -> str: |
|
"""Generate Vue template from page components.""" |
|
if not page.components: |
|
return " <div>No components defined</div>" |
|
|
|
template = "" |
|
for component in page.components: |
|
template += component.to_vue_template(2) # 2 spaces indent for page content |
|
|
|
return template |
|
|
|
def _generate_router(self, output_dir: str): |
|
"""Generate Vue Router configuration.""" |
|
router_content = '''import { route } from 'quasar/wrappers' |
|
import { createRouter, createMemoryHistory, createWebHistory, createWebHashHistory } from 'vue-router' |
|
|
|
import routes from './routes' |
|
|
|
export default route(function (/* { store, ssrContext } */) { |
|
const createHistory = process.env.SERVER |
|
? createMemoryHistory |
|
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory) |
|
|
|
const Router = createRouter({ |
|
scrollBehavior: () => ({ left: 0, top: 0 }), |
|
routes, |
|
history: createHistory(process.env.VUE_ROUTER_BASE) |
|
}) |
|
|
|
return Router |
|
}) |
|
''' |
|
|
|
# Generate routes from pages |
|
page_routes = [] |
|
if not self.app.pages: |
|
# Default route for backward compatibility |
|
page_routes.append(" { path: '', component: () => import('pages/HomePage.vue') }") |
|
else: |
|
for path, page in self.app.pages.items(): |
|
if path == '/': |
|
page_routes.append(f" {{ path: '', component: () => import('pages/{page.name}.vue') }}") |
|
else: |
|
page_routes.append(f" {{ path: '{path.lstrip('/')}', component: () => import('pages/{page.name}.vue') }}") |
|
|
|
routes_content = f'''const routes = [ |
|
{{ |
|
path: '/', |
|
component: () => import('layouts/MainLayout.vue'), |
|
children: [ |
|
{",".join([chr(10) + route for route in page_routes])} |
|
] |
|
}}, |
|
|
|
// Always leave this as last one, |
|
// but you can also remove it |
|
{{ |
|
path: '/:catchAll(.*)*', |
|
component: () => import('pages/ErrorNotFound.vue') |
|
}} |
|
] |
|
|
|
export default routes |
|
''' |
|
|
|
router_path = os.path.join(output_dir, 'src', 'router', 'index.js') |
|
routes_path = os.path.join(output_dir, 'src', 'router', 'routes.js') |
|
|
|
with open(router_path, 'w') as f: |
|
f.write(router_content) |
|
|
|
with open(routes_path, 'w') as f: |
|
f.write(routes_content) |
|
|
|
def _generate_app_vue(self, output_dir: str): |
|
"""Generate the main App.vue file.""" |
|
app_content = '''<template> |
|
<div id="q-app"> |
|
<router-view /> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
export default { |
|
name: 'App' |
|
} |
|
</script> |
|
''' |
|
|
|
app_path = os.path.join(output_dir, 'src', 'App.vue') |
|
with open(app_path, 'w') as f: |
|
f.write(app_content) |
|
|
|
def _generate_boot_files(self, output_dir: str): |
|
"""Generate boot files for Quasar.""" |
|
boot_content = '''import { boot } from 'quasar/wrappers' |
|
|
|
// BadGUI boot file |
|
export default boot(async ({ app, router, store }) => { |
|
// Initialize BadGUI specific functionality |
|
console.log('BadGUI initialized') |
|
}) |
|
''' |
|
|
|
boot_path = os.path.join(output_dir, 'src', 'boot', 'badgui.js') |
|
with open(boot_path, 'w') as f: |
|
f.write(boot_content) |
|
|
|
def _generate_css_files(self, output_dir: str): |
|
"""Generate CSS files.""" |
|
css_content = '''// app global css |
|
@import './quasar.variables.scss'; |
|
|
|
body { |
|
font-family: 'Roboto', sans-serif; |
|
} |
|
|
|
.row { |
|
display: flex; |
|
flex-direction: row; |
|
gap: 16px; |
|
align-items: center; |
|
} |
|
|
|
.column { |
|
display: flex; |
|
flex-direction: column; |
|
gap: 16px; |
|
} |
|
''' |
|
|
|
variables_content = '''// Quasar SCSS Variables |
|
$primary : #1976D2; |
|
$secondary : #26A69A; |
|
$accent : #9C27B0; |
|
|
|
$dark : #1D1D1D; |
|
$dark-page : #121212; |
|
|
|
$positive : #21BA45; |
|
$negative : #C10015; |
|
$info : #31CCEC; |
|
$warning : #F2C037; |
|
''' |
|
|
|
css_path = os.path.join(output_dir, 'src', 'css', 'app.scss') |
|
variables_path = os.path.join(output_dir, 'src', 'css', 'quasar.variables.scss') |
|
|
|
with open(css_path, 'w') as f: |
|
f.write(css_content) |
|
|
|
with open(variables_path, 'w') as f: |
|
f.write(variables_content) |
|
|
|
def _copy_static_files(self, output_dir: str): |
|
"""Copy static files and create additional necessary files.""" |
|
|
|
# Create index.html in root directory |
|
index_html = '''<!DOCTYPE html> |
|
<html> |
|
<head> |
|
<title><%= productName %></title> |
|
<meta charset="utf-8"> |
|
<meta name="description" content="<%= productDescription %>"> |
|
<meta name="format-detection" content="telephone=no"> |
|
<meta name="msapplication-tap-highlight" content="no"> |
|
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.theme && ctx.theme.capacitor || ctx.mode && ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"> |
|
|
|
<link rel="icon" type="image/png" sizes="128x128" href="icons/favicon-128x128.png"> |
|
<link rel="icon" type="image/png" sizes="96x96" href="icons/favicon-96x96.png"> |
|
<link rel="icon" type="image/png" sizes="32x32" href="icons/favicon-32x32.png"> |
|
<link rel="icon" type="image/png" sizes="16x16" href="icons/favicon-16x16.png"> |
|
<link rel="icon" type="image/ico" href="favicon.ico"> |
|
</head> |
|
<body> |
|
<!-- quasar:entry-point --> |
|
</body> |
|
</html> |
|
''' |
|
|
|
# Create main.js |
|
main_js = '''import { createApp } from 'vue' |
|
import { Quasar } from 'quasar' |
|
|
|
// Import icon libraries |
|
import '@quasar/extras/material-icons/material-icons.css' |
|
|
|
// Import Quasar css |
|
import 'quasar/src/css/index.sass' |
|
|
|
// Assumes your root component is App.vue |
|
// and placed in same folder as main.js |
|
import App from './App.vue' |
|
import router from './router' |
|
|
|
const myApp = createApp(App) |
|
|
|
myApp.use(Quasar, { |
|
plugins: {}, // import Quasar plugins and add here |
|
}) |
|
|
|
myApp.use(router) |
|
|
|
// Assumes you have a <div id="app"></div> in your index.html |
|
myApp.mount('#q-app') |
|
''' |
|
|
|
# Create index.html in root directory for Quasar |
|
index_path = os.path.join(output_dir, 'index.html') |
|
main_path = os.path.join(output_dir, 'src', 'main.js') |
|
|
|
with open(index_path, 'w') as f: |
|
f.write(index_html) |
|
|
|
with open(main_path, 'w') as f: |
|
f.write(main_js) |
|
|
|
# Create error page |
|
error_page = '''<template> |
|
<div class="fullscreen bg-blue text-white text-center q-pa-md flex flex-center"> |
|
<div> |
|
<div style="font-size: 30vh"> |
|
404 |
|
</div> |
|
|
|
<div class="text-h2" style="opacity:.4"> |
|
Oops. Nothing here... |
|
</div> |
|
|
|
<q-btn |
|
class="q-mt-xl" |
|
color="white" |
|
text-color="blue" |
|
unelevated |
|
to="/" |
|
label="Go Home" |
|
no-caps |
|
/> |
|
</div> |
|
</div> |
|
</template> |
|
|
|
<script> |
|
export default { |
|
name: 'ErrorNotFound' |
|
} |
|
</script> |
|
''' |
|
|
|
error_path = os.path.join(output_dir, 'src', 'pages', 'ErrorNotFound.vue') |
|
with open(error_path, 'w') as f: |
|
f.write(error_page) |
|
|
|
# Create basic ESLint config to avoid errors |
|
eslint_config = '''{ |
|
"env": { |
|
"browser": true, |
|
"es2021": true, |
|
"node": true |
|
}, |
|
"extends": [ |
|
"eslint:recommended", |
|
"@vue/eslint-config-typescript" |
|
], |
|
"parser": "@typescript-eslint/parser", |
|
"parserOptions": { |
|
"ecmaVersion": 2021, |
|
"sourceType": "module" |
|
}, |
|
"plugins": [ |
|
"@typescript-eslint", |
|
"vue" |
|
], |
|
"rules": {}, |
|
"overrides": [ |
|
{ |
|
"files": ["*.vue"], |
|
"parser": "vue-eslint-parser" |
|
} |
|
] |
|
} |
|
''' |
|
|
|
eslint_path = os.path.join(output_dir, '.eslintrc.json') |
|
with open(eslint_path, 'w') as f: |
|
f.write(eslint_config) |