andrud 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/CODE_REVIEW_ANALYSIS.md +177 -0
- package/CONTRIBUTING.md +132 -0
- package/FIXES_IMPLEMENTED.md +546 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/bin/andrud.js +24 -0
- package/package.json +80 -0
- package/src/__tests__/context.test.ts +133 -0
- package/src/__tests__/generator.test.ts +107 -0
- package/src/__tests__/validation.test.ts +105 -0
- package/src/cli/commands/create.ts +252 -0
- package/src/cli/commands/info.ts +178 -0
- package/src/cli/commands/init.ts +186 -0
- package/src/cli/commands/list.ts +156 -0
- package/src/cli/commands/new.ts +316 -0
- package/src/cli/index.ts +116 -0
- package/src/core/config.ts +172 -0
- package/src/core/context.ts +212 -0
- package/src/core/generator.ts +1350 -0
- package/src/core/types.ts +184 -0
- package/src/templates/index.ts +162 -0
- package/src/types/gradient-string.d.ts +25 -0
- package/src/ui/colors.ts +139 -0
- package/src/ui/output.ts +230 -0
- package/src/ui/prompts.ts +170 -0
- package/src/ui/spinners.ts +95 -0
- package/src/ui/types.ts +41 -0
- package/src/utils/filesystem.ts +222 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/object.ts +456 -0
- package/src/utils/validation.ts +345 -0
- package/tsconfig.json +25 -0
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project generator - creates Android project structure and files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { exists, writeFile, createDirectory, isDirectory } from '../utils/filesystem.js';
|
|
6
|
+
import type { TemplateContext, GenerationResult, GeneratedFile } from './types.js';
|
|
7
|
+
import { buildTemplateContext, buildDefaultProjectContext } from './context.js';
|
|
8
|
+
import { GRADLE_VERSIONS } from './config.js';
|
|
9
|
+
import pc from 'picocolors';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
|
|
12
|
+
export interface GeneratorOptions {
|
|
13
|
+
overwrite?: boolean;
|
|
14
|
+
skipInstall?: boolean;
|
|
15
|
+
dryRun?: boolean;
|
|
16
|
+
verbose?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate project directory can be used
|
|
21
|
+
*/
|
|
22
|
+
export async function validateProjectDirectory(
|
|
23
|
+
projectPath: string,
|
|
24
|
+
options: { overwrite: boolean } = { overwrite: false }
|
|
25
|
+
): Promise<{ valid: boolean; error?: string; existingFiles?: string[] }> {
|
|
26
|
+
const pathExists = await exists(projectPath);
|
|
27
|
+
|
|
28
|
+
if (pathExists) {
|
|
29
|
+
const isDir = await isDirectory(projectPath);
|
|
30
|
+
if (!isDir) {
|
|
31
|
+
return { valid: false, error: 'Project path exists but is not a directory' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const dirContent = await getDirectoryContents(projectPath);
|
|
35
|
+
if (dirContent.length > 0 && !options.overwrite) {
|
|
36
|
+
return {
|
|
37
|
+
valid: false,
|
|
38
|
+
error: 'Directory is not empty. Use --force to overwrite existing files.',
|
|
39
|
+
existingFiles: dirContent
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { valid: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Validate context has all required fields
|
|
49
|
+
*/
|
|
50
|
+
export function validateContext(context: Partial<TemplateContext>): { valid: boolean; errors: string[] } {
|
|
51
|
+
const errors: string[] = [];
|
|
52
|
+
|
|
53
|
+
if (!context.appName) errors.push('appName is required');
|
|
54
|
+
if (!context.packageName) errors.push('packageName is required');
|
|
55
|
+
if (!context.projectDirectory) errors.push('projectDirectory is required');
|
|
56
|
+
if (!context.template) errors.push('template is required');
|
|
57
|
+
if (!context.language) errors.push('language is required');
|
|
58
|
+
if (!context.uiFramework) errors.push('uiFramework is required');
|
|
59
|
+
|
|
60
|
+
return { valid: errors.length === 0, errors };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Prepare project directory structure
|
|
65
|
+
*/
|
|
66
|
+
export async function prepareProjectStructure(
|
|
67
|
+
projectPath: string,
|
|
68
|
+
context: TemplateContext
|
|
69
|
+
): Promise<{ success: boolean; createdPaths: string[]; errors: string[] }> {
|
|
70
|
+
const createdPaths: string[] = [];
|
|
71
|
+
const errors: string[] = [];
|
|
72
|
+
|
|
73
|
+
const directories = [
|
|
74
|
+
'',
|
|
75
|
+
'gradle/wrapper',
|
|
76
|
+
'app/src/main/java',
|
|
77
|
+
'app/src/main/res/layout',
|
|
78
|
+
'app/src/main/res/values',
|
|
79
|
+
'app/src/main/res/values-night',
|
|
80
|
+
'app/src/main/res/drawable',
|
|
81
|
+
'app/src/main/res/xml',
|
|
82
|
+
'app/src/main/res/mipmap-anydpi-v26',
|
|
83
|
+
'app/src/main/res/mipmap-hdpi',
|
|
84
|
+
'app/src/main/res/mipmap-mdpi',
|
|
85
|
+
'app/src/main/res/mipmap-xhdpi',
|
|
86
|
+
'app/src/main/res/mipmap-xxhdpi',
|
|
87
|
+
'app/src/main/res/mipmap-xxxhdpi',
|
|
88
|
+
'app/src/test/java',
|
|
89
|
+
'app/src/androidTest/java',
|
|
90
|
+
context.language === 'kotlin' ? 'app/src/main/kotlin' : '',
|
|
91
|
+
context.template === 'native-cpp' ? 'app/src/main/cpp' : ''
|
|
92
|
+
].filter(d => d !== '');
|
|
93
|
+
|
|
94
|
+
const packagePath = context.packageName.replace(/\./g, '/');
|
|
95
|
+
directories.push(`app/src/main/java/${packagePath}`);
|
|
96
|
+
|
|
97
|
+
if (context.language === 'kotlin') {
|
|
98
|
+
directories.push(`app/src/main/kotlin/${packagePath}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Add UI theme directories for Jetpack Compose projects
|
|
102
|
+
if (context.uiFramework === 'compose' && context.language === 'kotlin') {
|
|
103
|
+
directories.push(`app/src/main/kotlin/${packagePath}/ui`);
|
|
104
|
+
directories.push(`app/src/main/kotlin/${packagePath}/ui/theme`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Add native cpp directories
|
|
108
|
+
if (context.template === 'native-cpp') {
|
|
109
|
+
directories.push(`app/src/main/cpp`);
|
|
110
|
+
directories.push(`app/src/main/jni`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (const dir of directories) {
|
|
114
|
+
const fullPath = join(projectPath, dir);
|
|
115
|
+
try {
|
|
116
|
+
await createDirectory(fullPath);
|
|
117
|
+
createdPaths.push(dir);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
errors.push(`Failed to create directory ${dir}: ${(error as Error).message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { success: errors.length === 0, createdPaths, errors };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Generate all project files
|
|
128
|
+
*/
|
|
129
|
+
export async function generateProject(
|
|
130
|
+
context: TemplateContext,
|
|
131
|
+
options: GeneratorOptions = {}
|
|
132
|
+
): Promise<GenerationResult> {
|
|
133
|
+
const startTime = Date.now();
|
|
134
|
+
const generatedFiles: string[] = [];
|
|
135
|
+
const skippedFiles: string[] = [];
|
|
136
|
+
const errors: Array<{ file?: string; message: string; code?: string }> = [];
|
|
137
|
+
const warnings: string[] = [];
|
|
138
|
+
const projectPath = context.projectDirectory;
|
|
139
|
+
|
|
140
|
+
const contextValidation = validateContext(context);
|
|
141
|
+
if (!contextValidation.valid) {
|
|
142
|
+
return {
|
|
143
|
+
success: false,
|
|
144
|
+
projectPath,
|
|
145
|
+
generatedFiles: [],
|
|
146
|
+
skippedFiles: [],
|
|
147
|
+
errors: contextValidation.errors.map(msg => ({ message: msg })),
|
|
148
|
+
warnings,
|
|
149
|
+
duration: Date.now() - startTime
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const dirValidation = await validateProjectDirectory(projectPath, { overwrite: options.overwrite ?? false });
|
|
154
|
+
if (!dirValidation.valid) {
|
|
155
|
+
return {
|
|
156
|
+
success: false,
|
|
157
|
+
projectPath,
|
|
158
|
+
generatedFiles: [],
|
|
159
|
+
skippedFiles: [],
|
|
160
|
+
errors: [{ message: dirValidation.error!, code: 'DIR_VALIDATION_ERROR' }],
|
|
161
|
+
warnings,
|
|
162
|
+
duration: Date.now() - startTime
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const prepResult = await prepareProjectStructure(projectPath, context);
|
|
167
|
+
if (!prepResult.success) {
|
|
168
|
+
errors.push(...prepResult.errors.map(msg => ({ message: msg, code: 'DIR_CREATE_ERROR' })));
|
|
169
|
+
}
|
|
170
|
+
generatedFiles.push(...prepResult.createdPaths);
|
|
171
|
+
|
|
172
|
+
const filesToGenerate: GeneratedFile[] = [
|
|
173
|
+
generateSettingsGradle(context),
|
|
174
|
+
generateRootBuildGradle(context),
|
|
175
|
+
generateGradleProperties(context),
|
|
176
|
+
generateGitIgnore(context),
|
|
177
|
+
generateReadme(context),
|
|
178
|
+
generateGradleWrapperProperties(context),
|
|
179
|
+
generateGradlewBat(context),
|
|
180
|
+
generateGradlewUnix(context),
|
|
181
|
+
generateAppBuildGradle(context),
|
|
182
|
+
generateAppProguardRules(context),
|
|
183
|
+
generateAppManifest(context),
|
|
184
|
+
generateApplicationClass(context),
|
|
185
|
+
generateMainActivity(context),
|
|
186
|
+
generateStrings(context),
|
|
187
|
+
generateColors(context),
|
|
188
|
+
generateThemes(context),
|
|
189
|
+
generateAppIcon(context),
|
|
190
|
+
generateActivityLayout(context),
|
|
191
|
+
...generateSourceSetFiles(context)
|
|
192
|
+
].filter((f): f is GeneratedFile => f !== null && typeof f === 'object' && 'path' in f);
|
|
193
|
+
|
|
194
|
+
for (const file of filesToGenerate) {
|
|
195
|
+
try {
|
|
196
|
+
const filePath = join(projectPath, file.path);
|
|
197
|
+
const fileExists = await exists(filePath);
|
|
198
|
+
|
|
199
|
+
if (fileExists && !options.overwrite && file.overwrite !== true) {
|
|
200
|
+
skippedFiles.push(file.path);
|
|
201
|
+
if (options.verbose) {
|
|
202
|
+
console.log(pc.yellow(`Skipped: ${file.path} (already exists)`));
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await writeFile(filePath, file.content);
|
|
208
|
+
generatedFiles.push(file.path);
|
|
209
|
+
|
|
210
|
+
if (options.verbose) {
|
|
211
|
+
console.log(pc.green(`Generated: ${file.path}`));
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
errors.push({
|
|
215
|
+
file: file.path,
|
|
216
|
+
message: (error as Error).message,
|
|
217
|
+
code: 'WRITE_ERROR'
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const duration = Date.now() - startTime;
|
|
223
|
+
const success = errors.length === 0;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
success,
|
|
227
|
+
projectPath,
|
|
228
|
+
generatedFiles,
|
|
229
|
+
skippedFiles,
|
|
230
|
+
errors,
|
|
231
|
+
warnings,
|
|
232
|
+
duration
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function generateSettingsGradle(ctx: TemplateContext): GeneratedFile {
|
|
237
|
+
return {
|
|
238
|
+
path: 'settings.gradle.kts',
|
|
239
|
+
content: `pluginManagement {
|
|
240
|
+
repositories {
|
|
241
|
+
google()
|
|
242
|
+
mavenCentral()
|
|
243
|
+
gradlePluginPortal()
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
dependencyResolutionManagement {
|
|
248
|
+
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
|
249
|
+
repositories {
|
|
250
|
+
google()
|
|
251
|
+
mavenCentral()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
rootProject.name = "${ctx.appName}"
|
|
256
|
+
include(":app")
|
|
257
|
+
`
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function generateRootBuildGradle(ctx: TemplateContext): GeneratedFile {
|
|
262
|
+
const kotlinPlugin = ctx.language === 'kotlin' ? `
|
|
263
|
+
id("org.jetbrains.kotlin.android") version "${ctx.gradle.kotlinVersion || GRADLE_VERSIONS.KOTLIN}" apply false` : '';
|
|
264
|
+
|
|
265
|
+
const composePlugin = ctx.uiFramework === 'compose' ? `
|
|
266
|
+
id("org.jetbrains.kotlin.plugin.compose") version "${ctx.gradle.kotlinVersion || GRADLE_VERSIONS.KOTLIN}" apply false` : '';
|
|
267
|
+
|
|
268
|
+
const kaptPlugin = ctx.language === 'kotlin' ? `
|
|
269
|
+
id("org.jetbrains.kotlin.kapt") version "${ctx.gradle.kotlinVersion || GRADLE_VERSIONS.KOTLIN}" apply false` : '';
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
path: 'build.gradle.kts',
|
|
273
|
+
content: `plugins {
|
|
274
|
+
id("com.android.application") version "${ctx.gradle.agpVersion}" apply false${kotlinPlugin}${composePlugin}${kaptPlugin}
|
|
275
|
+
}
|
|
276
|
+
`
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function generateGradleProperties(_ctx: TemplateContext): GeneratedFile {
|
|
281
|
+
return {
|
|
282
|
+
path: 'gradle.properties',
|
|
283
|
+
content: `# Project-wide Gradle settings
|
|
284
|
+
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
285
|
+
org.gradle.parallel=true
|
|
286
|
+
org.gradle.caching=true
|
|
287
|
+
org.gradle.configureondemand=true
|
|
288
|
+
|
|
289
|
+
# Android settings
|
|
290
|
+
android.useAndroidX=true
|
|
291
|
+
android.nonTransitiveRClass=true
|
|
292
|
+
android.suppressUnsupportedCompileSdk=36
|
|
293
|
+
|
|
294
|
+
# Kotlin settings
|
|
295
|
+
kotlin.code.style=official
|
|
296
|
+
`
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function generateGitIgnore(_ctx: TemplateContext): GeneratedFile {
|
|
301
|
+
return {
|
|
302
|
+
path: '.gitignore',
|
|
303
|
+
content: `# Built application files
|
|
304
|
+
*.apk
|
|
305
|
+
*.ap_
|
|
306
|
+
*.aab
|
|
307
|
+
*.dex
|
|
308
|
+
*.class
|
|
309
|
+
bin/
|
|
310
|
+
gen/
|
|
311
|
+
out/
|
|
312
|
+
.gradle/
|
|
313
|
+
build/
|
|
314
|
+
local.properties
|
|
315
|
+
proguard/
|
|
316
|
+
*.log
|
|
317
|
+
.navigation/
|
|
318
|
+
captures/
|
|
319
|
+
*.iml
|
|
320
|
+
.idea/
|
|
321
|
+
*.jks
|
|
322
|
+
*.keystore
|
|
323
|
+
.externalNativeBuild
|
|
324
|
+
.cxx/
|
|
325
|
+
google-services.json
|
|
326
|
+
vcs.xml
|
|
327
|
+
`
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function generateReadme(ctx: TemplateContext): GeneratedFile {
|
|
332
|
+
const lang = ctx.language === 'kotlin' ? 'Kotlin' : 'Java';
|
|
333
|
+
const ui = ctx.uiFramework === 'compose' ? 'Jetpack Compose' : 'XML Layouts';
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
path: 'README.md',
|
|
337
|
+
content: `# ${ctx.appName}
|
|
338
|
+
|
|
339
|
+
An Android application built with ${lang} using ${ui}.
|
|
340
|
+
|
|
341
|
+
## Requirements
|
|
342
|
+
- Android Studio Hedgehog (2023.1.1) or later
|
|
343
|
+
- JDK 17 or later
|
|
344
|
+
- Android SDK API ${ctx.android.targetSdk}
|
|
345
|
+
- Gradle ${ctx.gradle.gradleVersion}
|
|
346
|
+
|
|
347
|
+
## Getting Started
|
|
348
|
+
1. Open the project in Android Studio
|
|
349
|
+
2. Sync Gradle files
|
|
350
|
+
3. Build and run
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
MIT License
|
|
354
|
+
`
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function generateGradleWrapperProperties(ctx: TemplateContext): GeneratedFile {
|
|
359
|
+
return {
|
|
360
|
+
path: 'gradle/wrapper/gradle-wrapper.properties',
|
|
361
|
+
content: `distributionBase=GRADLE_USER_HOME
|
|
362
|
+
distributionPath=wrapper/dists
|
|
363
|
+
distributionUrl=https\\://services.gradle.org/distributions/gradle-${ctx.gradle.gradleVersion}-bin.zip
|
|
364
|
+
networkTimeout=10000
|
|
365
|
+
validateDistributionUrl=true
|
|
366
|
+
zipStoreBase=GRADLE_USER_HOME
|
|
367
|
+
zipStorePath=wrapper/dists
|
|
368
|
+
`
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function generateGradlewBat(_ctx: TemplateContext): GeneratedFile {
|
|
373
|
+
return {
|
|
374
|
+
path: 'gradlew.bat',
|
|
375
|
+
content: `@echo off
|
|
376
|
+
setlocal enabledelayedexpansion
|
|
377
|
+
set DIRNAME=%~dp0
|
|
378
|
+
if "%DIRNAME%"=="" set DIRNAME=.
|
|
379
|
+
set APP_BASE_NAME=%~n0
|
|
380
|
+
set APP_HOME=%DIRNAME%
|
|
381
|
+
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
|
382
|
+
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
|
383
|
+
set JAVA_EXE=java.exe
|
|
384
|
+
%JAVA_EXE% -version >NUL 2>&1
|
|
385
|
+
if %ERRORLEVEL% equ 0 goto execute
|
|
386
|
+
echo ERROR: JAVA_HOME is not set
|
|
387
|
+
goto fail
|
|
388
|
+
:execute
|
|
389
|
+
set CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar
|
|
390
|
+
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
|
391
|
+
:end
|
|
392
|
+
if %ERRORLEVEL% equ 0 goto mainEnd
|
|
393
|
+
:fail
|
|
394
|
+
exit /b 1
|
|
395
|
+
:mainEnd
|
|
396
|
+
endlocal
|
|
397
|
+
`
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function generateGradlewUnix(_ctx: TemplateContext): GeneratedFile {
|
|
402
|
+
return {
|
|
403
|
+
path: 'gradlew',
|
|
404
|
+
content: `#!/bin/sh
|
|
405
|
+
|
|
406
|
+
APP_HOME=$(dirname "$(cd "$(dirname "$0")" && pwd)")
|
|
407
|
+
CLASSPATH="$APP_HOME/gradle/wrapper/gradle-wrapper.jar"
|
|
408
|
+
JAVA_HOME="${'$'}{JAVA_HOME:-}"
|
|
409
|
+
JAVACMD="${'$'}{JAVACMD:-java}"
|
|
410
|
+
|
|
411
|
+
if [ ! -x "$JAVACMD" ] && [ -n "$JAVA_HOME" ]; then
|
|
412
|
+
JAVACMD="$JAVA_HOME/bin/java"
|
|
413
|
+
fi
|
|
414
|
+
|
|
415
|
+
if [ ! -x "$JAVACMD" ]; then
|
|
416
|
+
echo "ERROR: JAVA_HOME is not set and no 'java' command could be found"
|
|
417
|
+
exit 1
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
DEFAULT_JVM_OPTS="-Xmx64m -Xms64m"
|
|
421
|
+
|
|
422
|
+
exec "$JAVACMD" $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "-Dorg.gradle.appname=$(basename "$0")" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
|
423
|
+
`
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function generateAppBuildGradle(ctx: TemplateContext): GeneratedFile {
|
|
428
|
+
const isCompose = ctx.uiFramework === 'compose';
|
|
429
|
+
const isKotlin = ctx.language === 'kotlin';
|
|
430
|
+
|
|
431
|
+
let plugins = 'plugins {\n id("com.android.application")\n';
|
|
432
|
+
if (isKotlin) {
|
|
433
|
+
plugins += ' id("org.jetbrains.kotlin.android")\n';
|
|
434
|
+
if (isCompose) {
|
|
435
|
+
plugins += ' id("org.jetbrains.kotlin.plugin.compose")\n';
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
plugins += '}\n';
|
|
439
|
+
|
|
440
|
+
let config = `android {
|
|
441
|
+
namespace = "${ctx.packageName}"
|
|
442
|
+
compileSdk = ${ctx.android.compileSdk}
|
|
443
|
+
|
|
444
|
+
defaultConfig {
|
|
445
|
+
applicationId = "${ctx.packageName}"
|
|
446
|
+
minSdk = ${ctx.android.minSdk}
|
|
447
|
+
targetSdk = ${ctx.android.targetSdk}
|
|
448
|
+
versionCode = 1
|
|
449
|
+
versionName = "1.0.0"
|
|
450
|
+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
|
451
|
+
`;
|
|
452
|
+
|
|
453
|
+
if (isCompose) {
|
|
454
|
+
config += ` vectorDrawables {
|
|
455
|
+
useSupportLibrary = true
|
|
456
|
+
}
|
|
457
|
+
`;
|
|
458
|
+
}
|
|
459
|
+
config += ` }
|
|
460
|
+
|
|
461
|
+
buildTypes {
|
|
462
|
+
release {
|
|
463
|
+
isMinifyEnabled = true
|
|
464
|
+
proguardFiles(
|
|
465
|
+
getDefaultProguardFile("proguard-android-optimize.txt"),
|
|
466
|
+
"proguard-rules.pro"
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
compileOptions {
|
|
472
|
+
sourceCompatibility = JavaVersion.VERSION_17
|
|
473
|
+
targetCompatibility = JavaVersion.VERSION_17
|
|
474
|
+
}
|
|
475
|
+
`;
|
|
476
|
+
|
|
477
|
+
if (isKotlin) {
|
|
478
|
+
config += ` kotlinOptions {
|
|
479
|
+
jvmTarget = "17"
|
|
480
|
+
}
|
|
481
|
+
`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (isCompose) {
|
|
485
|
+
config += ` buildFeatures {
|
|
486
|
+
compose = true
|
|
487
|
+
}
|
|
488
|
+
`;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
config += ` packaging {
|
|
492
|
+
resources {
|
|
493
|
+
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
dependencies {
|
|
499
|
+
implementation("androidx.core:core-ktx:1.13.1")
|
|
500
|
+
implementation("androidx.appcompat:appcompat:1.7.0")
|
|
501
|
+
implementation("com.google.android.material:material:1.12.0")
|
|
502
|
+
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
|
503
|
+
`;
|
|
504
|
+
|
|
505
|
+
if (isCompose) {
|
|
506
|
+
config += ` implementation(platform("androidx.compose:compose-bom:2025.01.00"))
|
|
507
|
+
implementation("androidx.compose.ui:ui")
|
|
508
|
+
implementation("androidx.compose.ui:ui-graphics")
|
|
509
|
+
implementation("androidx.compose.ui:ui-tooling-preview")
|
|
510
|
+
implementation("androidx.compose.material3:material3")
|
|
511
|
+
implementation("androidx.compose.foundation:foundation")
|
|
512
|
+
implementation("androidx.activity:activity-compose:1.9.3")
|
|
513
|
+
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
|
|
514
|
+
implementation("androidx.navigation:navigation-compose:2.8.4")
|
|
515
|
+
debugImplementation("androidx.compose.ui:ui-tooling")
|
|
516
|
+
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
|
517
|
+
`;
|
|
518
|
+
} else {
|
|
519
|
+
config += ` implementation("androidx.activity:activity-ktx:1.9.2")
|
|
520
|
+
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.5")
|
|
521
|
+
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5")
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Add native C++ configuration if needed
|
|
526
|
+
if (ctx.template === 'native-cpp') {
|
|
527
|
+
const ndkVersion = ctx.nativeCpp?.ndkVersion ?? '28.2.13676358';
|
|
528
|
+
const abiFilters = ctx.nativeCpp?.abiFilters?.join(', ') ?? '"armeabi-v7a", "arm64-v8a", "x86", "x86_64"';
|
|
529
|
+
|
|
530
|
+
config = config.replace(
|
|
531
|
+
'kotlinOptions {',
|
|
532
|
+
`externalNativeBuild {
|
|
533
|
+
cmake {
|
|
534
|
+
cppFlags += "-std=c++17"
|
|
535
|
+
arguments += "-DANDROID_STL=c++_static"
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
ndk {
|
|
540
|
+
abiFilters += [${abiFilters.split(', ').map((a: string) => `"${a.replace(/"/g, '')}"`).join(', ')}]
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
kotlinOptions {`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
config += ` testImplementation("junit:junit:4.13.2")
|
|
548
|
+
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
|
549
|
+
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
|
550
|
+
}
|
|
551
|
+
`;
|
|
552
|
+
|
|
553
|
+
return {
|
|
554
|
+
path: 'app/build.gradle.kts',
|
|
555
|
+
content: plugins + config
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function generateAppProguardRules(_ctx: TemplateContext): GeneratedFile {
|
|
560
|
+
return {
|
|
561
|
+
path: 'app/proguard-rules.pro',
|
|
562
|
+
content: `# ProGuard rules for Android
|
|
563
|
+
-keepattributes SourceFile,LineNumberTable
|
|
564
|
+
-renamesourcefileattribute SourceFile
|
|
565
|
+
-keep class kotlin.** { *; }
|
|
566
|
+
-keep class kotlin.Metadata { *; }
|
|
567
|
+
-dontwarn kotlin.**
|
|
568
|
+
-keep class androidx.** { *; }
|
|
569
|
+
-keep interface androidx.** { *; }
|
|
570
|
+
`
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function generateAppManifest(ctx: TemplateContext): GeneratedFile {
|
|
575
|
+
const activityClass = ctx.language === 'kotlin' ? 'MainActivity' : '.MainActivity';
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
path: 'app/src/main/AndroidManifest.xml',
|
|
579
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
580
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
581
|
+
|
|
582
|
+
<application
|
|
583
|
+
android:allowBackup="true"
|
|
584
|
+
android:icon="@mipmap/ic_launcher"
|
|
585
|
+
android:label="@string/app_name"
|
|
586
|
+
android:roundIcon="@mipmap/ic_launcher_round"
|
|
587
|
+
android:supportsRtl="true"
|
|
588
|
+
android:theme="@style/Theme.${ctx.appNamePascal}">
|
|
589
|
+
|
|
590
|
+
<activity
|
|
591
|
+
android:name="${activityClass}"
|
|
592
|
+
android:exported="true"
|
|
593
|
+
android:theme="@style/Theme.${ctx.appNamePascal}">
|
|
594
|
+
<intent-filter>
|
|
595
|
+
<action android:name="android.intent.action.MAIN" />
|
|
596
|
+
<category android:name="android.intent.category.LAUNCHER" />
|
|
597
|
+
</intent-filter>
|
|
598
|
+
</activity>
|
|
599
|
+
|
|
600
|
+
</application>
|
|
601
|
+
|
|
602
|
+
</manifest>
|
|
603
|
+
`
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function generateApplicationClass(ctx: TemplateContext): GeneratedFile {
|
|
608
|
+
const packagePath = ctx.packagePath;
|
|
609
|
+
const className = `${ctx.appNamePascal}Application`;
|
|
610
|
+
|
|
611
|
+
if (ctx.language === 'kotlin') {
|
|
612
|
+
return {
|
|
613
|
+
path: `app/src/main/kotlin/${packagePath}/${className}.kt`,
|
|
614
|
+
content: `package ${ctx.packageName}
|
|
615
|
+
|
|
616
|
+
import android.app.Application
|
|
617
|
+
|
|
618
|
+
class ${className} : Application() {
|
|
619
|
+
override fun onCreate() {
|
|
620
|
+
super.onCreate()
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
`
|
|
624
|
+
};
|
|
625
|
+
} else {
|
|
626
|
+
return {
|
|
627
|
+
path: `app/src/main/java/${packagePath}/${className}.java`,
|
|
628
|
+
content: `package ${ctx.packageName};
|
|
629
|
+
|
|
630
|
+
import android.app.Application;
|
|
631
|
+
|
|
632
|
+
public class ${className} extends Application {
|
|
633
|
+
@Override
|
|
634
|
+
public void onCreate() {
|
|
635
|
+
super.onCreate();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
`
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function generateMainActivity(ctx: TemplateContext): GeneratedFile {
|
|
644
|
+
const packagePath = ctx.packagePath;
|
|
645
|
+
|
|
646
|
+
if (ctx.uiFramework === 'compose' && ctx.language === 'kotlin') {
|
|
647
|
+
return {
|
|
648
|
+
path: `app/src/main/kotlin/${packagePath}/MainActivity.kt`,
|
|
649
|
+
content: `package ${ctx.packageName}
|
|
650
|
+
|
|
651
|
+
import android.os.Bundle
|
|
652
|
+
import androidx.activity.ComponentActivity
|
|
653
|
+
import androidx.activity.compose.setContent
|
|
654
|
+
import androidx.activity.enableEdgeToEdge
|
|
655
|
+
import androidx.compose.foundation.background
|
|
656
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
657
|
+
import androidx.compose.foundation.layout.Column
|
|
658
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
659
|
+
import androidx.compose.foundation.layout.padding
|
|
660
|
+
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
661
|
+
import androidx.compose.foundation.layout.size
|
|
662
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
663
|
+
import androidx.compose.material3.Card
|
|
664
|
+
import androidx.compose.material3.CardDefaults
|
|
665
|
+
import androidx.compose.material3.MaterialTheme
|
|
666
|
+
import androidx.compose.material3.Surface
|
|
667
|
+
import androidx.compose.material3.Text
|
|
668
|
+
import androidx.compose.runtime.Composable
|
|
669
|
+
import androidx.compose.runtime.getValue
|
|
670
|
+
import androidx.compose.runtime.mutableIntStateOf
|
|
671
|
+
import androidx.compose.runtime.remember
|
|
672
|
+
import androidx.compose.runtime.setValue
|
|
673
|
+
import androidx.compose.ui.Alignment
|
|
674
|
+
import androidx.compose.ui.Modifier
|
|
675
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
676
|
+
import androidx.compose.ui.unit.dp
|
|
677
|
+
import androidx.compose.ui.unit.sp
|
|
678
|
+
import ${ctx.packageName}.ui.theme.${ctx.appNamePascal}Theme
|
|
679
|
+
|
|
680
|
+
class MainActivity : ComponentActivity() {
|
|
681
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
682
|
+
super.onCreate(savedInstanceState)
|
|
683
|
+
enableEdgeToEdge()
|
|
684
|
+
setContent {
|
|
685
|
+
${ctx.appNamePascal}Theme {
|
|
686
|
+
MainScreen()
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
@Composable
|
|
693
|
+
fun MainScreen() {
|
|
694
|
+
var count by remember { mutableIntStateOf(0) }
|
|
695
|
+
|
|
696
|
+
Surface(
|
|
697
|
+
modifier = Modifier.fillMaxSize(),
|
|
698
|
+
color = MaterialTheme.colorScheme.background
|
|
699
|
+
) {
|
|
700
|
+
Column(
|
|
701
|
+
modifier = Modifier
|
|
702
|
+
.fillMaxSize()
|
|
703
|
+
.padding(24.dp)
|
|
704
|
+
.safeDrawingPadding(),
|
|
705
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
706
|
+
verticalArrangement = Arrangement.Center
|
|
707
|
+
) {
|
|
708
|
+
Text(
|
|
709
|
+
text = "${ctx.appNamePascal}",
|
|
710
|
+
style = MaterialTheme.typography.headlineMedium,
|
|
711
|
+
color = MaterialTheme.colorScheme.onBackground,
|
|
712
|
+
fontWeight = FontWeight.SemiBold
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
Text(
|
|
716
|
+
text = "$count",
|
|
717
|
+
style = MaterialTheme.typography.displayLarge,
|
|
718
|
+
color = MaterialTheme.colorScheme.primary,
|
|
719
|
+
fontWeight = FontWeight.Bold,
|
|
720
|
+
modifier = Modifier.padding(vertical = 32.dp)
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
Card(
|
|
724
|
+
onClick = { count++ },
|
|
725
|
+
modifier = Modifier.size(120.dp),
|
|
726
|
+
shape = RoundedCornerShape(60.dp),
|
|
727
|
+
colors = CardDefaults.cardColors(
|
|
728
|
+
containerColor = MaterialTheme.colorScheme.primary
|
|
729
|
+
)
|
|
730
|
+
) {
|
|
731
|
+
Column(
|
|
732
|
+
modifier = Modifier.fillMaxSize(),
|
|
733
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
734
|
+
verticalArrangement = Arrangement.Center
|
|
735
|
+
) {
|
|
736
|
+
Text(
|
|
737
|
+
text = "+",
|
|
738
|
+
fontSize = 48.sp,
|
|
739
|
+
fontWeight = FontWeight.Bold,
|
|
740
|
+
color = MaterialTheme.colorScheme.onPrimary
|
|
741
|
+
)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
Text(
|
|
746
|
+
text = "Tap button to count",
|
|
747
|
+
style = MaterialTheme.typography.bodyMedium,
|
|
748
|
+
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
749
|
+
modifier = Modifier.padding(top = 24.dp)
|
|
750
|
+
)
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
`
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (ctx.language === 'kotlin') {
|
|
759
|
+
return {
|
|
760
|
+
path: `app/src/main/kotlin/${packagePath}/MainActivity.kt`,
|
|
761
|
+
content: `package ${ctx.packageName}
|
|
762
|
+
|
|
763
|
+
import android.os.Bundle
|
|
764
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
765
|
+
import androidx.core.view.ViewCompat
|
|
766
|
+
import androidx.core.view.WindowCompat
|
|
767
|
+
import androidx.core.view.WindowInsetsCompat
|
|
768
|
+
import androidx.core.view.updatePadding
|
|
769
|
+
import ${ctx.packageName}.R
|
|
770
|
+
|
|
771
|
+
class MainActivity : AppCompatActivity() {
|
|
772
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
773
|
+
super.onCreate(savedInstanceState)
|
|
774
|
+
|
|
775
|
+
// Enable edge-to-edge
|
|
776
|
+
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
777
|
+
|
|
778
|
+
setContentView(R.layout.activity_main)
|
|
779
|
+
|
|
780
|
+
// Apply window insets for edge-to-edge
|
|
781
|
+
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content)) { view, windowInsets ->
|
|
782
|
+
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
783
|
+
view.updatePadding(
|
|
784
|
+
left = insets.left,
|
|
785
|
+
right = insets.right
|
|
786
|
+
)
|
|
787
|
+
windowInsets
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
`
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
return {
|
|
796
|
+
path: `app/src/main/java/${packagePath}/MainActivity.java`,
|
|
797
|
+
content: `package ${ctx.packageName};
|
|
798
|
+
|
|
799
|
+
import android.os.Bundle;
|
|
800
|
+
import androidx.appcompat.app.AppCompatActivity;
|
|
801
|
+
import androidx.core.view.ViewCompat;
|
|
802
|
+
import androidx.core.view.WindowCompat;
|
|
803
|
+
import androidx.core.view.WindowInsetsCompat;
|
|
804
|
+
import androidx.core.view.WindowInsetsControllerCompat;
|
|
805
|
+
import ${ctx.packageName}.R;
|
|
806
|
+
|
|
807
|
+
public class MainActivity extends AppCompatActivity {
|
|
808
|
+
@Override
|
|
809
|
+
protected void onCreate(Bundle savedInstanceState) {
|
|
810
|
+
super.onCreate(savedInstanceState);
|
|
811
|
+
|
|
812
|
+
// Enable edge-to-edge
|
|
813
|
+
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
|
814
|
+
|
|
815
|
+
setContentView(R.layout.activity_main);
|
|
816
|
+
|
|
817
|
+
// Apply window insets for edge-to-edge
|
|
818
|
+
ViewCompat.setOnApplyWindowInsetsListener(findViewById(android.R.id.content), (view, windowInsets) -> {
|
|
819
|
+
var insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
|
820
|
+
view.setPadding(insets.left, insets.top, insets.right, insets.bottom);
|
|
821
|
+
return WindowInsetsCompat.CONSUMED;
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
`
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function generateStrings(ctx: TemplateContext): GeneratedFile {
|
|
830
|
+
const displayName = ctx.appName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
831
|
+
|
|
832
|
+
return {
|
|
833
|
+
path: 'app/src/main/res/values/strings.xml',
|
|
834
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
835
|
+
<resources>
|
|
836
|
+
<string name="app_name">${displayName}</string>
|
|
837
|
+
<string name="app_name_short">${ctx.appNamePascal}</string>
|
|
838
|
+
<string name="content_description_app_icon">Application icon</string>
|
|
839
|
+
</resources>
|
|
840
|
+
`
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function generateColors(_ctx: TemplateContext): GeneratedFile {
|
|
845
|
+
return {
|
|
846
|
+
path: 'app/src/main/res/values/colors.xml',
|
|
847
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
848
|
+
<resources>
|
|
849
|
+
<!-- Primary Colors -->
|
|
850
|
+
<color name="primary">#6750A4</color>
|
|
851
|
+
<color name="primary_variant">#7F67BE</color>
|
|
852
|
+
<color name="on_primary">#FFFFFF</color>
|
|
853
|
+
|
|
854
|
+
<!-- Secondary Colors -->
|
|
855
|
+
<color name="secondary">#625B71</color>
|
|
856
|
+
<color name="secondary_variant">#7A7289</color>
|
|
857
|
+
<color name="on_secondary">#FFFFFF</color>
|
|
858
|
+
|
|
859
|
+
<!-- Tertiary Colors -->
|
|
860
|
+
<color name="tertiary">#7D5260</color>
|
|
861
|
+
<color name="on_tertiary">#FFFFFF</color>
|
|
862
|
+
|
|
863
|
+
<!-- Background Colors -->
|
|
864
|
+
<color name="background">#FFFBFE</color>
|
|
865
|
+
<color name="on_background">#1C1B1F</color>
|
|
866
|
+
<color name="surface">#FFFBFE</color>
|
|
867
|
+
<color name="on_surface">#1C1B1F</color>
|
|
868
|
+
|
|
869
|
+
<!-- Status Colors -->
|
|
870
|
+
<color name="error">#B3261E</color>
|
|
871
|
+
<color name="on_error">#FFFFFF</color>
|
|
872
|
+
<color name="outline">#79747E</color>
|
|
873
|
+
|
|
874
|
+
<!-- Icon Colors -->
|
|
875
|
+
<color name="ic_launcher_background">#6750A4</color>
|
|
876
|
+
</resources>
|
|
877
|
+
`
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function generateThemes(ctx: TemplateContext): GeneratedFile {
|
|
882
|
+
const themeName = `Theme.${ctx.appNamePascal}`;
|
|
883
|
+
|
|
884
|
+
if (ctx.uiFramework === 'compose') {
|
|
885
|
+
return {
|
|
886
|
+
path: 'app/src/main/res/values/themes.xml',
|
|
887
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
888
|
+
<resources>
|
|
889
|
+
<style name="${themeName}" parent="Theme.Material3.Light.NoActionBar">
|
|
890
|
+
<item name="android:statusBarColor">@android:color/transparent</item>
|
|
891
|
+
<item name="android:navigationBarColor">@android:color/transparent</item>
|
|
892
|
+
<item name="android:windowLightStatusBar">true</item>
|
|
893
|
+
<item name="android:windowLightNavigationBar">true</item>
|
|
894
|
+
</style>
|
|
895
|
+
</resources>
|
|
896
|
+
`
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
return {
|
|
901
|
+
path: 'app/src/main/res/values/themes.xml',
|
|
902
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
903
|
+
<resources>
|
|
904
|
+
<style name="${themeName}" parent="Theme.Material3.Light.NoActionBar">
|
|
905
|
+
<item name="colorPrimary">@color/primary</item>
|
|
906
|
+
<item name="colorOnPrimary">@color/on_primary</item>
|
|
907
|
+
<item name="colorPrimaryContainer">@color/primary_variant</item>
|
|
908
|
+
<item name="colorSecondary">@color/secondary</item>
|
|
909
|
+
<item name="colorOnSecondary">@color/on_secondary</item>
|
|
910
|
+
<item name="colorTertiary">@color/tertiary</item>
|
|
911
|
+
<item name="colorOnTertiary">@color/on_tertiary</item>
|
|
912
|
+
<item name="android:colorBackground">@color/background</item>
|
|
913
|
+
<item name="colorOnBackground">@color/on_background</item>
|
|
914
|
+
<item name="colorSurface">@color/surface</item>
|
|
915
|
+
<item name="colorOnSurface">@color/on_surface</item>
|
|
916
|
+
<item name="colorError">@color/error</item>
|
|
917
|
+
<item name="colorOnError">@color/on_error</item>
|
|
918
|
+
<item name="colorOutline">@color/outline</item>
|
|
919
|
+
<item name="android:statusBarColor">@color/surface</item>
|
|
920
|
+
<item name="android:navigationBarColor">@color/surface</item>
|
|
921
|
+
<item name="android:windowLightStatusBar">true</item>
|
|
922
|
+
<item name="android:windowLightNavigationBar">true</item>
|
|
923
|
+
</style>
|
|
924
|
+
</resources>
|
|
925
|
+
`
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function generateAppIcon(_ctx: TemplateContext): GeneratedFile {
|
|
930
|
+
return {
|
|
931
|
+
path: 'app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml',
|
|
932
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
933
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
934
|
+
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
935
|
+
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
936
|
+
</adaptive-icon>
|
|
937
|
+
`
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function generateActivityLayout(ctx: TemplateContext): GeneratedFile {
|
|
942
|
+
if (ctx.uiFramework === 'compose') {
|
|
943
|
+
return {
|
|
944
|
+
path: 'app/src/main/res/layout/activity_main.xml',
|
|
945
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
946
|
+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
947
|
+
android:layout_width="match_parent"
|
|
948
|
+
android:layout_height="match_parent"
|
|
949
|
+
android:background="@color/background">
|
|
950
|
+
|
|
951
|
+
<TextView
|
|
952
|
+
android:id="@+id/textView"
|
|
953
|
+
android:layout_width="wrap_content"
|
|
954
|
+
android:layout_height="wrap_content"
|
|
955
|
+
android:layout_gravity="center"
|
|
956
|
+
android:text="@string/app_name"
|
|
957
|
+
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
|
958
|
+
android:textColor="@color/on_background" />
|
|
959
|
+
|
|
960
|
+
</FrameLayout>
|
|
961
|
+
`
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
return {
|
|
966
|
+
path: 'app/src/main/res/layout/activity_main.xml',
|
|
967
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
968
|
+
<androidx.constraintlayout.widget.ConstraintLayout
|
|
969
|
+
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
970
|
+
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
971
|
+
xmlns:tools="http://schemas.android.com/tools"
|
|
972
|
+
android:layout_width="match_parent"
|
|
973
|
+
android:layout_height="match_parent"
|
|
974
|
+
android:background="@color/background"
|
|
975
|
+
tools:context=".MainActivity">
|
|
976
|
+
|
|
977
|
+
<TextView
|
|
978
|
+
android:id="@+id/textView"
|
|
979
|
+
android:layout_width="wrap_content"
|
|
980
|
+
android:layout_height="wrap_content"
|
|
981
|
+
android:text="@string/app_name"
|
|
982
|
+
android:textAppearance="?attr/textAppearanceHeadlineMedium"
|
|
983
|
+
android:textColor="@color/on_background"
|
|
984
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
985
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
986
|
+
app:layout_constraintStart_toStartOf="parent"
|
|
987
|
+
app:layout_constraintTop_toTopOf="parent" />
|
|
988
|
+
|
|
989
|
+
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
|
990
|
+
android:id="@+id/fab"
|
|
991
|
+
android:layout_width="wrap_content"
|
|
992
|
+
android:layout_height="wrap_content"
|
|
993
|
+
android:layout_margin="16dp"
|
|
994
|
+
android:contentDescription="@string/app_name_short"
|
|
995
|
+
android:src="@drawable/ic_add"
|
|
996
|
+
app:layout_constraintBottom_toBottomOf="parent"
|
|
997
|
+
app:layout_constraintEnd_toEndOf="parent"
|
|
998
|
+
app:tint="@color/on_primary" />
|
|
999
|
+
|
|
1000
|
+
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
1001
|
+
`
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function generateSourceSetFiles(ctx: TemplateContext): GeneratedFile[] {
|
|
1006
|
+
const files: GeneratedFile[] = [];
|
|
1007
|
+
const packagePath = ctx.packagePath;
|
|
1008
|
+
|
|
1009
|
+
// Professional adaptive icon background
|
|
1010
|
+
files.push({
|
|
1011
|
+
path: 'app/src/main/res/drawable/ic_launcher_background.xml',
|
|
1012
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
1013
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1014
|
+
android:width="108dp"
|
|
1015
|
+
android:height="108dp"
|
|
1016
|
+
android:viewportWidth="108"
|
|
1017
|
+
android:viewportHeight="108">
|
|
1018
|
+
<path
|
|
1019
|
+
android:fillColor="#6750A4"
|
|
1020
|
+
android:pathData="M0,0h108v108h-108z"/>
|
|
1021
|
+
</vector>
|
|
1022
|
+
`
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Professional icon foreground - Android robot stylized "A"
|
|
1026
|
+
files.push({
|
|
1027
|
+
path: 'app/src/main/res/drawable/ic_launcher_foreground.xml',
|
|
1028
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
1029
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1030
|
+
android:width="108dp"
|
|
1031
|
+
android:height="108dp"
|
|
1032
|
+
android:viewportWidth="108"
|
|
1033
|
+
android:viewportHeight="108">
|
|
1034
|
+
<!-- Android robot head stylized as "A" -->
|
|
1035
|
+
<group android:translateX="27" android:translateY="27">
|
|
1036
|
+
<!-- Circle background -->
|
|
1037
|
+
<path
|
|
1038
|
+
android:fillColor="#FFFFFF"
|
|
1039
|
+
android:pathData="M27,27m-24,0a24,24 0,1 1,48 0a24,24 0,1 1,-48 0"/>
|
|
1040
|
+
<!-- A shape -->
|
|
1041
|
+
<path
|
|
1042
|
+
android:fillColor="#6750A4"
|
|
1043
|
+
android:pathData="M27,42L17,22h4l6,12 6,-12h4L27,42zM24,35l2.5,-5 2.5,5h-5z"/>
|
|
1044
|
+
<!-- Antenna -->
|
|
1045
|
+
<path
|
|
1046
|
+
android:strokeColor="#6750A4"
|
|
1047
|
+
android:strokeWidth="2"
|
|
1048
|
+
android:pathData="M20,18L12,10"/>
|
|
1049
|
+
<path
|
|
1050
|
+
android:strokeColor="#6750A4"
|
|
1051
|
+
android:strokeWidth="2"
|
|
1052
|
+
android:pathData="M34,18L42,10"/>
|
|
1053
|
+
</group>
|
|
1054
|
+
</vector>
|
|
1055
|
+
`
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
files.push({
|
|
1059
|
+
path: 'app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml',
|
|
1060
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
1061
|
+
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
1062
|
+
<background android:drawable="@drawable/ic_launcher_background"/>
|
|
1063
|
+
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
|
|
1064
|
+
</adaptive-icon>
|
|
1065
|
+
`
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
// Add icon for FAB
|
|
1069
|
+
files.push({
|
|
1070
|
+
path: 'app/src/main/res/drawable/ic_add.xml',
|
|
1071
|
+
content: `<?xml version="1.0" encoding="utf-8"?>
|
|
1072
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
1073
|
+
android:width="24dp"
|
|
1074
|
+
android:height="24dp"
|
|
1075
|
+
android:viewportWidth="24"
|
|
1076
|
+
android:viewportHeight="24">
|
|
1077
|
+
<path
|
|
1078
|
+
android:fillColor="#FFFFFF"
|
|
1079
|
+
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
|
1080
|
+
</vector>
|
|
1081
|
+
`
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
if (ctx.uiFramework === 'compose') {
|
|
1085
|
+
files.push({
|
|
1086
|
+
path: `app/src/main/kotlin/${packagePath}/ui/theme/Color.kt`,
|
|
1087
|
+
content: `package ${ctx.packageName}.ui.theme
|
|
1088
|
+
|
|
1089
|
+
import androidx.compose.ui.graphics.Color
|
|
1090
|
+
|
|
1091
|
+
// Light theme primary colors
|
|
1092
|
+
val Purple40 = Color(0xFF6750A4)
|
|
1093
|
+
val PurpleGrey40 = Color(0xFF625B71)
|
|
1094
|
+
val Pink40 = Color(0xFF7D5260)
|
|
1095
|
+
|
|
1096
|
+
// Dark theme primary colors
|
|
1097
|
+
val Purple80 = Color(0xFFD0BCFF)
|
|
1098
|
+
val PurpleGrey80 = Color(0xFFCCC2DC)
|
|
1099
|
+
val Pink80 = Color(0xFFEFB8C8)
|
|
1100
|
+
|
|
1101
|
+
// Custom brand colors
|
|
1102
|
+
val Teal40 = Color(0xFF006B5B)
|
|
1103
|
+
val Teal80 = Color(0xFF8BD8CE)
|
|
1104
|
+
`
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
files.push({
|
|
1108
|
+
path: `app/src/main/kotlin/${packagePath}/ui/theme/Theme.kt`,
|
|
1109
|
+
content: `package ${ctx.packageName}.ui.theme
|
|
1110
|
+
|
|
1111
|
+
import android.app.Activity
|
|
1112
|
+
import android.os.Build
|
|
1113
|
+
import androidx.compose.foundation.isSystemInDarkTheme
|
|
1114
|
+
import androidx.compose.material3.MaterialTheme
|
|
1115
|
+
import androidx.compose.material3.darkColorScheme
|
|
1116
|
+
import androidx.compose.material3.dynamicDarkColorScheme
|
|
1117
|
+
import androidx.compose.material3.dynamicLightColorScheme
|
|
1118
|
+
import androidx.compose.material3.lightColorScheme
|
|
1119
|
+
import androidx.compose.runtime.Composable
|
|
1120
|
+
import androidx.compose.runtime.SideEffect
|
|
1121
|
+
import androidx.compose.ui.graphics.Color
|
|
1122
|
+
import androidx.compose.ui.graphics.toArgb
|
|
1123
|
+
import androidx.compose.ui.platform.LocalView
|
|
1124
|
+
import androidx.core.view.WindowCompat
|
|
1125
|
+
|
|
1126
|
+
private val DarkColorScheme = darkColorScheme(
|
|
1127
|
+
primary = Purple80,
|
|
1128
|
+
onPrimary = Color(0xFF381E72),
|
|
1129
|
+
primaryContainer = Color(0xFF4F378B),
|
|
1130
|
+
onPrimaryContainer = Color(0xFFEADDFF),
|
|
1131
|
+
secondary = PurpleGrey80,
|
|
1132
|
+
onSecondary = Color(0xFF332D41),
|
|
1133
|
+
secondaryContainer = Color(0xFF4A4458),
|
|
1134
|
+
onSecondaryContainer = Color(0xFFE8DEF8),
|
|
1135
|
+
tertiary = Pink80,
|
|
1136
|
+
onTertiary = Color(0xFF492532),
|
|
1137
|
+
tertiaryContainer = Color(0xFF633B48),
|
|
1138
|
+
onTertiaryContainer = Color(0xFFFFD8E4),
|
|
1139
|
+
background = Color(0xFF1C1B1F),
|
|
1140
|
+
onBackground = Color(0xFFE6E1E5),
|
|
1141
|
+
surface = Color(0xFF1C1B1F),
|
|
1142
|
+
onSurface = Color(0xFFE6E1E5),
|
|
1143
|
+
surfaceVariant = Color(0xFF49454F),
|
|
1144
|
+
onSurfaceVariant = Color(0xFFCAC4D0)
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
private val LightColorScheme = lightColorScheme(
|
|
1148
|
+
primary = Purple40,
|
|
1149
|
+
onPrimary = Color.White,
|
|
1150
|
+
primaryContainer = Color(0xFFEADDFF),
|
|
1151
|
+
onPrimaryContainer = Color(0xFF21005D),
|
|
1152
|
+
secondary = PurpleGrey40,
|
|
1153
|
+
onSecondary = Color.White,
|
|
1154
|
+
secondaryContainer = Color(0xFFE8DEF8),
|
|
1155
|
+
onSecondaryContainer = Color(0xFF1D192B),
|
|
1156
|
+
tertiary = Pink40,
|
|
1157
|
+
onTertiary = Color.White,
|
|
1158
|
+
tertiaryContainer = Color(0xFFFFD8E4),
|
|
1159
|
+
onTertiaryContainer = Color(0xFF31111D),
|
|
1160
|
+
background = Color(0xFFFFFBFE),
|
|
1161
|
+
onBackground = Color(0xFF1C1B1F),
|
|
1162
|
+
surface = Color(0xFFFFFBFE),
|
|
1163
|
+
onSurface = Color(0xFF1C1B1F),
|
|
1164
|
+
surfaceVariant = Color(0xFFE7E0EC),
|
|
1165
|
+
onSurfaceVariant = Color(0xFF49454F)
|
|
1166
|
+
)
|
|
1167
|
+
|
|
1168
|
+
@Composable
|
|
1169
|
+
fun ${ctx.appNamePascal}Theme(
|
|
1170
|
+
darkTheme: Boolean = isSystemInDarkTheme(),
|
|
1171
|
+
dynamicColor: Boolean = false,
|
|
1172
|
+
content: @Composable () -> Unit
|
|
1173
|
+
) {
|
|
1174
|
+
val colorScheme = when {
|
|
1175
|
+
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
|
1176
|
+
if (darkTheme) dynamicDarkColorScheme(LocalView.current.context) else dynamicLightColorScheme(LocalView.current.context)
|
|
1177
|
+
}
|
|
1178
|
+
darkTheme -> DarkColorScheme
|
|
1179
|
+
else -> LightColorScheme
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
val view = LocalView.current
|
|
1183
|
+
if (!view.isInEditMode) {
|
|
1184
|
+
SideEffect {
|
|
1185
|
+
val window = (view.context as Activity).window
|
|
1186
|
+
window.statusBarColor = Color.Transparent.toArgb()
|
|
1187
|
+
window.navigationBarColor = Color.Transparent.toArgb()
|
|
1188
|
+
WindowCompat.getInsetsController(window, view).apply {
|
|
1189
|
+
isAppearanceLightStatusBars = !darkTheme
|
|
1190
|
+
isAppearanceLightNavigationBars = !darkTheme
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
MaterialTheme(
|
|
1196
|
+
colorScheme = colorScheme,
|
|
1197
|
+
typography = Typography,
|
|
1198
|
+
content = content
|
|
1199
|
+
)
|
|
1200
|
+
}
|
|
1201
|
+
`
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
files.push({
|
|
1205
|
+
path: `app/src/main/kotlin/${packagePath}/ui/theme/Type.kt`,
|
|
1206
|
+
content: `package ${ctx.packageName}.ui.theme
|
|
1207
|
+
|
|
1208
|
+
import androidx.compose.material3.Typography
|
|
1209
|
+
import androidx.compose.ui.text.TextStyle
|
|
1210
|
+
import androidx.compose.ui.text.font.FontFamily
|
|
1211
|
+
import androidx.compose.ui.text.font.FontWeight
|
|
1212
|
+
import androidx.compose.ui.unit.sp
|
|
1213
|
+
|
|
1214
|
+
val Typography = Typography(
|
|
1215
|
+
displayLarge = TextStyle(
|
|
1216
|
+
fontFamily = FontFamily.Default,
|
|
1217
|
+
fontWeight = FontWeight.Normal,
|
|
1218
|
+
fontSize = 57.sp,
|
|
1219
|
+
lineHeight = 64.sp,
|
|
1220
|
+
letterSpacing = (-0.25).sp
|
|
1221
|
+
),
|
|
1222
|
+
displayMedium = TextStyle(
|
|
1223
|
+
fontFamily = FontFamily.Default,
|
|
1224
|
+
fontWeight = FontWeight.Normal,
|
|
1225
|
+
fontSize = 45.sp,
|
|
1226
|
+
lineHeight = 52.sp,
|
|
1227
|
+
letterSpacing = 0.sp
|
|
1228
|
+
),
|
|
1229
|
+
displaySmall = TextStyle(
|
|
1230
|
+
fontFamily = FontFamily.Default,
|
|
1231
|
+
fontWeight = FontWeight.Normal,
|
|
1232
|
+
fontSize = 36.sp,
|
|
1233
|
+
lineHeight = 44.sp,
|
|
1234
|
+
letterSpacing = 0.sp
|
|
1235
|
+
),
|
|
1236
|
+
headlineLarge = TextStyle(
|
|
1237
|
+
fontFamily = FontFamily.Default,
|
|
1238
|
+
fontWeight = FontWeight.SemiBold,
|
|
1239
|
+
fontSize = 32.sp,
|
|
1240
|
+
lineHeight = 40.sp,
|
|
1241
|
+
letterSpacing = 0.sp
|
|
1242
|
+
),
|
|
1243
|
+
headlineMedium = TextStyle(
|
|
1244
|
+
fontFamily = FontFamily.Default,
|
|
1245
|
+
fontWeight = FontWeight.SemiBold,
|
|
1246
|
+
fontSize = 28.sp,
|
|
1247
|
+
lineHeight = 36.sp,
|
|
1248
|
+
letterSpacing = 0.sp
|
|
1249
|
+
),
|
|
1250
|
+
headlineSmall = TextStyle(
|
|
1251
|
+
fontFamily = FontFamily.Default,
|
|
1252
|
+
fontWeight = FontWeight.SemiBold,
|
|
1253
|
+
fontSize = 24.sp,
|
|
1254
|
+
lineHeight = 32.sp,
|
|
1255
|
+
letterSpacing = 0.sp
|
|
1256
|
+
),
|
|
1257
|
+
titleLarge = TextStyle(
|
|
1258
|
+
fontFamily = FontFamily.Default,
|
|
1259
|
+
fontWeight = FontWeight.Medium,
|
|
1260
|
+
fontSize = 22.sp,
|
|
1261
|
+
lineHeight = 28.sp,
|
|
1262
|
+
letterSpacing = 0.sp
|
|
1263
|
+
),
|
|
1264
|
+
titleMedium = TextStyle(
|
|
1265
|
+
fontFamily = FontFamily.Default,
|
|
1266
|
+
fontWeight = FontWeight.Medium,
|
|
1267
|
+
fontSize = 16.sp,
|
|
1268
|
+
lineHeight = 24.sp,
|
|
1269
|
+
letterSpacing = 0.15.sp
|
|
1270
|
+
),
|
|
1271
|
+
titleSmall = TextStyle(
|
|
1272
|
+
fontFamily = FontFamily.Default,
|
|
1273
|
+
fontWeight = FontWeight.Medium,
|
|
1274
|
+
fontSize = 14.sp,
|
|
1275
|
+
lineHeight = 20.sp,
|
|
1276
|
+
letterSpacing = 0.1.sp
|
|
1277
|
+
),
|
|
1278
|
+
bodyLarge = TextStyle(
|
|
1279
|
+
fontFamily = FontFamily.Default,
|
|
1280
|
+
fontWeight = FontWeight.Normal,
|
|
1281
|
+
fontSize = 16.sp,
|
|
1282
|
+
lineHeight = 24.sp,
|
|
1283
|
+
letterSpacing = 0.5.sp
|
|
1284
|
+
),
|
|
1285
|
+
bodyMedium = TextStyle(
|
|
1286
|
+
fontFamily = FontFamily.Default,
|
|
1287
|
+
fontWeight = FontWeight.Normal,
|
|
1288
|
+
fontSize = 14.sp,
|
|
1289
|
+
lineHeight = 20.sp,
|
|
1290
|
+
letterSpacing = 0.25.sp
|
|
1291
|
+
),
|
|
1292
|
+
bodySmall = TextStyle(
|
|
1293
|
+
fontFamily = FontFamily.Default,
|
|
1294
|
+
fontWeight = FontWeight.Normal,
|
|
1295
|
+
fontSize = 12.sp,
|
|
1296
|
+
lineHeight = 16.sp,
|
|
1297
|
+
letterSpacing = 0.4.sp
|
|
1298
|
+
),
|
|
1299
|
+
labelLarge = TextStyle(
|
|
1300
|
+
fontFamily = FontFamily.Default,
|
|
1301
|
+
fontWeight = FontWeight.Medium,
|
|
1302
|
+
fontSize = 14.sp,
|
|
1303
|
+
lineHeight = 20.sp,
|
|
1304
|
+
letterSpacing = 0.1.sp
|
|
1305
|
+
),
|
|
1306
|
+
labelMedium = TextStyle(
|
|
1307
|
+
fontFamily = FontFamily.Default,
|
|
1308
|
+
fontWeight = FontWeight.Medium,
|
|
1309
|
+
fontSize = 12.sp,
|
|
1310
|
+
lineHeight = 16.sp,
|
|
1311
|
+
letterSpacing = 0.5.sp
|
|
1312
|
+
),
|
|
1313
|
+
labelSmall = TextStyle(
|
|
1314
|
+
fontFamily = FontFamily.Default,
|
|
1315
|
+
fontWeight = FontWeight.Medium,
|
|
1316
|
+
fontSize = 11.sp,
|
|
1317
|
+
lineHeight = 16.sp,
|
|
1318
|
+
letterSpacing = 0.5.sp
|
|
1319
|
+
)
|
|
1320
|
+
)
|
|
1321
|
+
`
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
return files;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function isKotlinActivity(ctx: TemplateContext): boolean {
|
|
1329
|
+
return ctx.language === 'kotlin';
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function getAndroidVersionName(apiLevel: number): string {
|
|
1333
|
+
const versions: Record<number, string> = {
|
|
1334
|
+
35: '15', 34: '14', 33: '13', 32: '12L', 31: '12',
|
|
1335
|
+
30: '11', 29: '10', 28: '9', 27: '8.1', 26: '8.0', 24: '7.0', 21: '5.0'
|
|
1336
|
+
};
|
|
1337
|
+
return versions[apiLevel] || 'Unknown';
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* Get directory contents
|
|
1342
|
+
*/
|
|
1343
|
+
export async function getDirectoryContents(path: string): Promise<string[]> {
|
|
1344
|
+
try {
|
|
1345
|
+
const items = await import('fs-extra').then(fse => fse.default.readdir(path));
|
|
1346
|
+
return items as string[];
|
|
1347
|
+
} catch (error) {
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
}
|