diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b16c5ab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,27 @@ +name: CI + +on: + push: + branches: [ coreV4, master ] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + distribution: temurin + java-version: 17 + cache: gradle + + # 🔑 OBLIGATORIO Y ANTES DE TODO + - name: Make gradlew executable + run: chmod +x gradlew + + - name: Assemble + run: ./gradlew composeApp:compileKotlinJvm --no-daemon diff --git a/README.md b/README.md index fd4dc01..6c04fd8 100644 --- a/README.md +++ b/README.md @@ -1,188 +1,204 @@ -# AppMakeup — Core V2 - ---- +# AppMakeup — Core V4 (Closed) ## 🚀 What is AppMakeup? -**AppMakeup** is a desktop application built with **Kotlin Multiplatform** and **Compose Desktop** whose goal is to **visually model application architecture and generate deterministic project structures** following Clean Architecture principles. +**AppMakeup** is a desktop application built with **Kotlin Multiplatform** and **Compose Desktop** that allows you to **model application architecture visually** and **generate deterministic project structures and code** following Clean Architecture principles. -AppMakeup is intentionally **not** an AI tool, nor a "smart" code generator. +AppMakeup is **not an AI tool**. It does **not**: -- guess business logic -- generate application behavior -- run or compile generated apps +- infer business logic +- guess architecture decisions +- generate behavior automatically -Instead, AppMakeup focuses on **architecture as a first-class concept**. +Instead, AppMakeup is a **deterministic architecture compiler**. -> You design the architecture explicitly. AppMakeup materializes it safely. +> You declare architecture. AppMakeup materializes it safely. --- -## 🎯 Why AppMakeup exists - -In real projects, most long-term problems come from: - -- unclear architecture decisions -- inconsistent project structures -- premature coupling between layers -- uncontrolled growth of modules - -AppMakeup exists to solve those problems **before code is written**. - -It allows developers to: -- model architecture visually -- enforce structure without loss of control -- evolve projects without rewrites -- keep Clean Architecture explicit and verifiable - ---- - -## 🧠 Core philosophy - -AppMakeup is built around these principles: +## 🧠 Core Philosophy (Unchanged) - Architecture first, code second - Structure before implementation - Predictability over magic - Deterministic generation -- Strict separation of layers +- Explicit contracts and layers - No filesystem access from UI - No domain logic in presentation -- Explicit, atomic use cases +- Atomic, testable use cases --- -## ✅ Current State — **Core V2 (Stable)** +## ✅ Current State — **Core V4 (Closed & Stable)** -AppMakeup is currently in **Core V2**, which is considered **stable and closed**. +Core V4 is now **feature-complete and closed**. -**Core V2 is about structure, not code.** +It represents the **first full generation-capable core**, where AppMakeup moves from *structure modeling* to **real, validated code generation** while preserving strict architectural guarantees. -This core establishes a solid foundation that future versions build upon without breaking. +Core V4 builds on Core V2 and Core V3 concepts but introduces a **generation pipeline**, **planning stage**, and **dry-run previews**. --- -## ✨ What Core V2 Includes +## ✨ What Core V4 Includes -### 🧭 Project lifecycle +### 🧭 Project Lifecycle - Create new projects - - application name - - package name - - workspace location - Open existing projects -- Persist recent projects on disk -- Normalize and deduplicate paths -- Remove invalid projects automatically +- Persist and validate projects on disk +- Versioned project format +- Recent projects registry + +--- + +### 🧩 Architecture Modeling + +- Features +- Domain entities +- Entity properties +- Identifier enforcement +- Layer selection per feature +- Validation before generation + +Invalid states are **prevented at editor level**. + +--- -### 🔍 Project validation +### 🧪 Generation Pipeline (Core V4) -Before opening a project, AppMakeup validates: +Core V4 introduces a **5-stage deterministic pipeline**: -- directory existence -- AppMakeup project structure -- presence of `project.amk.json` -- JSON integrity -- project version compatibility +1. **ValidationStage** + - Entity rules + - Feature rules + - Architecture constraints -All validation errors are **explicit and user-friendly**. +2. **PlanningStage** + - Decides what layers will be generated + - Domain / Data / Repositories / Mappers + - Fully testable and previewable + +3. **GenerationStage** + - Layer generators + - Templates + - No filesystem access + +4. **WritingStage** + - Real filesystem writer + - Dry-run writer (preview mode) + +5. **ReportingStage** + - CLI / Table / JSON output --- -## 🧩 Architecture Modeling +### 🧪 Dry-Run Mode -Inside a project, Core V2 supports: +Core V4 supports **true dry-run execution**: -- Feature definition -- Domain entity modeling -- Entity property modeling -- Dirty-state tracking -- Model validation before generation -- Deterministic structure generation +- No files are written +- Generated artifacts are collected +- Output paths are simulated +- Safe to run repeatedly -All modeling happens at the **domain level**, never at the code level. +Used for: +- UI preview +- Tests +- Validation before export --- -## 🏗️ Project Architecture (Internal) +### 🧩 Generation Plan Preview (UI) -AppMakeup follows **Clean Architecture**, adapted for a modeling-first tool. +Before exporting, users can see: -### Main layers +- Which layers will be generated +- Whether repositories/mappers apply +- Why a layer is skipped +- Validation errors per feature + +This makes generation **explainable**, not magical. + +--- + +### 🧾 Files Preview (Dry-Run Visual) + +The UI shows: +- Exact files that would be generated +- Relative paths +- Per-feature grouping + +Nothing is written unless explicitly exported. + +--- + +### 🏗️ Internal Architecture (Clean Architecture) **Domain** -- modeling entities -- validation rules -- use cases +- Core models +- Validators +- Generation pipeline +- Planning logic -**Data** -- repositories -- JSON persistence -- filesystem access (Okio) +**Application** +- Use cases +- Intent orchestration -**Templates & Generation** -- project templates -- structure generators -- filesystem writers +**Infrastructure** +- Filesystem +- Exporters +- Pipelines wiring (Koin) **Presentation** - Compose Desktop UI - ViewModels -- UI state management - -**Infrastructure** -- dependency injection (Koin) -- settings persistence -- application configuration +- UI state only --- ## 🧪 Testing & Quality -Core V2 has strong test coverage: +Core V4 is heavily tested: -- Domain logic is fully unit tested -- Structure generation is deterministic and testable -- Filesystem operations are tested using fake filesystems -- Tests validate **structure**, not code +- Pipeline unit tests +- Dry-run generation tests +- Failure-path tests +- Deterministic artifact assertions -Code style consistency is enforced using **ktlint**. +Generation is **100% testable without filesystem**. --- -## 🚫 What Core V2 Intentionally Does NOT Do - -To keep the core clean and extensible, Core V2 does **not**: +## 🚫 What Core V4 Does NOT Do -- generate Kotlin/Java code -- create ViewModels or Screens automatically -- run or compile generated projects -- perform automatic migrations -- provide undo/redo -- act as an IDE replacement +By design, Core V4 does **not**: -These are **deliberate design decisions**. +- generate UI screens +- generate ViewModels +- guess repository contracts +- auto-migrate projects +- act as an IDE +- include undo/redo (yet) --- -## 🗺️ Roadmap +## 🔮 What Comes Next — **Core V5 (Planned)** -### 🔜 Core V3 — Code Generation +Core V5 will focus on **advanced modeling and extensibility**: -- Code generation layer -- Android Clean Architecture generator -- `.kt` file generation -- Separation between structure and code -- Pluggable code generators +### Planned features -### 🔮 Core V4 — Advanced Modeling - -- Undo / Redo (Memento pattern) +- RepositoryContract editor (visual CRUD modeling) +- Explicit Mapper contracts +- Multi-platform generators (Android / KMP / Backend) - Plugin system -- Multi-platform generators -- Advanced templates -- Migration tooling +- Template customization +- Undo / Redo (Memento pattern) +- Project migrations +- Export profiles + +Core V5 will **not break Core V4 projects**. --- @@ -194,25 +210,16 @@ These are **deliberate design decisions**. ./gradlew :composeApp:run ``` -### Windows installer (MSI) - -```bash -./gradlew packageMsi -``` - --- -## 🤝 For contributors & forks - -This repository represents a **stable Core V2**. - -You can safely: +## 🤝 Final Notes -- build new code generators -- add project migrations -- extend templates -- improve UI/UX -- target new platforms +Core V4 marks a **major milestone**: -> Core V2 is designed to grow — not to be rewritten. +- Architecture is explicit +- Generation is deterministic +- Previews are safe +- Errors are explainable +> AppMakeup is no longer a generator. +> It is an **architecture compiler**. diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index e65739e..fcdc0b3 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -60,6 +60,9 @@ kotlin { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutinesSwing) } + jvmTest.dependencies { + + } } } diff --git a/composeApp/src/commonMain/composeResources/font/abrilFatface_regular.ttf b/composeApp/src/commonMain/composeResources/font/abrilFatface_regular.ttf new file mode 100644 index 0000000..e761f7b Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/abrilFatface_regular.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_black.ttf b/composeApp/src/commonMain/composeResources/font/poppins_black.ttf new file mode 100644 index 0000000..71c0f99 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_black.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_bold.ttf b/composeApp/src/commonMain/composeResources/font/poppins_bold.ttf new file mode 100644 index 0000000..00559ee Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_bold.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_extraBold.ttf b/composeApp/src/commonMain/composeResources/font/poppins_extraBold.ttf new file mode 100644 index 0000000..df70936 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_extraBold.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_italic.ttf b/composeApp/src/commonMain/composeResources/font/poppins_italic.ttf new file mode 100644 index 0000000..12b7b3c Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_italic.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_light.ttf b/composeApp/src/commonMain/composeResources/font/poppins_light.ttf new file mode 100644 index 0000000..bc36bcc Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_light.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_medium.ttf b/composeApp/src/commonMain/composeResources/font/poppins_medium.ttf new file mode 100644 index 0000000..6bcdcc2 Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_medium.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_regular.ttf b/composeApp/src/commonMain/composeResources/font/poppins_regular.ttf new file mode 100644 index 0000000..9f0c71b Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_regular.ttf differ diff --git a/composeApp/src/commonMain/composeResources/font/poppins_semiBold.ttf b/composeApp/src/commonMain/composeResources/font/poppins_semiBold.ttf new file mode 100644 index 0000000..74c726e Binary files /dev/null and b/composeApp/src/commonMain/composeResources/font/poppins_semiBold.ttf differ diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Platform.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Platform.kt index 3269f82..6749e96 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Platform.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Platform.kt @@ -4,4 +4,6 @@ interface Platform { val name: String } -expect fun getPlatform(): Platform \ No newline at end of file +expect fun getPlatform(): Platform + +// \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Test.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Test.kt new file mode 100644 index 0000000..fc4d204 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/Test.kt @@ -0,0 +1,27 @@ +package com.elitec.appmakeup + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import appmakeup.composeapp.generated.resources.Res +import appmakeup.composeapp.generated.resources.compose_multiplatform +import org.jetbrains.compose.resources.painterResource + +@Composable +fun test() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Image( + painter = painterResource(Res.drawable.compose_multiplatform), + modifier = Modifier.size(400.dp), + contentDescription = "" + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/contracts/GenerationResult.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/contracts/GenerationResult.kt index 2ee0a7a..aa5a320 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/contracts/GenerationResult.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/contracts/GenerationResult.kt @@ -1,12 +1,9 @@ package com.elitec.appmakeup.core.v4.contracts sealed class GenerationResult { - object Success : GenerationResult() - - data class Failure( - val reason: String - ) : GenerationResult() + data class Failure(val reason: String) : GenerationResult() + data class Preview(val files: List) : GenerationResult() fun isSuccess(): Boolean = this is Success } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/generators/CompositeLayerGenerator.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/generators/CompositeLayerGenerator.kt new file mode 100644 index 0000000..267ac03 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/generators/CompositeLayerGenerator.kt @@ -0,0 +1,15 @@ +package com.elitec.appmakeup.core.v4.generators + +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.pipeline.GeneratedArtifact +import com.elitec.appmakeup.core.v4.pipeline.LayerGenerator + +class CompositeLayerGenerator( + private val generators: List +) : LayerGenerator { + + override fun generate( + context: GenerationContext + ): List = + generators.flatMap { it.generate(context) } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/DefaultPlanningStage.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/DefaultPlanningStage.kt index 4d64116..8b03776 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/DefaultPlanningStage.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/DefaultPlanningStage.kt @@ -10,21 +10,37 @@ class DefaultPlanningStage( private val mapperContracts: List = emptyList() ) : PlanningStage { + private val tag = "[DefaultPlanningStage]---> " override fun plan(context: GenerationContext): GenerationPlan { + println("$tag GenerationContext received: $context") + val layers = context.feature.layers + println("$tag Layers: $layers") val generateDomain = layers.contains(CoreLayer.DOMAIN) + println("$tag Generate domain: $generateDomain") val generateData = layers.contains(CoreLayer.DATA) + println("$tag Generate data: $generateData") val generatePresentation = layers.contains(CoreLayer.PRESENTATION) + println("$tag Generate presentation: $generatePresentation") val generateRepositories = generateData && repositoryContracts.isNotEmpty() + println("$tag Generate repositories: $generateRepositories") val generateMappers = generateData && mapperContracts.isNotEmpty() + println("$tag Generate mappers: $generateMappers") + + println( + "🧠 Plan => domain=$generateDomain, " + + "data=$generateData, " + + "repo=$generateRepositories, " + + "mapper=$generateMappers" + ) return GenerationPlan( generateDomain = generateDomain, diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/GenerationPipeline.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/GenerationPipeline.kt index 347d785..84b0ea8 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/GenerationPipeline.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/GenerationPipeline.kt @@ -12,9 +12,14 @@ class GenerationPipeline( private val reportingStage: ReportingStage ) { + private val tag = "[GenerationPipeline]---> " fun run(context: GenerationContext): GenerationResult { + println("$tag 🚀 GenerationPipeline.run START for feature: ${context.feature.name}") + val validation = validationStage.validate(context) + println("$tag Validation of GenerationContext: $validation") + if (!validation.isValid()) { return GenerationResult.Failure( (validation as ValidationResult.Invalid).reason @@ -22,10 +27,17 @@ class GenerationPipeline( } val plan = planningStage.plan(context) + println("$tag Plan of GenerationContext: $plan") val artifacts = generationStage.generate(context, plan) - val writeResult = writingStage.write(context.outputPath, artifacts) + println("📦 Artifacts generated: ${artifacts.size}") + + val writeResult = writingStage.write( + outputPath = context.outputPath, + artifacts = artifacts, + context = context + ) if (!writeResult.isSuccess()) return writeResult reportingStage.report(artifacts) diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/PreviewWritingStage.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/PreviewWritingStage.kt new file mode 100644 index 0000000..7c6f3e8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/PreviewWritingStage.kt @@ -0,0 +1,18 @@ +package com.elitec.appmakeup.core.v4.pipeline + +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.contracts.GenerationResult + +class PreviewWritingStage : WritingStage { + + override fun write( + outputPath: String, + artifacts: List, + context: GenerationContext + ): GenerationResult { + + return GenerationResult.Preview( + files = artifacts.map { it.relativePath } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/WritingStage.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/WritingStage.kt index 4e41346..71ff7b0 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/WritingStage.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/WritingStage.kt @@ -1,10 +1,12 @@ package com.elitec.appmakeup.core.v4.pipeline +import com.elitec.appmakeup.core.v4.contracts.GenerationContext import com.elitec.appmakeup.core.v4.contracts.GenerationResult interface WritingStage { fun write( outputPath: String, - artifacts: List + artifacts: List, + context: GenerationContext ): GenerationResult } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/ArchitectureValidator.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/ArchitectureValidator.kt index 65a2c46..6d62375 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/ArchitectureValidator.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/ArchitectureValidator.kt @@ -1,44 +1,44 @@ package com.elitec.appmakeup.core.v4.validation import com.elitec.appmakeup.core.v4.definition.CoreArchitecture +import com.elitec.appmakeup.core.v4.definition.CoreFeature import com.elitec.appmakeup.core.v4.definition.CoreLayer -class ArchitectureValidator : Validator { +class ArchitectureValidator : Validator> { - override fun validate(target: CoreArchitecture): ValidationResult { + override fun validate( + target: Pair + ): ValidationResult { - if (target.supportedLayers.isEmpty()) { - return ValidationResult.Invalid("Architecture must support at least one layer") - } - - target.dependencyRules.forEach { (from, dependencies) -> + val (architecture, feature) = target - if (!target.supportedLayers.contains(from)) { - return ValidationResult.Invalid( - "Dependency rule defined for unsupported layer: $from" - ) - } + // 1️⃣ Layers soportadas + val unsupported = feature.layers - architecture.supportedLayers + if (unsupported.isNotEmpty()) { + return ValidationResult.Invalid( + "Feature '${feature.name}' uses unsupported layers: $unsupported" + ) + } - dependencies.forEach { to -> - if (!target.supportedLayers.contains(to)) { - return ValidationResult.Invalid( - "Layer $from depends on unsupported layer $to" - ) - } + // 2) (Opcional) Asegurar que hay reglas para esas layers + val missingRules = feature.layers.filterNot { architecture.dependencyRules.containsKey(it) }.toSet() + if (missingRules.isNotEmpty()) { + return ValidationResult.Invalid( + "Architecture has no dependency rules for layers: $missingRules" + ) + } - if (from == to) { + /*// 2️⃣ Reglas de dependencia + architecture.dependencyRules.forEach { (layer, allowedDeps) -> + if (feature.layers.contains(layer)) { + val invalidDeps = feature.layers - allowedDeps - layer + if (invalidDeps.isNotEmpty()) { return ValidationResult.Invalid( - "Layer $from cannot depend on itself" + "Layer $layer cannot depend on $invalidDeps" ) } } - } - - if (target.canDependOn(CoreLayer.DOMAIN, CoreLayer.DATA)) { - return ValidationResult.Invalid( - "DOMAIN layer cannot depend on DATA layer" - ) - } + }*/ return ValidationResult.Valid } diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/DefaultValidationStage.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/DefaultValidationStage.kt new file mode 100644 index 0000000..bb35889 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/DefaultValidationStage.kt @@ -0,0 +1,30 @@ +package com.elitec.appmakeup.core.v4.validation + +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.pipeline.ValidationStage + +class DefaultValidationStage( + private val featureValidator: FeatureValidator, + private val architectureValidator: ArchitectureValidator +) : ValidationStage { + + override fun validate(context: GenerationContext): ValidationResult { + + // 1️⃣ Validar feature (incluye entidades) + val featureResult = featureValidator.validate(context.feature) + if (!featureResult.isValid()) return featureResult + + // 2️⃣ Validar arquitectura vs feature + val archResult = architectureValidator.validate( + context.architecture to context.feature + ) + if (!archResult.isValid()) return archResult + + // 3️⃣ Output path básico + if (context.outputPath.isBlank()) { + return ValidationResult.Invalid("Output path cannot be blank") + } + + return ValidationResult.Valid + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/RepositoryContractValidator.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/RepositoryContractValidator.kt index 1cc6790..0c5d3ef 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/RepositoryContractValidator.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/core/v4/validation/RepositoryContractValidator.kt @@ -8,13 +8,11 @@ class RepositoryContractValidator( override fun validate(target: RepositoryContract): ValidationResult { - // 1. Validar entidad asociada + // 1️⃣ Validar entidad val entityResult = entityValidator.validate(target.entity) - if (!entityResult.isValid()) { - return entityResult - } + if (!entityResult.isValid()) return entityResult - // 2. Al menos una operación debe estar soportada + // 2️⃣ Al menos una operación if ( !target.supportsCreate && !target.supportsRead && @@ -26,11 +24,11 @@ class RepositoryContractValidator( ) } - // 3. Operaciones de escritura requieren identificador + // 3️⃣ Operaciones de escritura requieren identificador val hasWriteOperations = target.supportsCreate || target.supportsUpdate || target.supportsDelete - if (hasWriteOperations) { + if (hasWriteOperations && target.entity.identifier == null) { return ValidationResult.Invalid( "Repository with write operations requires entity ${target.entity.name} to have an identifier" ) diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/di/applicationModule.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/di/applicationModule.kt new file mode 100644 index 0000000..b871bd3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/di/applicationModule.kt @@ -0,0 +1,42 @@ +package com.elitec.appmakeup.di + +import com.elitec.appmakeup.generation.GenerateCodeUseCase +import com.elitec.appmakeup.generation.PreviewGenerationPlanUseCase +import com.elitec.appmakeup.projects.usecase.feature.AddFeatureUseCase +import com.elitec.appmakeup.projects.usecase.feature.GetFeatureUseCase +import com.elitec.appmakeup.projects.usecase.feature.ListFeaturesUseCase +import com.elitec.appmakeup.projects.usecase.feature.RemoveFeatureUseCase +import com.elitec.appmakeup.projects.usecase.project.CreateProjectUseCase +import com.elitec.appmakeup.projects.usecase.project.ListRecentProjectsUseCase +import com.elitec.appmakeup.projects.usecase.project.LoadProjectUseCase +import com.elitec.appmakeup.projects.usecase.project.SaveProjectUseCase +import com.elitec.appmakeup.projects.usecase.property.AddPropertyUseCase +import com.elitec.appmakeup.projects.usecase.property.ListPropertiesUseCase +import com.elitec.appmakeup.projects.usecase.property.RemovePropertyUseCase +import org.koin.dsl.module + +val applicationModule = module { + + // Project lifecycle + factory { CreateProjectUseCase(get(), get()) } + factory { LoadProjectUseCase(get(), get()) } + factory { SaveProjectUseCase(get()) } + + // Features + factory { ListFeaturesUseCase() } + factory { GetFeatureUseCase() } + factory { AddFeatureUseCase(get()) } + factory { RemoveFeatureUseCase(get()) } + + // Properties + factory { ListPropertiesUseCase() } + factory { AddPropertyUseCase(get()) } + factory { RemovePropertyUseCase(get()) } + + // Recents + factory { ListRecentProjectsUseCase(get()) } + + // Generation + factory { GenerateCodeUseCase(get()) } + factory { PreviewGenerationPlanUseCase(get()) } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/di/presentationModule.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/di/presentationModule.kt new file mode 100644 index 0000000..1366fa3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/di/presentationModule.kt @@ -0,0 +1,43 @@ +package com.elitec.appmakeup.di + +import com.elitec.appmakeup.presentation.viewmodels.CreateProjectViewModel +import com.elitec.appmakeup.presentation.viewmodels.ExportViewModel +import com.elitec.appmakeup.presentation.viewmodels.HomeViewModel +import com.elitec.appmakeup.presentation.viewmodels.ProjectEditorViewModel +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module + +val presentationModule = module { + + viewModel { + HomeViewModel( + listRecentProjectsUseCase = get() + ) + } + + viewModel { + CreateProjectViewModel( + createProjectUseCase = get() + ) + } + + viewModel { + ProjectEditorViewModel( + loadProjectUseCase = get(), + saveProjectUseCase = get(), + addFeatureUseCase = get(), + removeFeatureUseCase = get(), + addPropertyUseCase = get(), + removePropertyUseCase = get(), + generateCodeUseCase = get() + ) + } + + viewModel { + ExportViewModel( + loadProjectUseCase = get(), + generateCodeUseCase = get(), + previewGenerationPlanUseCase = get() + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/CodeGenerator.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/CodeGenerator.kt index daf4746..72d5033 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/CodeGenerator.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/CodeGenerator.kt @@ -1,6 +1,10 @@ package com.elitec.appmakeup.generation +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.projects.model.AppMakeupProject + interface CodeGenerator { - fun generate(intent: CodeGenerationIntent) + fun generate(intent: CodeGenerationIntent,dryRun: Boolean): GenerationResult + fun preview(project: AppMakeupProject): List } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/GenerateCodeUseCase.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/GenerateCodeUseCase.kt index e168b68..a73229e 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/GenerateCodeUseCase.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/GenerateCodeUseCase.kt @@ -1,22 +1,31 @@ package com.elitec.appmakeup.generation +import com.elitec.appmakeup.core.v4.contracts.GenerationResult import com.elitec.appmakeup.projects.model.AppMakeupProject class GenerateCodeUseCase( private val codeGenerator: CodeGenerator ) { - fun execute(project: AppMakeupProject) { + private val tag = "[GenerateCodeUseCase]" + fun execute(project: AppMakeupProject, dryRun: Boolean): GenerationResult { + println("$tag Init executing Generation intent") require(project.features.isNotEmpty()) { "Project must have at least one feature to generate code" } val intent = CodeGenerationIntent( project = project, - exportPath = project.exportPath + exportPath = "${project.path}/export" ) - codeGenerator.generate(intent) + + println("$tag make generation intent: $intent") + return codeGenerator.generate(intent, dryRun) + } + + fun preview(project: AppMakeupProject): List { + return codeGenerator.preview(project) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/PreviewGenerationPlanUseCase.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/PreviewGenerationPlanUseCase.kt new file mode 100644 index 0000000..329aa52 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/generation/PreviewGenerationPlanUseCase.kt @@ -0,0 +1,13 @@ +package com.elitec.appmakeup.generation + +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.pipeline.GenerationPlan +import com.elitec.appmakeup.core.v4.pipeline.PlanningStage + +class PreviewGenerationPlanUseCase( + private val planningStage: PlanningStage +) { + + fun execute(context: GenerationContext): GenerationPlan = + planningStage.plan(context) +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/logs/Logger.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/logs/Logger.kt new file mode 100644 index 0000000..ead1608 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/logs/Logger.kt @@ -0,0 +1,15 @@ +package com.elitec.appmakeup.logs + +object Logger { + fun error(tag: String? = "", message: String, error: Throwable) { + println("[$tag] ----> ❌ ERROR___ $message \n Cause: $error") + } + + fun success(tag: String? = "", message: String) { + println("[$tag] ----> ✅ $message") + } + + fun warning(tag: String? = "", message: String) { + println("[$tag] ----> ⚠️⚠ $message") + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/navigation/MainNavigationWrapper.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/navigation/MainNavigationWrapper.kt new file mode 100644 index 0000000..7a3b19c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/navigation/MainNavigationWrapper.kt @@ -0,0 +1,76 @@ +package com.elitec.appmakeup.presentation.navigation + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import com.elitec.appmakeup.presentation.screens.CreateProjectScreen +import com.elitec.appmakeup.presentation.screens.ExportScreen +import com.elitec.appmakeup.presentation.screens.HomeScreen +import com.elitec.appmakeup.presentation.screens.ProjectEditorScreen +import com.elitec.appmakeup.presentation.screens.SplashScreen +import com.elitec.appmakeup.presentation.theme.onBackgroundDark + +@Composable +fun MainNavigationWrapper( + onAppReady: () -> Unit, + modifier: Modifier = Modifier +) { + val navController = rememberNavController() + + NavHost( + navController = navController, + startDestination = MainScreens.Splash, + modifier = modifier.fillMaxSize() + ) { + composable { + SplashScreen( + navigateTo = { destination -> + onAppReady() + navController.navigate(MainScreens.Home) { + popUpTo(MainScreens.Splash) { inclusive = true } + } + } + ) + } + composable { + CreateProjectScreen( + onProjectCreate = { path -> + navController.navigate(MainScreens.ProjectEditor(path)) { + popUpTo(MainScreens.CreateProject) { inclusive = true } + } + } + ) + } + composable { + HomeScreen( + onProjectCreate = { + navController.navigate(MainScreens.CreateProject) + }, + onOpenProject = { path -> + navController.navigate(MainScreens.ProjectEditor(path)) + } + ) + } + composable { backStackEntry -> + val path = backStackEntry.toRoute().path + + ProjectEditorScreen( + projectPath = path, + onNavigateToExport = { + navController.navigate(MainScreens.Export(path)) + } + ) + } + composable { backStackEntry -> + val path = backStackEntry.toRoute().path + ExportScreen( + projectPath = path, + onBack = { navController.popBackStack() } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/navigation/MainScreens.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/navigation/MainScreens.kt new file mode 100644 index 0000000..1543bf1 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/navigation/MainScreens.kt @@ -0,0 +1,12 @@ +package com.elitec.appmakeup.presentation.navigation + +import kotlinx.serialization.Serializable + +@Serializable +sealed interface MainScreens { + @Serializable object Splash: MainScreens + @Serializable object CreateProject: MainScreens + @Serializable object Home: MainScreens + @Serializable data class ProjectEditor(val path: String): MainScreens + @Serializable data class Export(val path: String): MainScreens +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/CreateProjectScreen.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/CreateProjectScreen.kt new file mode 100644 index 0000000..f7ae246 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/CreateProjectScreen.kt @@ -0,0 +1,75 @@ +package com.elitec.appmakeup.presentation.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.elitec.appmakeup.presentation.util.pickDirectory +import com.elitec.appmakeup.presentation.viewmodels.CreateProjectViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun CreateProjectScreen( + onProjectCreate: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: CreateProjectViewModel = koinViewModel() +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + var domain by remember { mutableStateOf("com.mycompany.") } + + if (state.isCreated) { + onProjectCreate(state.path) + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text("Crear nuevo proyecto", style = MaterialTheme.typography.headlineSmall) + + OutlinedTextField( + value = state.name, + onValueChange = viewModel::onNameChange, + label = { Text("Nombre del proyecto") }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = state.packageName, + onValueChange = viewModel::onPackageChange, + label = { Text("Package base") }, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedTextField( + value = state.path, + onValueChange = viewModel::onPathChange, + label = { Text("Ruta del proyecto") }, + modifier = Modifier.fillMaxWidth(), + trailingIcon = { + Button( + onClick = { + pickDirectory()?.let { viewModel.onPathChange(it) } + } + ) { + Text("Elegir") + } + } + ) + + state.error?.let { + Text(it, color = MaterialTheme.colorScheme.error) + } + + Button( + onClick = viewModel::createProject, + modifier = Modifier.fillMaxWidth() + ) { + Text("Crear proyecto") + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/ExportScreen.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/ExportScreen.kt new file mode 100644 index 0000000..6848148 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/ExportScreen.kt @@ -0,0 +1,153 @@ +package com.elitec.appmakeup.presentation.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.elitec.appmakeup.presentation.screens.components.GenerationPlanPreview +import com.elitec.appmakeup.presentation.screens.components.PreviewFilesList +import com.elitec.appmakeup.presentation.viewmodels.ExportViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun ExportScreen( + projectPath: String, + onBack: () -> Unit, + viewModel: ExportViewModel = koinViewModel() +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + val previewState by viewModel.uiPreviewState.collectAsStateWithLifecycle() + + LaunchedEffect(projectPath) { + viewModel.loadProject(projectPath) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + + /* --------------------------- + * Header + * --------------------------- */ + + Text( + text = "Export project", + style = MaterialTheme.typography.headlineSmall + ) + + state.project?.let { project -> + Text("Project: ${project.name}") + Text("Package: ${project.packageName}") + Text("Features: ${project.features.size}") + } + + Divider() + + /* --------------------------- + * Options + * --------------------------- */ + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox( + checked = state.dryRun, + onCheckedChange = viewModel::toggleDryRun + ) + Spacer(Modifier.width(8.dp)) + Text("Dry-run (do not write files)") + } + + Divider() + + /* --------------------------- + * Preview: Generation plan + * --------------------------- */ + + Text( + text = "Generation plan", + style = MaterialTheme.typography.titleMedium + ) + + GenerationPlanPreview(previewState) + + Divider() + + /* --------------------------- + * Preview: Files (dry-run) + * --------------------------- */ + + if (previewState.previewFiles.isNotEmpty()) { + Text( + text = "Files preview", + style = MaterialTheme.typography.titleMedium + ) + + PreviewFilesList(previewState.previewFiles) + } + + Spacer(Modifier.height(8.dp)) + + /* --------------------------- + * Actions + * --------------------------- */ + + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { + + Button( + onClick = viewModel::runPreview, + enabled = state.project != null && !state.isRunning + ) { + Text("Preview") + } + + Button( + onClick = viewModel::runExport, + enabled = state.project != null && !state.isRunning + ) { + Text(if (state.dryRun) "Run dry-run" else "Export") + } + } + + if (state.isRunning) { + CircularProgressIndicator() + } + + state.result?.let { + Text(it, color = MaterialTheme.colorScheme.primary) + } + + state.error?.let { + Text(it, color = MaterialTheme.colorScheme.error) + } + + Spacer(Modifier.weight(1f)) + + OutlinedButton(onClick = onBack) { + Text("Back") + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/HomeScreen.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/HomeScreen.kt new file mode 100644 index 0000000..81bfa62 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/HomeScreen.kt @@ -0,0 +1,96 @@ +package com.elitec.appmakeup.presentation.screens + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.Divider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.elitec.appmakeup.presentation.viewmodels.HomeViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Suppress("EffectKeys") +@Composable +fun HomeScreen( + onProjectCreate: () -> Unit, + onOpenProject: (String) -> Unit, + modifier: Modifier = Modifier, + viewModel: HomeViewModel = koinViewModel() +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + val lazyColumnState = rememberLazyListState() + + LaunchedEffect(Unit) { + viewModel.loadRecentProjects() + } + + Column( + modifier = modifier + .fillMaxSize() + .padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text("AppMakeup", style = MaterialTheme.typography.headlineMedium) + + Button(onClick = onProjectCreate) { + Text("Nuevo proyecto") + } + + Divider() + + Text("Proyectos recientes", style = MaterialTheme.typography.titleMedium) + + AnimatedVisibility( + visible = state.recentProjects.isEmpty() + ) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Text("No hay proyectos recientes") + } + } + + AnimatedVisibility( + visible = state.recentProjects.isNotEmpty() + ) { + Surface( + color = MaterialTheme.colorScheme.surface, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(10.dp) + ) { + LazyColumn { + items(state.recentProjects) { path -> + Text( + text = path, + modifier = Modifier + .fillMaxWidth() + .clickable { onOpenProject(path) } + .padding(8.dp) + ) + } + } + } + } + + state.error?.let { + Text(it, color = MaterialTheme.colorScheme.error) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/ProjectEditorScreen.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/ProjectEditorScreen.kt new file mode 100644 index 0000000..5f520a7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/ProjectEditorScreen.kt @@ -0,0 +1,72 @@ +package com.elitec.appmakeup.presentation.screens + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.elitec.appmakeup.presentation.screens.components.EditorContent +import com.elitec.appmakeup.presentation.screens.components.ErrorView +import com.elitec.appmakeup.presentation.viewmodels.ProjectEditorViewModel +import org.koin.compose.viewmodel.koinViewModel + +@Composable +fun ProjectEditorScreen( + projectPath: String, + onNavigateToExport: () -> Unit, + viewModel: ProjectEditorViewModel = koinViewModel() +) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + + // Cargar proyecto una sola vez + LaunchedEffect(projectPath) { + viewModel.loadProject(projectPath) + } + + when { + state.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + + state.error != null -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = state.error ?: "Unknown error", + color = MaterialTheme.colorScheme.error + ) + } + } + + else -> { + EditorContent( + state = state, + onSelectFeature = viewModel::selectFeature, + onAddFeature = viewModel::addFeature, + onRemoveFeature = viewModel::removeFeature, + onAddProperty = viewModel::addProperty, + onRemoveProperty = viewModel::removeProperty, + onExport = { + viewModel.export() + if (state.canExport) { + onNavigateToExport() + } + } + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/SplashScreen.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/SplashScreen.kt new file mode 100644 index 0000000..4f66d7b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/SplashScreen.kt @@ -0,0 +1,37 @@ +package com.elitec.appmakeup.presentation.screens + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import appmakeup.composeapp.generated.resources.Res +import appmakeup.composeapp.generated.resources.sinfoto +import com.elitec.appmakeup.presentation.navigation.MainScreens +import kotlinx.coroutines.delay +import org.jetbrains.compose.resources.painterResource + +@Composable +fun SplashScreen( + navigateTo: (MainScreens) -> Unit, + modifier: Modifier = Modifier +) { + LaunchedEffect(null) { + delay(2000) + navigateTo(MainScreens.Home) + } + Box( + contentAlignment = Alignment.Center, + modifier = modifier.fillMaxSize() + ) { + Image( + painter = painterResource(Res.drawable.sinfoto), + contentDescription = "", + modifier = Modifier.size(200.dp) + ) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/AddFeatureInput.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/AddFeatureInput.kt new file mode 100644 index 0000000..94f964f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/AddFeatureInput.kt @@ -0,0 +1,40 @@ +package com.elitec.appmakeup.presentation.screens.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun AddFeatureInput(onAddFeature: (String) -> Unit) { + var name by remember { mutableStateOf("") } + + Row { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("New feature") }, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(8.dp)) + Button( + onClick = { + if (name.isNotBlank()) { + onAddFeature(name) + name = "" + } + } + ) { + Text("Add") + } + } +} diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/AddPropertyInput.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/AddPropertyInput.kt new file mode 100644 index 0000000..a634f42 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/AddPropertyInput.kt @@ -0,0 +1,55 @@ +package com.elitec.appmakeup.presentation.screens.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Button +import androidx.compose.material3.Checkbox +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment + +@Composable +fun AddPropertyInput( + featureName: String, + onAddProperty: (String, String, String, Boolean) -> Unit +) { + var name by remember { mutableStateOf("") } + var type by remember { mutableStateOf("String") } + var isId by remember { mutableStateOf(false) } + + Column { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Property name") } + ) + + OutlinedTextField( + value = type, + onValueChange = { type = it }, + label = { Text("Type") } + ) + + Row(verticalAlignment = Alignment.CenterVertically) { + Checkbox(checked = isId, onCheckedChange = { isId = it }) + Text("Identifier") + } + + Button( + onClick = { + if (name.isNotBlank()) { + onAddProperty(featureName, name, type, isId) + name = "" + isId = false + } + } + ) { + Text("Add property") + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/EditorContent.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/EditorContent.kt new file mode 100644 index 0000000..a6459ac --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/EditorContent.kt @@ -0,0 +1,119 @@ +package com.elitec.appmakeup.presentation.screens.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.elitec.appmakeup.presentation.states.ProjectEditorUiState + +@Composable +fun EditorContent( + state: ProjectEditorUiState, + onSelectFeature: (String) -> Unit, + onAddFeature: (String) -> Unit, + onRemoveFeature: (String) -> Unit, + onAddProperty: (String, String, String, Boolean) -> Unit, + onRemoveProperty: (String, String) -> Unit, + onExport: () -> Unit +) { + Row(modifier = Modifier.fillMaxSize().padding(16.dp)) { + + /* --------------------------- + * Features + * --------------------------- */ + Column(modifier = Modifier.weight(0.3f)) { + Text("Features", style = MaterialTheme.typography.titleMedium) + + state.features.forEach { feature -> + Row( + modifier = Modifier.fillMaxWidth() + .clickable { onSelectFeature(feature.name) } + .padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(feature.name, modifier = Modifier.weight(1f)) + IconButton(onClick = { onRemoveFeature(feature.name) }) { + Icon(Icons.Default.Delete, contentDescription = null) + } + } + } + + Spacer(Modifier.height(8.dp)) + AddFeatureInput(onAddFeature) + } + + Spacer(Modifier.width(16.dp)) + + /* --------------------------- + * Feature detail + * --------------------------- */ + Column(modifier = Modifier.weight(0.7f)) { + state.selectedFeature?.let { feature -> + Text( + "Feature: ${feature.name}", + style = MaterialTheme.typography.titleMedium + ) + + feature.properties.forEach { prop -> + Row( + modifier = Modifier.fillMaxWidth().padding(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "${prop.name}: ${prop.type}" + + if (prop.isIdentifier) " (ID)" else "", + modifier = Modifier.weight(1f) + ) + + IconButton( + onClick = { + onRemoveProperty(feature.name, prop.name) + } + ) { + Icon(Icons.Default.Delete, contentDescription = null) + } + } + } + + Spacer(Modifier.height(8.dp)) + AddPropertyInput(feature.name, onAddProperty) + } + + Spacer(Modifier.height(16.dp)) + + /* --------------------------- + * Validation + * --------------------------- */ + if (state.validationErrors.isNotEmpty()) { + state.validationErrors.forEach { + Text(it, color = MaterialTheme.colorScheme.error) + } + } + + Spacer(Modifier.height(16.dp)) + + Button( + onClick = onExport, + enabled = state.canExport + ) { + Text("Export project") + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/ErrorView.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/ErrorView.kt new file mode 100644 index 0000000..061c6b8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/ErrorView.kt @@ -0,0 +1,32 @@ +package com.elitec.appmakeup.presentation.screens.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ErrorView( + message: String, + onBack: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text(message, color = MaterialTheme.colorScheme.error) + Spacer(Modifier.height(16.dp)) + Button(onClick = onBack) { + Text("Back") + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/GenerationPlanPreview.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/GenerationPlanPreview.kt new file mode 100644 index 0000000..1b78e9d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/GenerationPlanPreview.kt @@ -0,0 +1,46 @@ +package com.elitec.appmakeup.presentation.screens.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.elitec.appmakeup.presentation.states.GenerationPreviewUiState + +@Composable +fun GenerationPlanPreview(state: GenerationPreviewUiState) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + PlanItem("Domain layer", state.generateDomain) + PlanItem("Data layer", state.generateData) + PlanItem("Repositories", state.generateRepositories) + PlanItem("Mappers", state.generateMappers) + } +} + +@Composable +private fun PlanItem(label: String, enabled: Boolean) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = + if (enabled) Icons.Default.Check else Icons.Default.Close, + contentDescription = null, + tint = + if (enabled) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(Modifier.width(8.dp)) + Text(label) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/PreviewFilesList.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/PreviewFilesList.kt new file mode 100644 index 0000000..b5c599d --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/screens/components/PreviewFilesList.kt @@ -0,0 +1,30 @@ +package com.elitec.appmakeup.presentation.screens.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun PreviewFilesList(files: List) { + Column( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 200.dp) + .padding(start = 8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + files.forEach { file -> + Text( + text = "• $file", + style = MaterialTheme.typography.bodySmall + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/CreateProjectUiState.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/CreateProjectUiState.kt new file mode 100644 index 0000000..c120f93 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/CreateProjectUiState.kt @@ -0,0 +1,9 @@ +package com.elitec.appmakeup.presentation.states + +data class CreateProjectUiState( + val name: String = "", + val packageName: String = "com.company.$name", + val path: String = "", + val error: String? = null, + val isCreated: Boolean = false +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ExportUiState.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ExportUiState.kt new file mode 100644 index 0000000..f786bfb --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ExportUiState.kt @@ -0,0 +1,11 @@ +package com.elitec.appmakeup.presentation.states + +import com.elitec.appmakeup.projects.model.AppMakeupProject + +data class ExportUiState( + val project: AppMakeupProject? = null, + val dryRun: Boolean = true, + val isRunning: Boolean = false, + val result: String? = null, + val error: String? = null +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/GenerationPreviewUiState.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/GenerationPreviewUiState.kt new file mode 100644 index 0000000..5ea4fca --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/GenerationPreviewUiState.kt @@ -0,0 +1,9 @@ +package com.elitec.appmakeup.presentation.states + +data class GenerationPreviewUiState( + val generateDomain: Boolean = false, + val generateData: Boolean = false, + val generateRepositories: Boolean = false, + val generateMappers: Boolean = false, + val previewFiles: List = emptyList() +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/HomeUiState.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/HomeUiState.kt new file mode 100644 index 0000000..816f65c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/HomeUiState.kt @@ -0,0 +1,6 @@ +package com.elitec.appmakeup.presentation.states + +data class HomeUiState( + val recentProjects: List = emptyList(), + val error: String? = null +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ProjectEditorState.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ProjectEditorState.kt new file mode 100644 index 0000000..e289a82 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ProjectEditorState.kt @@ -0,0 +1,11 @@ +package com.elitec.appmakeup.presentation.states + +import com.elitec.appmakeup.projects.model.AppFeature +import com.elitec.appmakeup.projects.model.AppMakeupProject + +data class ProjectEditorState( + val project: AppMakeupProject, + val selectedFeature: AppFeature? = null, + val isExporting: Boolean = false, + val error: String? = null +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ProjectEditorUiState.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ProjectEditorUiState.kt new file mode 100644 index 0000000..042ed99 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/states/ProjectEditorUiState.kt @@ -0,0 +1,16 @@ +package com.elitec.appmakeup.presentation.states + +import com.elitec.appmakeup.core.v4.definition.CoreEntity +import com.elitec.appmakeup.projects.model.AppFeature +import com.elitec.appmakeup.projects.model.AppMakeupProject +import com.elitec.appmakeup.projects.model.AppProperty + +data class ProjectEditorUiState( + val project: AppMakeupProject? = null, + val features: List = emptyList(), + val selectedFeature: AppFeature? = null, + val validationErrors: List = emptyList(), + val isLoading: Boolean = false, + val canExport: Boolean = false, + val error: String? = null +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Color.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Color.kt new file mode 100644 index 0000000..5011e11 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Color.kt @@ -0,0 +1,226 @@ +package com.elitec.appmakeup.presentation.theme + +import androidx.compose.ui.graphics.Color + +val primaryLight = Color(0xFF636117) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFEAE68E) +val onPrimaryContainerLight = Color(0xFF4B4900) +val secondaryLight = Color(0xFF616042) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFE8E4BF) +val onSecondaryContainerLight = Color(0xFF49482C) +val tertiaryLight = Color(0xFF3E6655) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFC0ECD7) +val onTertiaryContainerLight = Color(0xFF264E3F) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFFDF9EC) +val onBackgroundLight = Color(0xFF1C1C14) +val surfaceLight = Color(0xFFFDF9EC) +val onSurfaceLight = Color(0xFF1C1C14) +val surfaceVariantLight = Color(0xFFE6E3D1) +val onSurfaceVariantLight = Color(0xFF48473A) +val outlineLight = Color(0xFF797769) +val outlineVariantLight = Color(0xFFCAC7B6) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF323128) +val inverseOnSurfaceLight = Color(0xFFF5F1E3) +val inversePrimaryLight = Color(0xFFCECA75) +val surfaceDimLight = Color(0xFFDEDACD) +val surfaceBrightLight = Color(0xFFFDF9EC) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF8F4E6) +val surfaceContainerLight = Color(0xFFF2EEE0) +val surfaceContainerHighLight = Color(0xFFECE8DB) +val surfaceContainerHighestLight = Color(0xFFE6E2D5) + +val primaryLightMediumContrast = Color(0xFF393800) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF727025) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF38371D) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF706E50) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF143D2E) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF4D7564) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF740006) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFCF2C27) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFFDF9EC) +val onBackgroundLightMediumContrast = Color(0xFF1C1C14) +val surfaceLightMediumContrast = Color(0xFFFDF9EC) +val onSurfaceLightMediumContrast = Color(0xFF12110A) +val surfaceVariantLightMediumContrast = Color(0xFFE6E3D1) +val onSurfaceVariantLightMediumContrast = Color(0xFF38372A) +val outlineLightMediumContrast = Color(0xFF545345) +val outlineVariantLightMediumContrast = Color(0xFF6F6D5F) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF323128) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF5F1E3) +val inversePrimaryLightMediumContrast = Color(0xFFCECA75) +val surfaceDimLightMediumContrast = Color(0xFFCAC7BA) +val surfaceBrightLightMediumContrast = Color(0xFFFDF9EC) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF8F4E6) +val surfaceContainerLightMediumContrast = Color(0xFFECE8DB) +val surfaceContainerHighLightMediumContrast = Color(0xFFE0DDD0) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD5D2C5) + +val primaryLightHighContrast = Color(0xFF2F2E00) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF4D4B00) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF2E2D14) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF4C4A2F) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF063325) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF285141) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF600004) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF98000A) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFFDF9EC) +val onBackgroundLightHighContrast = Color(0xFF1C1C14) +val surfaceLightHighContrast = Color(0xFFFDF9EC) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFE6E3D1) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF2D2D21) +val outlineVariantLightHighContrast = Color(0xFF4B4A3C) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF323128) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFCECA75) +val surfaceDimLightHighContrast = Color(0xFFBCB9AC) +val surfaceBrightLightHighContrast = Color(0xFFFDF9EC) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF5F1E3) +val surfaceContainerLightHighContrast = Color(0xFFE6E2D5) +val surfaceContainerHighLightHighContrast = Color(0xFFD8D4C7) +val surfaceContainerHighestLightHighContrast = Color(0xFFCAC7BA) + +val primaryDark = Color(0xFFCECA75) +val onPrimaryDark = Color(0xFF333200) +val primaryContainerDark = Color(0xFF4B4900) +val onPrimaryContainerDark = Color(0xFFEAE68E) +val secondaryDark = Color(0xFFCBC8A4) +val onSecondaryDark = Color(0xFF323118) +val secondaryContainerDark = Color(0xFF49482C) +val onSecondaryContainerDark = Color(0xFFE8E4BF) +val tertiaryDark = Color(0xFFA5D0BB) +val onTertiaryDark = Color(0xFF0C3729) +val tertiaryContainerDark = Color(0xFF264E3F) +val onTertiaryContainerDark = Color(0xFFC0ECD7) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF14140C) +val onBackgroundDark = Color(0xFFE6E2D5) +val surfaceDark = Color(0xFF14140C) +val onSurfaceDark = Color(0xFFE6E2D5) +val surfaceVariantDark = Color(0xFF48473A) +val onSurfaceVariantDark = Color(0xFFCAC7B6) +val outlineDark = Color(0xFF949181) +val outlineVariantDark = Color(0xFF48473A) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE6E2D5) +val inverseOnSurfaceDark = Color(0xFF323128) +val inversePrimaryDark = Color(0xFF636117) +val surfaceDimDark = Color(0xFF14140C) +val surfaceBrightDark = Color(0xFF3B3930) +val surfaceContainerLowestDark = Color(0xFF0F0E07) +val surfaceContainerLowDark = Color(0xFF1C1C14) +val surfaceContainerDark = Color(0xFF212018) +val surfaceContainerHighDark = Color(0xFF2B2A22) +val surfaceContainerHighestDark = Color(0xFF36352C) + +val primaryDarkMediumContrast = Color(0xFFE4E088) +val onPrimaryDarkMediumContrast = Color(0xFF282700) +val primaryContainerDarkMediumContrast = Color(0xFF979445) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFE1DEB9) +val onSecondaryDarkMediumContrast = Color(0xFF27270E) +val secondaryContainerDarkMediumContrast = Color(0xFF949271) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFBAE6D1) +val onTertiaryDarkMediumContrast = Color(0xFF002C1E) +val tertiaryContainerDarkMediumContrast = Color(0xFF709A87) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFD2CC) +val onErrorDarkMediumContrast = Color(0xFF540003) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF14140C) +val onBackgroundDarkMediumContrast = Color(0xFFE6E2D5) +val surfaceDarkMediumContrast = Color(0xFF14140C) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF48473A) +val onSurfaceVariantDarkMediumContrast = Color(0xFFE0DDCB) +val outlineDarkMediumContrast = Color(0xFFB5B2A2) +val outlineVariantDarkMediumContrast = Color(0xFF939181) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE6E2D5) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF2B2A22) +val inversePrimaryDarkMediumContrast = Color(0xFF4C4A00) +val surfaceDimDarkMediumContrast = Color(0xFF14140C) +val surfaceBrightDarkMediumContrast = Color(0xFF46453B) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF080803) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1F1E16) +val surfaceContainerDarkMediumContrast = Color(0xFF292820) +val surfaceContainerHighDarkMediumContrast = Color(0xFF34332A) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3F3E35) + +val primaryDarkHighContrast = Color(0xFFF8F49A) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFCAC671) +val onPrimaryContainerDarkHighContrast = Color(0xFF0C0C00) +val secondaryDarkHighContrast = Color(0xFFF5F1CB) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFC7C4A0) +val onSecondaryContainerDarkHighContrast = Color(0xFF0C0C00) +val tertiaryDarkHighContrast = Color(0xFFCDFAE4) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFA1CCB8) +val onTertiaryContainerDarkHighContrast = Color(0xFF000E08) +val errorDarkHighContrast = Color(0xFFFFECE9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFAEA4) +val onErrorContainerDarkHighContrast = Color(0xFF220001) +val backgroundDarkHighContrast = Color(0xFF14140C) +val onBackgroundDarkHighContrast = Color(0xFFE6E2D5) +val surfaceDarkHighContrast = Color(0xFF14140C) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF48473A) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFF4F0DE) +val outlineVariantDarkHighContrast = Color(0xFFC6C3B2) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE6E2D5) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF4C4A00) +val surfaceDimDarkHighContrast = Color(0xFF14140C) +val surfaceBrightDarkHighContrast = Color(0xFF525046) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF212018) +val surfaceContainerDarkHighContrast = Color(0xFF323128) +val surfaceContainerHighDarkHighContrast = Color(0xFF3D3C32) +val surfaceContainerHighestDarkHighContrast = Color(0xFF48473D) + + + + + + + diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Theme.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Theme.kt new file mode 100644 index 0000000..f1c8035 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Theme.kt @@ -0,0 +1,98 @@ +package com.elitec.appmakeup.presentation.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +// Colores para el tema oscuro +val DarkColorScheme = darkColorScheme( + primary = Color(0xFF5B88B2), // Azul medio (base) + onPrimary = Color(0xFFE6E6E6), + primaryContainer = Color(0xFF3A5E7E), // Azul oscuro para contraste + onPrimaryContainer = Color(0xFFD6E3F3), + + secondary = Color(0xFF4E6E8F), // Azul grisáceo + onSecondary = Color(0xFFE6E6E6), + secondaryContainer = Color(0xFF39516F), + onSecondaryContainer = Color(0xFFD8E3F0), + + tertiary = Color(0xFF8C4A6F), // Magenta oscuro + onTertiary = Color(0xFFE6E6E6), + tertiaryContainer = Color(0xFF6A3857), + onTertiaryContainer = Color(0xFFF3D5E6), + + background = Color(0xFF121212), // Negro puro + onBackground = Color(0xFFE6E6E6), + + surface = Color(0xFF1E252F), // Gris azulado oscuro + onSurface = Color(0xFFE6E6E6), + surfaceVariant = Color(0xFF2A323E), + onSurfaceVariant = Color(0xFFB0B0B0), + + error = Color(0xFFCF6679), // Rojo suave + onError = Color(0xFF121212), + errorContainer = Color(0xFFB00020), + onErrorContainer = Color(0xFFE6E6E6) +) + +val LightColorScheme = lightColorScheme( + primary = Color(0xFF5B88B2), // Azul medio (color base) + onPrimary = Color(0xFFFFFFFF), // Blanco para contraste + primaryContainer = Color(0xFFD5E3F3), // Azul muy claro (fondo de contenedores) + onPrimaryContainer = Color(0xFF0F1D2B), + + secondary = Color(0xFF6B94B5), // Azul-grisáceo secundario + onSecondary = Color(0xFFFFFFFF), + secondaryContainer = Color(0xFFD8E3F0), + onSecondaryContainer = Color(0xFF172430), + + tertiary = Color(0xFFB25B8E), // Magenta suave de apoyo + onTertiary = Color(0xFFFFFFFF), + tertiaryContainer = Color(0xFFF3D5E6), + onTertiaryContainer = Color(0xFF2B0F1D), + + background = Color(0xFFFFFFFF), // Blanco puro + onBackground = Color(0xFF1C2526), + + surface = Color(0xFFFAFAFA), // Gris muy claro + onSurface = Color(0xFF1C2526), + surfaceVariant = Color(0xFFE1E7ED), // Gris-azulado suave + onSurfaceVariant = Color(0xFF454F57), + + error = Color(0xFFB00020), // Rojo brillante + onError = Color(0xFFFFFFFF), + errorContainer = Color(0xFFFCDADA), + onErrorContainer = Color(0xFF410002) +) + +@Immutable +data class ColorFamily( + val color: Color, + val onColor: Color, + val colorContainer: Color, + val onColorContainer: Color +) + +val unspecified_scheme = ColorFamily( + Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified +) + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + content: @Composable() () -> Unit +) { + val colorScheme = if (darkTheme) { DarkColorScheme } else { LightColorScheme } + + MaterialTheme( + colorScheme = colorScheme, + typography = AppTypography, + content = content + ) +} + diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Type.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Type.kt new file mode 100644 index 0000000..ad84042 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/theme/Type.kt @@ -0,0 +1,36 @@ +package com.elitec.appmakeup.presentation.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import appmakeup.composeapp.generated.resources.Res +import appmakeup.composeapp.generated.resources.abrilFatface_regular +import appmakeup.composeapp.generated.resources.poppins_black +import appmakeup.composeapp.generated.resources.poppins_bold +import appmakeup.composeapp.generated.resources.poppins_extraBold +import appmakeup.composeapp.generated.resources.poppins_italic +import appmakeup.composeapp.generated.resources.poppins_light +import appmakeup.composeapp.generated.resources.poppins_medium +import appmakeup.composeapp.generated.resources.poppins_regular +import appmakeup.composeapp.generated.resources.poppins_semiBold +import org.jetbrains.compose.resources.Font + +val AppTypography = Typography() + +@Composable +fun abrilFatfaceFontFamily() = FontFamily( + Font(Res.font.abrilFatface_regular, weight = FontWeight.Normal) +) + +@Composable +fun poppinsFontFamily() = FontFamily( + Font(Res.font.poppins_regular, weight = FontWeight.Normal), + Font(Res.font.poppins_italic, weight = FontWeight.Medium), + Font(Res.font.poppins_medium, weight = FontWeight.Medium), + Font(Res.font.poppins_bold, weight = FontWeight.Bold), + Font(Res.font.poppins_extraBold, weight = FontWeight.ExtraBold), + Font(Res.font.poppins_light, weight = FontWeight.Light), + Font(Res.font.poppins_semiBold, weight = FontWeight.SemiBold), + Font(Res.font.poppins_black, weight = FontWeight.Black), +) diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/util/pickDirectory.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/util/pickDirectory.kt new file mode 100644 index 0000000..788c0ad --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/util/pickDirectory.kt @@ -0,0 +1,3 @@ +package com.elitec.appmakeup.presentation.util + +expect fun pickDirectory(): String? \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/CreateProjectViewModel.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/CreateProjectViewModel.kt new file mode 100644 index 0000000..a075364 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/CreateProjectViewModel.kt @@ -0,0 +1,52 @@ +package com.elitec.appmakeup.presentation.viewmodels + +import androidx.lifecycle.ViewModel +import com.elitec.appmakeup.presentation.states.CreateProjectUiState +import com.elitec.appmakeup.projects.model.AppMakeupProject +import com.elitec.appmakeup.projects.usecase.project.CreateProjectUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class CreateProjectViewModel( + private val createProjectUseCase: CreateProjectUseCase +): ViewModel() { + private val _uiState = MutableStateFlow(CreateProjectUiState()) + val uiState: StateFlow = _uiState + + fun onNameChange(value: String) = + _uiState.update { it.copy(name = value) } + + fun onPackageChange(value: String) = + _uiState.update { it.copy(packageName = value) } + + fun onPathChange(value: String) = + _uiState.update { it.copy(path = value) } + + fun createProject() { + val state = _uiState.value + + if (state.name.isBlank() || + state.packageName.isBlank() || + state.path.isBlank() + ) { + _uiState.update { + it.copy(error = "Todos los campos son obligatorios") + } + return + } + + val project = AppMakeupProject( + name = state.name, + packageName = state.packageName, + path = state.path, + features = emptyList() + ) + + createProjectUseCase.execute(project) + + _uiState.update { + it.copy(isCreated = true, error = null) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/ExportViewModel.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/ExportViewModel.kt new file mode 100644 index 0000000..35ccc97 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/ExportViewModel.kt @@ -0,0 +1,111 @@ +package com.elitec.appmakeup.presentation.viewmodels + +import androidx.lifecycle.ViewModel +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.generation.GenerateCodeUseCase +import com.elitec.appmakeup.generation.PreviewGenerationPlanUseCase +import com.elitec.appmakeup.presentation.states.ExportUiState +import com.elitec.appmakeup.presentation.states.GenerationPreviewUiState +import com.elitec.appmakeup.projects.usecase.project.LoadProjectUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class ExportViewModel( + private val loadProjectUseCase: LoadProjectUseCase, + private val generateCodeUseCase: GenerateCodeUseCase, + private val previewGenerationPlanUseCase: PreviewGenerationPlanUseCase +) : ViewModel() { + + private val _uiState = MutableStateFlow(ExportUiState()) + val uiState: StateFlow = _uiState + + private val _uiPreviewState = MutableStateFlow(GenerationPreviewUiState()) + val uiPreviewState: StateFlow = _uiPreviewState + + /* --------------------------- + * Init + * --------------------------- */ + + fun loadProject(path: String) { + try { + val project = loadProjectUseCase.execute(path) + _uiState.update { + it.copy(project = project) + } + } catch (e: Exception) { + _uiState.update { + it.copy(error = e.message) + } + } + } + + /* --------------------------- + * Options + * --------------------------- */ + + fun toggleDryRun(enabled: Boolean) { + _uiState.update { it.copy(dryRun = enabled) } + } + + /* --------------------------- + * Export + * --------------------------- */ + + fun runExport() { + val project = _uiState.value.project ?: return + + _uiState.update { + it.copy(isRunning = true, error = null, result = null) + } + + try { + generateCodeUseCase.execute( + project = project, + dryRun = _uiState.value.dryRun + ) + + _uiState.update { + it.copy( + result = + if (_uiState.value.dryRun) + "Dry-run completed successfully" + else + "Project exported successfully" + ) + } + } catch (e: Exception) { + _uiState.update { + it.copy(error = e.message) + } + } finally { + _uiState.update { + it.copy(isRunning = false) + } + } + } + + fun previewPlan(context: GenerationContext) { + val plan = previewGenerationPlanUseCase.execute(context) + + _uiPreviewState.update { + it.copy( + generateDomain = plan.generateDomain, + generateData = plan.generateData, + generateRepositories = plan.generateRepositories, + generateMappers = plan.generateMappers + ) + } + } + + fun runPreview() { + val project = _uiState.value.project ?: return + + val files = generateCodeUseCase.preview(project) + + _uiPreviewState.update { + it.copy(previewFiles = files) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/HomeViewModel.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/HomeViewModel.kt new file mode 100644 index 0000000..9428a06 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/HomeViewModel.kt @@ -0,0 +1,25 @@ +package com.elitec.appmakeup.presentation.viewmodels + +import androidx.lifecycle.ViewModel +import com.elitec.appmakeup.presentation.states.HomeUiState +import com.elitec.appmakeup.projects.usecase.project.ListRecentProjectsUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class HomeViewModel( + private val listRecentProjectsUseCase: ListRecentProjectsUseCase +): ViewModel() { + + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState: StateFlow = _uiState + + fun loadRecentProjects() { + try { + val recent = listRecentProjectsUseCase.execute() + _uiState.update { it.copy(recentProjects = recent) } + } catch (e: Exception) { + _uiState.update { it.copy(error = e.message) } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/ProjectEditorViewModel.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/ProjectEditorViewModel.kt new file mode 100644 index 0000000..87c2a45 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/presentation/viewmodels/ProjectEditorViewModel.kt @@ -0,0 +1,223 @@ +package com.elitec.appmakeup.presentation.viewmodels + +import androidx.lifecycle.ViewModel +import com.elitec.appmakeup.core.v4.definition.CoreEntity +import com.elitec.appmakeup.core.v4.definition.CoreLayer +import com.elitec.appmakeup.core.v4.definition.CoreProperty +import com.elitec.appmakeup.generation.GenerateCodeUseCase +import com.elitec.appmakeup.presentation.states.ProjectEditorUiState +import com.elitec.appmakeup.projects.model.AppFeature +import com.elitec.appmakeup.projects.model.AppMakeupProject +import com.elitec.appmakeup.projects.model.AppProperty +import com.elitec.appmakeup.projects.usecase.feature.AddFeatureUseCase +import com.elitec.appmakeup.projects.usecase.feature.GetFeatureUseCase +import com.elitec.appmakeup.projects.usecase.feature.ListFeaturesUseCase +import com.elitec.appmakeup.projects.usecase.feature.RemoveFeatureUseCase +import com.elitec.appmakeup.projects.usecase.project.LoadProjectUseCase +import com.elitec.appmakeup.projects.usecase.project.SaveProjectUseCase +import com.elitec.appmakeup.projects.usecase.property.AddPropertyUseCase +import com.elitec.appmakeup.projects.usecase.property.ListPropertiesUseCase +import com.elitec.appmakeup.projects.usecase.property.RemovePropertyUseCase +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update + +class ProjectEditorViewModel( + private val loadProjectUseCase: LoadProjectUseCase, + private val saveProjectUseCase: SaveProjectUseCase, + private val addFeatureUseCase: AddFeatureUseCase, + private val removeFeatureUseCase: RemoveFeatureUseCase, + private val addPropertyUseCase: AddPropertyUseCase, + private val removePropertyUseCase: RemovePropertyUseCase, + private val generateCodeUseCase: GenerateCodeUseCase +) : ViewModel() { + + private val _uiState = MutableStateFlow(ProjectEditorUiState(isLoading = true)) + val uiState: StateFlow = _uiState + + /* --------------------------- + * Load + * --------------------------- */ + + fun loadProject(path: String) { + try { + val project = loadProjectUseCase.execute(path) + refresh(project) + } catch (e: Exception) { + _uiState.update { + it.copy(isLoading = false, error = e.message) + } + } + } + + /* --------------------------- + * Feature + * --------------------------- */ + + fun addFeature(name: String) { + val project = currentProject() ?: return + + if (project.features.any { it.name == name }) { + fail("Feature '$name' already exists") + return + } + + val feature = AppFeature( + name = name, + properties = listOf( + AppProperty( + name = "id", + type = "String", + isIdentifier = true + ) + ) + ) + + refresh(addFeatureUseCase.execute(project, feature)) + } + + fun removeFeature(name: String) { + val project = currentProject() ?: return + + if (project.features.size == 1) { + fail("Project must contain at least one feature") + return + } + + refresh(removeFeatureUseCase.execute(project, name)) + } + + fun selectFeature(name: String) { + val feature = currentProject()?.features?.find { it.name == name } + _uiState.update { it.copy(selectedFeature = feature) } + } + + /* --------------------------- + * Properties + * --------------------------- */ + + fun addProperty( + featureName: String, + propertyName: String, + type: String, + isIdentifier: Boolean + ) { + val project = currentProject() ?: return + + val feature = project.features.find { it.name == featureName } ?: return + + if (isIdentifier && feature.properties.any { it.isIdentifier }) { + fail("Feature '$featureName' already has an identifier") + return + } + + refresh( + addPropertyUseCase.execute( + project, + featureName, + AppProperty(propertyName, type, isIdentifier) + ) + ) + } + + fun removeProperty(featureName: String, propertyName: String) { + val project = currentProject() ?: return + val feature = project.features.find { it.name == featureName } ?: return + + val property = feature.properties.find { it.name == propertyName } ?: return + + if (property.isIdentifier) { + fail("An identifier property cannot be removed") + return + } + + refresh(removePropertyUseCase.execute(project, featureName, propertyName)) + } + + /* --------------------------- + * Export + * --------------------------- */ + + fun export() { + val project = currentProject() ?: return + + val errors = validateProject(project) + if (errors.isNotEmpty()) { + _uiState.update { it.copy(validationErrors = errors) } + return + } + + generateCodeUseCase.execute(project, true) + } + + /* --------------------------- + * Helpers + * --------------------------- */ + + private fun refresh(project: AppMakeupProject) { + val errors = validateProject(project) + + _uiState.update { + it.copy( + project = project, + features = project.features, + selectedFeature = null, + validationErrors = errors, + canExport = errors.isEmpty(), + isLoading = false, + error = null + ) + } + + saveProjectUseCase.execute(project) + } + + private fun fail(message: String) { + _uiState.update { it.copy(error = message) } + } + + private fun currentProject(): AppMakeupProject? = + _uiState.value.project + + private fun validateProject(project: AppMakeupProject): List { + val errors = mutableListOf() + + if (project.features.isEmpty()) { + errors += "Project must contain at least one feature" + } + + project.features.forEach { feature -> + if (feature.properties.isEmpty()) { + errors += "Feature '${feature.name}' must have at least one property" + } + + val identifiers = feature.properties.count { it.isIdentifier } + if (identifiers != 1) { + errors += "Feature '${feature.name}' must have exactly one identifier property" + } + } + + return errors + } + + private fun validateFeature(feature: AppFeature): String? { + + val identifiers = + feature.properties.count { it.isIdentifier } + + if (identifiers != 1) { + return "Feature '${feature.name}' must have exactly one identifier" + } + + feature.repository?.let { + if (!it.isValid()) { + return "Repository for '${feature.name}' must support at least one operation" + } + } + + return null + } + + fun canExport(): Boolean = + _uiState.value.features.all { validateFeature(it) == null } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/mappers/ProjectToCoreMapper.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/mappers/ProjectToCoreMapper.kt index 327d650..2671451 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/mappers/ProjectToCoreMapper.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/mappers/ProjectToCoreMapper.kt @@ -12,7 +12,7 @@ class ProjectToCoreMapper { fun mapFeature(feature: AppFeature): CoreFeature { val entity = CoreEntity( - name = feature.name, + name = feature.name.replaceFirstChar { it.uppercase() }, properties = feature.properties.map { CoreProperty( name = it.name, diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppFeature.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppFeature.kt index 9e0ee84..de7b3e0 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppFeature.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppFeature.kt @@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable @Serializable data class AppFeature( val name: String, - val properties: List + val properties: List, + val repository: RepositoryConfig? = null ) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppMakeupProject.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppMakeupProject.kt index 0906e0f..e0675d7 100644 --- a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppMakeupProject.kt +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/AppMakeupProject.kt @@ -7,6 +7,8 @@ data class AppMakeupProject( val name: String, val packageName: String, val path: String, - val features: List, + val features: List +) { val exportPath: String -) \ No newline at end of file + get() = "$path/export" +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/RepositoryConfig.kt b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/RepositoryConfig.kt new file mode 100644 index 0000000..4eb1e2a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/elitec/appmakeup/projects/model/RepositoryConfig.kt @@ -0,0 +1,14 @@ +package com.elitec.appmakeup.projects.model + +import kotlinx.serialization.Serializable + +@Serializable +data class RepositoryConfig( + val supportsCreate: Boolean = false, + val supportsRead: Boolean = true, + val supportsUpdate: Boolean = false, + val supportsDelete: Boolean = false +) { + fun isValid(): Boolean = + supportsCreate || supportsRead || supportsUpdate || supportsDelete +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/exports/ProjectExporter.kt b/composeApp/src/jvmMain/java/com/elitec/appmakeup/exports/ProjectExporter.kt deleted file mode 100644 index f0ec31f..0000000 --- a/composeApp/src/jvmMain/java/com/elitec/appmakeup/exports/ProjectExporter.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.elitec.appmakeup.exports - -import com.elitec.appmakeup.core.architecture.DefaultArchitecture -import com.elitec.appmakeup.core.v4.contracts.GenerationContext -import com.elitec.appmakeup.core.v4.pipeline.GenerationPipeline -import com.elitec.appmakeup.projects.mappers.ProjectToCoreMapper -import com.elitec.appmakeup.projects.model.AppMakeupProject - -class ProjectExporter( - private val pipeline: GenerationPipeline -) { - - fun export(project: AppMakeupProject) { - - val packagePath = project.packageName.replace(".", "/") - - val outputPath = - "${project.path}/export/composeApp/src/androidMain/kotlin/$packagePath" - - project.features.forEach { feature -> - - val coreFeature = - ProjectToCoreMapper().mapFeature(feature) - - pipeline.run( - GenerationContext( - architecture = DefaultArchitecture.value, - feature = coreFeature, - outputPath = outputPath - ) - ) - } - } -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/generation/ProjectCodeGenerator.kt b/composeApp/src/jvmMain/java/com/elitec/appmakeup/generation/ProjectCodeGenerator.kt deleted file mode 100644 index 1df677c..0000000 --- a/composeApp/src/jvmMain/java/com/elitec/appmakeup/generation/ProjectCodeGenerator.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.elitec.appmakeup.generation - -import com.elitec.appmakeup.exports.ProjectExporter - -class ProjectCodeGenerator( - private val exporter: ProjectExporter -) : CodeGenerator { - - override fun generate(intent: CodeGenerationIntent) { - exporter.export(intent.project) - } -} \ No newline at end of file diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/presentation/util/pickDirectory.jvm.kt b/composeApp/src/jvmMain/java/com/elitec/appmakeup/presentation/util/pickDirectory.jvm.kt new file mode 100644 index 0000000..57d9c3b --- /dev/null +++ b/composeApp/src/jvmMain/java/com/elitec/appmakeup/presentation/util/pickDirectory.jvm.kt @@ -0,0 +1,10 @@ +package com.elitec.appmakeup.presentation.util + +import java.awt.FileDialog +import java.awt.Frame + +actual fun pickDirectory(): String? { + val dialog = FileDialog(Frame(), "Selecciona una carpeta") + dialog.isVisible = true + return dialog.directory +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/core/v4/pipeline/FileSystemWritingStage.jvm.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/FileSystemWritingStage.jvm.kt similarity index 68% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/core/v4/pipeline/FileSystemWritingStage.jvm.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/FileSystemWritingStage.jvm.kt index 5b05639..d028bca 100644 --- a/composeApp/src/jvmMain/java/com/elitec/appmakeup/core/v4/pipeline/FileSystemWritingStage.jvm.kt +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/core/v4/pipeline/FileSystemWritingStage.jvm.kt @@ -1,5 +1,6 @@ package com.elitec.appmakeup.core.v4.pipeline +import com.elitec.appmakeup.core.v4.contracts.GenerationContext import com.elitec.appmakeup.core.v4.contracts.GenerationResult import java.io.File @@ -9,34 +10,28 @@ class FileSystemWritingStage( override fun write( outputPath: String, - artifacts: List + artifacts: List, + context: GenerationContext ): GenerationResult { - artifacts.forEach { artifact -> + val dryRun = context.options["dryRun"] as? Boolean ?: false + artifacts.forEach { artifact -> val file = File(outputPath, artifact.relativePath) - if (options.dryRun) { - // Simulación: no tocar filesystem + if (dryRun) { println("[DRY-RUN] Would write: ${file.path}") return@forEach } - // Crear directorios si no existen - file.parentFile?.let { parent -> - if (!parent.exists()) { - parent.mkdirs() - } - } + file.parentFile?.mkdirs() - // Archivo ya existe if (file.exists() && !options.overwrite) { return GenerationResult.Failure( "File already exists and overwrite=false: ${file.path}" ) } - // Escritura real file.writeText(artifact.content) } diff --git a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/coreV4Module.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/coreV4Module.kt new file mode 100644 index 0000000..e8f0c4e --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/coreV4Module.kt @@ -0,0 +1,152 @@ +package com.elitec.appmakeup.di + +import com.elitec.appmakeup.core.v4.contracts.MapperContract +import com.elitec.appmakeup.core.v4.contracts.RepositoryContract +import com.elitec.appmakeup.core.v4.generators.CompositeLayerGenerator +import com.elitec.appmakeup.core.v4.generators.data.RepositoryImplGenerator +import com.elitec.appmakeup.core.v4.generators.domain.DomainEntityGenerator +import com.elitec.appmakeup.core.v4.generators.feature.FeatureSkeletonGenerator +import com.elitec.appmakeup.core.v4.generators.mapper.MapperGenerator +import com.elitec.appmakeup.core.v4.generators.repository.RepositoryGenerator +import com.elitec.appmakeup.core.v4.generators.usecase.UseCaseGenerator +import com.elitec.appmakeup.core.v4.pipeline.DefaultGenerationStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultPlanningStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultReportingStage +import com.elitec.appmakeup.core.v4.pipeline.FileSystemWritingStage +import com.elitec.appmakeup.core.v4.pipeline.GenerationPipeline +import com.elitec.appmakeup.core.v4.pipeline.GenerationStage +import com.elitec.appmakeup.core.v4.pipeline.PlanningStage +import com.elitec.appmakeup.core.v4.pipeline.PreviewWritingStage +import com.elitec.appmakeup.core.v4.pipeline.ReportFormat +import com.elitec.appmakeup.core.v4.pipeline.ReportingStage +import com.elitec.appmakeup.core.v4.pipeline.ValidationStage +import com.elitec.appmakeup.core.v4.pipeline.WritingOptions +import com.elitec.appmakeup.core.v4.pipeline.WritingStage +import com.elitec.appmakeup.core.v4.validation.ArchitectureValidator +import com.elitec.appmakeup.core.v4.validation.DefaultValidationStage +import com.elitec.appmakeup.core.v4.validation.EntityValidator +import com.elitec.appmakeup.core.v4.validation.FeatureValidator +import org.koin.dsl.module + +val coreV4Module = module { + + /* ========================================================= + * GENERATORS + * ========================================================= */ + + single { FeatureSkeletonGenerator() } + single { DomainEntityGenerator() } + single { RepositoryGenerator(contracts = get()) } + single { UseCaseGenerator(contracts = get()) } + single { RepositoryImplGenerator(contracts = get()) } + single { MapperGenerator(contracts = get()) } + + /* ========================================================= + * VALIDATION + * ========================================================= */ + + single { EntityValidator() } + single { FeatureValidator(get()) } + single { ArchitectureValidator() } + + single { + DefaultValidationStage( + featureValidator = get(), + architectureValidator = get() + ) + } + + /* ========================================================= + * PLANNING + * ========================================================= */ + + single> { emptyList() } + single> { emptyList() } + + single { + DefaultPlanningStage( + repositoryContracts = get(), + mapperContracts = get() + ) + } + + /* ========================================================= + * GENERATION + * ========================================================= */ + + single { + DefaultGenerationStage( + domainGenerator = CompositeLayerGenerator( + listOf( + get(), + get(), + get() + ) + ), + dataGenerator = CompositeLayerGenerator( + listOf( + get(), + get() + ) + ), + presentationGenerator = null, + repositoryGenerator = null, + mapperGenerator = null + ) + } + + /* ========================================================= + * WRITING (REAL) + * ========================================================= */ + + single { + FileSystemWritingStage( + options = WritingOptions( + dryRun = false, + overwrite = true + ) + ) + } + + /* ========================================================= + * WRITING (PREVIEW) + * ========================================================= */ + + single { + PreviewWritingStage() + } + + /* ========================================================= + * REPORTING + * ========================================================= */ + + single { + DefaultReportingStage(ReportFormat.CLI) + } + + /* ========================================================= + * PIPELINES + * ========================================================= */ + + // Pipeline REAL + single { + GenerationPipeline( + validationStage = get(), + planningStage = get(), + generationStage = get(), + writingStage = get(), + reportingStage = get() + ) + } + + // Pipeline PREVIEW + single { + GenerationPipeline( + validationStage = get(), + planningStage = get(), + generationStage = get(), + writingStage = get(), + reportingStage = get() + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/infrastructureModule.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/infrastructureModule.kt new file mode 100644 index 0000000..320b9a3 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/infrastructureModule.kt @@ -0,0 +1,32 @@ +package com.elitec.appmakeup.di + +import com.elitec.appmakeup.exports.ProjectExporter +import com.elitec.appmakeup.generation.CodeGenerator +import com.elitec.appmakeup.generation.ProjectCodeGenerator +import com.elitec.appmakeup.projects.persistence.FileProjectPersistence +import com.elitec.appmakeup.projects.persistence.ProjectPersistence +import com.elitec.appmakeup.recent.FileRecentProjectsRepository +import com.elitec.appmakeup.recent.RecentProjectsRepository +import org.koin.dsl.module + +val infrastructureModule = module { + + single { + FileProjectPersistence() + } + + single { + FileRecentProjectsRepository() + } + + single { + ProjectExporter( + pipeline = get(), + previewPipeline = get() + ) + } + + single { + ProjectCodeGenerator(get()) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/initKoin.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/initKoin.kt new file mode 100644 index 0000000..cfffdda --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/di/initKoin.kt @@ -0,0 +1,14 @@ +package com.elitec.appmakeup.di + +import org.koin.core.context.GlobalContext.startKoin + +fun initKoin() { + startKoin { + modules( + infrastructureModule, + coreV4Module, + applicationModule, + presentationModule + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/exports/ProjectExporter.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/exports/ProjectExporter.kt new file mode 100644 index 0000000..c5800a1 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/exports/ProjectExporter.kt @@ -0,0 +1,123 @@ +package com.elitec.appmakeup.exports + +import com.elitec.appmakeup.core.architecture.DefaultArchitecture +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.core.v4.pipeline.GenerationPipeline +import com.elitec.appmakeup.logs.Logger +import com.elitec.appmakeup.projects.mappers.ProjectToCoreMapper +import com.elitec.appmakeup.projects.model.AppMakeupProject + +class ProjectExporter( + private val pipeline: GenerationPipeline, + private val previewPipeline: GenerationPipeline +) { + + private val tag = "ProjectExporter::DESKTOP" + + fun export( + project: AppMakeupProject, + dryRun: Boolean + ): GenerationResult { + + Logger.success( + tag, + "Init generation code in platform (dryRun=$dryRun)" + ) + + val packagePath = project.packageName.replace(".", "/") + Logger.success( + tag, + "Generation code package path: $packagePath" + ) + + val outputPath = + "${project.path}/export/composeApp/src/androidMain/kotlin/$packagePath" + + Logger.success( + tag, + "Generation output path: $outputPath" + ) + + val pipelineToUse = + if (dryRun) previewPipeline else pipeline + + var lastResult: GenerationResult = GenerationResult.Success + + project.features.forEach { feature -> + + Logger.warning( + tag, + "Scanning feature: ${feature.name}" + ) + + val coreFeature = + ProjectToCoreMapper().mapFeature(feature) + + val result = pipelineToUse.run( + GenerationContext( + architecture = DefaultArchitecture.value, + feature = coreFeature, + outputPath = outputPath, + options = mapOf("dryRun" to dryRun) + ) + ) + + when (result) { + is GenerationResult.Failure -> { + Logger.error( + tag, + "Generation failed for feature ${feature.name}", + RuntimeException(result.reason) + ) + return result + } + + is GenerationResult.Preview -> { + Logger.success( + tag, + "Preview generated for feature ${feature.name} (${result.files.size} files)" + ) + lastResult = result + } + + GenerationResult.Success -> { + Logger.success( + tag, + "Generation completed for feature ${feature.name}" + ) + lastResult = result + } + } + } + + return lastResult + } + + fun preview(project: AppMakeupProject): List { + val allFiles = mutableListOf() + + val packagePath = project.packageName.replace(".", "/") + val outputPath = + "${project.path}/export/composeApp/src/androidMain/kotlin/$packagePath" + + project.features.forEach { feature -> + val coreFeature = ProjectToCoreMapper().mapFeature(feature) + + val result = previewPipeline.run( + GenerationContext( + architecture = DefaultArchitecture.value, + feature = coreFeature, + outputPath = outputPath, + options = mapOf("dryRun" to true) + ) + ) + + if (result is GenerationResult.Preview) { + allFiles += result.files + } + } + + return allFiles + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/generation/ProjectCodeGenerator.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/generation/ProjectCodeGenerator.kt new file mode 100644 index 0000000..5007c0f --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/generation/ProjectCodeGenerator.kt @@ -0,0 +1,22 @@ +package com.elitec.appmakeup.generation + +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.exports.ProjectExporter +import com.elitec.appmakeup.projects.model.AppMakeupProject + +class ProjectCodeGenerator( + private val exporter: ProjectExporter +) : CodeGenerator { + + private val tag = "[ProjectCodeGenerator]---> " + + override fun generate(intent: CodeGenerationIntent, dryRun: Boolean): GenerationResult { + println("$tag Init executing Generation in platform with \ndryRun: $dryRun") + return exporter.export(intent.project, dryRun) + } + + override fun preview(project: AppMakeupProject): List { + println("$tag Preview Generation in platform ") + return exporter.preview(project) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/main.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/main.kt index 1aea421..65acc6b 100644 --- a/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/main.kt +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/main.kt @@ -1,15 +1,46 @@ package com.elitec.appmakeup +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Maximize +import androidx.compose.material.icons.filled.Minimize +import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Window import androidx.compose.ui.window.WindowPlacement import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.application import appmakeup.composeapp.generated.resources.Res +import appmakeup.composeapp.generated.resources.sinfotow import appmakeup.composeapp.generated.resources.toolicon +import com.elitec.appmakeup.di.initKoin +import com.elitec.appmakeup.presentation.navigation.MainNavigationWrapper +import com.elitec.appmakeup.presentation.theme.AppTheme +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import org.koin.core.context.startKoin @@ -18,8 +49,10 @@ fun main() = application { position = WindowPosition(alignment = Alignment.Center), placement = WindowPlacement.Floating ) - startKoin { + LaunchedEffect(null) { + initKoin() } + val scope = rememberCoroutineScope() Window( resizable = false, undecorated = true, @@ -28,6 +61,76 @@ fun main() = application { onCloseRequest = ::exitApplication, title = "AppMakeup", ) { + AppTheme { + Scaffold( + topBar = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.End, + modifier = Modifier.fillMaxWidth() + ) { + Surface( + shape = RoundedCornerShape(5.dp), + color = MaterialTheme.colorScheme.onBackground, + onClick = { + windowState.isMinimized = true + } + ) { + Icon( + imageVector = Icons.Default.Minimize, + contentDescription = "", + tint = MaterialTheme.colorScheme.background, + modifier = Modifier.size(20.dp) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Surface( + shape = RoundedCornerShape(5.dp), + color = MaterialTheme.colorScheme.onBackground, + onClick = { + if(windowState.placement == WindowPlacement.Maximized) { + windowState.placement = WindowPlacement.Floating + return@Surface + } + windowState.placement = WindowPlacement.Maximized + } + ) { + Icon( + imageVector = Icons.Default.Maximize, + contentDescription = "", + tint = MaterialTheme.colorScheme.background, + modifier = Modifier.size(20.dp) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Surface( + shape = RoundedCornerShape(5.dp), + color = MaterialTheme.colorScheme.onBackground, + onClick = { + exitApplication() + } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "", + tint = MaterialTheme.colorScheme.background, + modifier = Modifier.size(20.dp) + ) + } + } + }, + bottomBar = { + Text(text = isSystemInDarkTheme().toString()) + }, + ) { innerPaddings -> + MainNavigationWrapper( + onAppReady = { + windowState.placement = WindowPlacement.Maximized + }, + modifier = Modifier.fillMaxSize().padding(innerPaddings) + ) + } + } } } \ No newline at end of file diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/filesystem/DirectoryCreator.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/filesystem/DirectoryCreator.kt similarity index 100% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/filesystem/DirectoryCreator.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/filesystem/DirectoryCreator.kt diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/filesystem/FileCopier.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/filesystem/FileCopier.kt similarity index 100% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/filesystem/FileCopier.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/filesystem/FileCopier.kt diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/filesystem/FileRenderer.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/filesystem/FileRenderer.kt similarity index 100% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/filesystem/FileRenderer.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/filesystem/FileRenderer.kt diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/initializer/ProjectInitializer.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/initializer/ProjectInitializer.kt similarity index 100% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/initializer/ProjectInitializer.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/initializer/ProjectInitializer.kt diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/persistence/FileProjectPersistence.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/persistence/FileProjectPersistence.kt similarity index 85% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/persistence/FileProjectPersistence.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/persistence/FileProjectPersistence.kt index 1eb933e..6dd8769 100644 --- a/composeApp/src/jvmMain/java/com/elitec/appmakeup/projects/persistence/FileProjectPersistence.kt +++ b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/projects/persistence/FileProjectPersistence.kt @@ -12,12 +12,12 @@ class FileProjectPersistence : ProjectPersistence { } override fun save(project: AppMakeupProject) { - val file = File(project.path, "appmakeup.json") + val file = File(project.path, "projectConfig.amk") file.writeText(json.encodeToString(AppMakeupProject.serializer(), project)) } override fun load(path: String): AppMakeupProject { - val file = File(path, "appmakeup.json") + val file = File(path, "projectConfig.amk") return json.decodeFromString( AppMakeupProject.serializer(), file.readText() diff --git a/composeApp/src/jvmMain/java/com/elitec/appmakeup/recent/FileRecentProjectsRepository.kt b/composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/recent/FileRecentProjectsRepository.kt similarity index 100% rename from composeApp/src/jvmMain/java/com/elitec/appmakeup/recent/FileRecentProjectsRepository.kt rename to composeApp/src/jvmMain/kotlin/com/elitec/appmakeup/recent/FileRecentProjectsRepository.kt diff --git a/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineDataDryRunTest.kt b/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineDataDryRunTest.kt new file mode 100644 index 0000000..7db8d10 --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineDataDryRunTest.kt @@ -0,0 +1,124 @@ +package com.elitec.appmakeup.core.v4 + +import com.elitec.appmakeup.core.architecture.DefaultArchitecture +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.core.v4.contracts.RepositoryContract +import com.elitec.appmakeup.core.v4.definition.CoreEntity +import com.elitec.appmakeup.core.v4.definition.CoreFeature +import com.elitec.appmakeup.core.v4.definition.CoreLayer +import com.elitec.appmakeup.core.v4.definition.CoreProperty +import com.elitec.appmakeup.core.v4.generators.CompositeLayerGenerator +import com.elitec.appmakeup.core.v4.generators.data.RepositoryImplGenerator +import com.elitec.appmakeup.core.v4.generators.domain.DomainEntityGenerator +import com.elitec.appmakeup.core.v4.generators.repository.RepositoryGenerator +import com.elitec.appmakeup.core.v4.generators.usecase.UseCaseGenerator +import com.elitec.appmakeup.core.v4.pipeline.DefaultGenerationStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultPlanningStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultReportingStage +import com.elitec.appmakeup.core.v4.pipeline.FileSystemWritingStage +import com.elitec.appmakeup.core.v4.pipeline.GenerationPipeline +import com.elitec.appmakeup.core.v4.pipeline.ReportFormat +import com.elitec.appmakeup.core.v4.pipeline.WritingOptions +import com.elitec.appmakeup.core.v4.validation.ArchitectureValidator +import com.elitec.appmakeup.core.v4.validation.DefaultValidationStage +import com.elitec.appmakeup.core.v4.validation.FeatureValidator +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class GenerationPipelineDataDryRunTest { + @Test + fun `dry-run should generate domain and data artifacts when repository contract exists`() { + + // GIVEN + val tempDir = Files.createTempDirectory("appmakeup-data-test").toFile() + + val userEntity = CoreEntity( + name = "User", + properties = listOf( + CoreProperty( + name = "id", + type = "String", + isIdentifier = true + ), + CoreProperty( + name = "name", + type = "String" + ) + ) + ) + + val repositoryContract = RepositoryContract( + entity = userEntity, + supportsCreate = true, + supportsRead = true, + supportsUpdate = true, + supportsDelete = false + ) + + val feature = CoreFeature( + name = "User", + layers = setOf( + CoreLayer.DOMAIN, + CoreLayer.DATA + ), + entities = listOf(userEntity) + ) + + val context = GenerationContext( + architecture = DefaultArchitecture.value, + feature = feature, + outputPath = tempDir.absolutePath + ) + + val pipeline = GenerationPipeline( + validationStage = DefaultValidationStage( + featureValidator = FeatureValidator(), + architectureValidator = ArchitectureValidator() + ), + planningStage = DefaultPlanningStage( + repositoryContracts = listOf(repositoryContract), + mapperContracts = emptyList() + ), + generationStage = DefaultGenerationStage( + domainGenerator = CompositeLayerGenerator( + listOf( + DomainEntityGenerator(), + RepositoryGenerator(listOf(repositoryContract)), + UseCaseGenerator(listOf(repositoryContract)) + ) + ), + dataGenerator = CompositeLayerGenerator( + listOf( + RepositoryImplGenerator(listOf(repositoryContract)) + ) + ), + presentationGenerator = null, + repositoryGenerator = null, + mapperGenerator = null + ), + writingStage = FileSystemWritingStage( + WritingOptions(dryRun = true, overwrite = true) + ), + reportingStage = DefaultReportingStage(ReportFormat.CLI) + ) + + // WHEN + val result = pipeline.run(context) + + // THEN + assertTrue( + result is GenerationResult.Success, + "Pipeline should succeed when DATA contracts are valid" + ) + + // filesystem debe estar vacío (solo el directorio raíz) + assertEquals( + 1, + tempDir.walkTopDown().toList().size, + "No files should be written in dry-run mode" + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineDryRunTest.kt b/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineDryRunTest.kt new file mode 100644 index 0000000..dcb8717 --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineDryRunTest.kt @@ -0,0 +1,152 @@ +package com.elitec.appmakeup.core.v4 + +import com.elitec.appmakeup.core.architecture.DefaultArchitecture +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.core.v4.definition.CoreEntity +import com.elitec.appmakeup.core.v4.definition.CoreFeature +import com.elitec.appmakeup.core.v4.definition.CoreLayer +import com.elitec.appmakeup.core.v4.definition.CoreProperty +import com.elitec.appmakeup.core.v4.generators.CompositeLayerGenerator +import com.elitec.appmakeup.core.v4.generators.domain.DomainEntityGenerator +import com.elitec.appmakeup.core.v4.generators.repository.RepositoryGenerator +import com.elitec.appmakeup.core.v4.pipeline.DefaultGenerationStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultPlanningStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultReportingStage +import com.elitec.appmakeup.core.v4.pipeline.FileSystemWritingStage +import com.elitec.appmakeup.core.v4.pipeline.GenerationPipeline +import com.elitec.appmakeup.core.v4.pipeline.ReportFormat +import com.elitec.appmakeup.core.v4.pipeline.WritingOptions +import com.elitec.appmakeup.core.v4.validation.ArchitectureValidator +import com.elitec.appmakeup.core.v4.validation.DefaultValidationStage +import com.elitec.appmakeup.core.v4.validation.FeatureValidator +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class GenerationPipelineDryRunTest { + @Test + fun `dry-run should generate domain artifacts without writing files`() { + + val tempDir = Files.createTempDirectory("appmakeup-test").toFile() + + val feature = CoreFeature( + name = "User", + layers = setOf(CoreLayer.DOMAIN), + entities = listOf( + CoreEntity( + name = "User", + properties = listOf( + CoreProperty("id", "String", isIdentifier = true), + CoreProperty("name", "String") + ) + ) + ) + ) + + val context = GenerationContext( + architecture = DefaultArchitecture.value, + feature = feature, + outputPath = tempDir.absolutePath + ) + + val pipeline = GenerationPipeline( + validationStage = DefaultValidationStage( + featureValidator = FeatureValidator(), + architectureValidator = ArchitectureValidator() + ), + planningStage = DefaultPlanningStage(), + generationStage = DefaultGenerationStage( + domainGenerator = DomainEntityGenerator(), + dataGenerator = null, + presentationGenerator = null, + repositoryGenerator = null, + mapperGenerator = null + ), + writingStage = FileSystemWritingStage( + WritingOptions(dryRun = true, overwrite = true) + ), + reportingStage = DefaultReportingStage(ReportFormat.CLI) + ) + + val result = pipeline.run(context) + + assertTrue(result is GenerationResult.Success) + + // filesystem vacío + assertEquals( + 1, + tempDir.walkTopDown().toList().size + ) + } +} + +private fun pipeline(): GenerationPipeline = + GenerationPipeline( + validationStage = DefaultValidationStage( + featureValidator = FeatureValidator(), + architectureValidator = ArchitectureValidator() + ), + planningStage = DefaultPlanningStage( + repositoryContracts = emptyList(), + mapperContracts = emptyList() + ), + generationStage = DefaultGenerationStage( + domainGenerator = CompositeLayerGenerator( + listOf( + DomainEntityGenerator() + ) + ), + dataGenerator = CompositeLayerGenerator( + listOf( + RepositoryGenerator(emptyList()) + ) + ), + presentationGenerator = null, + repositoryGenerator = null, + mapperGenerator = null + ), + writingStage = dryRunWritingStage(), + reportingStage = DefaultReportingStage(ReportFormat.CLI) + ) + +private fun dryRunWritingStage() = + FileSystemWritingStage( + options = WritingOptions( + dryRun = true, + overwrite = true + ) + ) + +private fun sampleContext(outputPath: String) = + GenerationContext( + architecture = DefaultArchitecture.value, + feature = sampleFeature(), + outputPath = outputPath + ) + +private fun sampleFeature(): CoreFeature = + CoreFeature( + name = "User", + layers = setOf( + CoreLayer.DOMAIN, + CoreLayer.DATA + ), + entities = listOf( + CoreEntity( + name = "User", + properties = listOf( + CoreProperty( + name = "id", + type = "String", + isIdentifier = true + ), + CoreProperty( + name = "name", + type = "String" + ) + ) + ) + ) + ) \ No newline at end of file diff --git a/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineFailureTest.kt b/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineFailureTest.kt new file mode 100644 index 0000000..ed7ca2a --- /dev/null +++ b/composeApp/src/jvmTest/kotlin/com/elitec/appmakeup/core/v4/GenerationPipelineFailureTest.kt @@ -0,0 +1,78 @@ +package com.elitec.appmakeup.core.v4 + +import com.elitec.appmakeup.core.architecture.DefaultArchitecture +import com.elitec.appmakeup.core.v4.contracts.GenerationContext +import com.elitec.appmakeup.core.v4.contracts.GenerationResult +import com.elitec.appmakeup.core.v4.definition.CoreFeature +import com.elitec.appmakeup.core.v4.definition.CoreLayer +import com.elitec.appmakeup.core.v4.generators.domain.DomainEntityGenerator +import com.elitec.appmakeup.core.v4.pipeline.DefaultGenerationStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultPlanningStage +import com.elitec.appmakeup.core.v4.pipeline.DefaultReportingStage +import com.elitec.appmakeup.core.v4.pipeline.FileSystemWritingStage +import com.elitec.appmakeup.core.v4.pipeline.GenerationPipeline +import com.elitec.appmakeup.core.v4.pipeline.ReportFormat +import com.elitec.appmakeup.core.v4.pipeline.WritingOptions +import com.elitec.appmakeup.core.v4.validation.ArchitectureValidator +import com.elitec.appmakeup.core.v4.validation.DefaultValidationStage +import com.elitec.appmakeup.core.v4.validation.FeatureValidator +import java.nio.file.Files +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertTrue + +class GenerationPipelineFailureTest { + + @Test + fun `pipeline should fail when feature has no entities`() { + + // GIVEN + val tempDir = Files.createTempDirectory("appmakeup-failure-test").toFile() + + val invalidFeature = CoreFeature( + name = "InvalidFeature", + layers = setOf(CoreLayer.DOMAIN), + entities = emptyList() // ❌ inválido + ) + + val context = GenerationContext( + architecture = DefaultArchitecture.value, + feature = invalidFeature, + outputPath = tempDir.absolutePath + ) + + val pipeline = GenerationPipeline( + validationStage = DefaultValidationStage( + featureValidator = FeatureValidator(), + architectureValidator = ArchitectureValidator() + ), + planningStage = DefaultPlanningStage(), + generationStage = DefaultGenerationStage( + domainGenerator = DomainEntityGenerator(), + dataGenerator = null, + presentationGenerator = null, + repositoryGenerator = null, + mapperGenerator = null + ), + writingStage = FileSystemWritingStage( + WritingOptions(dryRun = true) + ), + reportingStage = DefaultReportingStage(ReportFormat.CLI) + ) + + // WHEN + val result = pipeline.run(context) + + // THEN + assertTrue( + result is GenerationResult.Failure, + "Pipeline should fail for invalid feature" + ) + + val failure = result as GenerationResult.Failure + assertTrue( + failure.reason.contains("must contain at least one entity"), + "Failure reason should explain validation error" + ) + } +} \ No newline at end of file