/* * YukiHookAPI Project Builder - A Xposed Project Builder by YukiHookAPI. * Copyright (C) 2019-2023 HighCapable * https://github.com/HighCapable/YukiHookAPI-ProjectBuilder * * This software is non-free but opensource software: you can redistribute it * and/or modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation; either * version 3 of the License, or any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * and eula along with this software. If not, see * * * This file is Created by fankes on 2022/7/21. */ const {system} = require('./libs/system'); const {locale} = require('./libs/locale'); const {fileSystem} = require('./libs/file-system'); const {unzip} = require('./libs/unzip'); const {dateTime} = require('./libs/date-time'); const {appWindow, appConfig} = require('./app-space'); /** * 创建项目事务命名空间 */ const transaction = { /** 项目模板名称 */ templateName: 'project-template', /** 当前项目创建路径 */ projectPath: '', /** 打开已创建的项目 */ openProject: () => { system.openExplorer(transaction.projectPath); }, /** * 开始任务 * @param configs 项目配置 */ start: (configs) => { appWindow.showOpenDirectory({ title: locale.i18n.selectProjectPath, message: locale.i18n.selectProjectPathTip, buttonLabel: locale.i18n.selectThisDirectory }, (result) => { if (!result.canceled) { transaction.projectPath = fileSystem.path(result.filePaths[0], configs.basicConfig.projectName); if (!fileSystem.exists(transaction.projectPath)) { const ossPath = fileSystem.path(appConfig.sourcePath, 'public', transaction.templateName.concat('.zip')); const outPath = fileSystem.path(appConfig.dataPath, transaction.templateName.concat('.zip')); const targetPath = fileSystem.path(appConfig.dataPath, transaction.templateName); if (fileSystem.exists(ossPath)) if (fileSystem.exists(targetPath)) fileSystem.delete(targetPath, () => { transaction.packaing(configs, ossPath, outPath, targetPath); }); else transaction.packaing(configs, ossPath, outPath, targetPath); else appWindow.webContents.methods.build.failure(locale.i18n.buildDependProblemTip); } else appWindow.webContents.methods.build.failure(locale.i18n.fileAlreadyExistsTip); } else appWindow.webContents.methods.build.cancel(); }, () => { appWindow.webContents.methods.page.snack(locale.i18n.somethingWentWrongTip); }); }, /** * 开始打包 * @param configs 项目配置 * @param ossPath 原始路径 (应用程序内部路径) * @param outPath 导出路径 (外部数据临时路径) * @param targetPath 目标路径 (解包打包操作路径) */ packaing: (configs, ossPath, outPath, targetPath) => { /** * 代码文件模板命名空间 */ const codeFiles = { /** * 获取代码缩进空格 * @param count 空格个数 * @return string */ space: (count) => { let space = ''; for (let i = 1; i <= count; i++) space = space.concat(' '); return space; }, /** * 插入代码 * @param origCode 原始代码 * @param newCodes 新的代码数组 * @return string */ append: (origCode, ...newCodes) => { if (newCodes.length <= 0) return; let finalCode = origCode; newCodes.forEach((value) => { if (value !== '') finalCode = finalCode.concat(value, '\n', codeFiles.space(8)); }); return finalCode; }, 'AndroidManifest.xml': { newXSharePrefs: (isEnabled) => (''), xposedModule: () => (''), moduleScope: () => ('') }, 'array.xml': { item: (name) => '' + name + '' }, 'HookEntry.kt': { annotations: { entryClassName: (name) => 'entryClassName = "' + name + '"', supportResourcesHook: (isEnabled) => 'isUsingResourcesHook = ' + (isEnabled ? 'true' : 'false') }, configs: { debugLog: { tagName: (name) => 'tag = "' + name + '"', enable: (isEnabled) => 'isEnable = ' + (isEnabled ? 'true' : 'false') }, enableDebug: (isEnabled) => 'isDebug = ' + (isEnabled ? 'true' : 'false'), enableResourcesCache: (isEnabled) => 'isEnableModuleAppResourcesCache = ' + (isEnabled ? 'true' : 'false'), enableModuleStatus: (isEnabled) => 'isEnableHookModuleStatus = ' + (isEnabled ? 'true' : 'false'), enableYChannel: (isEnabled) => 'isEnableDataChannel = ' + (isEnabled ? 'true' : 'false') } }, 'MainActivity.kt': { isModuleActive: () => 'YukiHookAPI.Status.isModuleActive', isXposedModuleActive: () => 'YukiHookAPI.Status.isXposedModuleActive', isTaiChiModuleActive: () => 'YukiHookAPI.Status.isTaiChiModuleActive' } }; /** * Settings 插件命名空间 */ const settingsPlugins = { // TODO SweetDependency 和 SweetProperty 暂时不支持自动获取版本 // 后期合并到 IDEA 插件后再做这里的更新 sweetDependencyVersion: '1.0.2', sweetPropertyVersion: '1.0.3' } /** * 更新创建进度显示 * @param progress 当前进度 */ function changeProgress(progress) { appWindow.webContents.methods.build.changeStatus(locale.format(locale.i18n.packagingAndBuildingTip, progress, 4)); } /** 结束创建项目 */ function finishBuild() { changeProgress(4); fileSystem.copy(targetPath, transaction.projectPath, () => { if (fileSystem.exists(transaction.projectPath)) fileSystem.deletes(() => { transaction.finish(configs); }, outPath, targetPath); else appWindow.webContents.methods.build.failure(locale.i18n.readOnlyFileSystemTip); }); } /** 移动项目代码到包名目录 */ function moveToPackageName() { changeProgress(3); const folderNames = configs.basicConfig.packageName.split('.'); const androidTestSpPath = fileSystem.path(targetPath, 'app', 'src', 'androidTest', 'java', 'scope'); const androidTestPath = fileSystem.path(targetPath, 'app', 'src', 'androidTest', 'java', folderNames); const testSpPath = fileSystem.path(targetPath, 'app', 'src', 'test', 'java', 'scope'); const testPath = fileSystem.path(targetPath, 'app', 'src', 'test', 'java', folderNames); const mainSpPath = fileSystem.path(targetPath, 'app', 'src', 'main', 'java', 'scope'); const mainPath = fileSystem.path(targetPath, 'app', 'src', 'main', 'java', folderNames); if (fileSystem.exists(androidTestSpPath, testSpPath, mainSpPath)) fileSystem.copy(androidTestSpPath, androidTestPath, () => { fileSystem.copy(testSpPath, testPath, () => { fileSystem.copy(mainSpPath, mainPath, () => { fileSystem.deletes(() => { finishBuild(); }, androidTestSpPath, testSpPath, mainSpPath); }); }); }); else appWindow.webContents.methods.build.failure(locale.i18n.buildDependProblemTip); } /** 替换代码文件中的配置信息 */ function replaceFiles() { changeProgress(2); let manifestMetaDataCode = ''; const newXShareCode = configs.basicConfig.newXSharePrefs === 0 ? '' : codeFiles['AndroidManifest.xml'].newXSharePrefs(configs.basicConfig.newXSharePrefs === 1); switch (configs.basicConfig.targetXposedPlatform) { case 0: manifestMetaDataCode = codeFiles.append(manifestMetaDataCode, codeFiles['AndroidManifest.xml'].xposedModule(), newXShareCode, codeFiles['AndroidManifest.xml'].moduleScope()); break; case 1: manifestMetaDataCode = codeFiles.append(manifestMetaDataCode, newXShareCode, codeFiles['AndroidManifest.xml'].moduleScope()); break; case 2: // TODO LSPatch WIP break; case 3: case 4: manifestMetaDataCode = codeFiles.append(manifestMetaDataCode, codeFiles['AndroidManifest.xml'].xposedModule()); /** 不需要作用域直接删掉作用域定义的数组文件 */ fileSystem.delete(fileSystem.path(targetPath, 'app', 'src', 'main', 'res', 'values', 'array.xml')); break; } manifestMetaDataCode = manifestMetaDataCode.trim(); let moduleActiveStatusCode = ''; switch (configs.basicConfig.targetXposedPlatform) { case 2: // TODO LSPatch WIP break; case 0: moduleActiveStatusCode = codeFiles['MainActivity.kt'].isModuleActive(); break; case 1: case 4: moduleActiveStatusCode = codeFiles['MainActivity.kt'].isXposedModuleActive(); break; case 3: moduleActiveStatusCode = codeFiles['MainActivity.kt'].isTaiChiModuleActive(); break; } moduleActiveStatusCode = moduleActiveStatusCode.trim(); let moduleScopesCode = ''; if (configs.basicConfig.moduleScopes.length > 0) configs.basicConfig.moduleScopes.forEach((value) => { moduleScopesCode = codeFiles.append(moduleScopesCode, codeFiles['array.xml'].item(value)); }); moduleScopesCode = moduleScopesCode.trim(); let hookEntryAnnotationCode = ''; if (configs.yukiHookApiConfig.entryClassName !== '') hookEntryAnnotationCode = hookEntryAnnotationCode.concat( codeFiles['HookEntry.kt'].annotations.entryClassName(configs.yukiHookApiConfig.entryClassName), ', '); if (configs.yukiHookApiConfig.supportResourcesHook !== 0) hookEntryAnnotationCode = hookEntryAnnotationCode.concat( codeFiles['HookEntry.kt'].annotations.supportResourcesHook(configs.yukiHookApiConfig.supportResourcesHook === 1)); if (hookEntryAnnotationCode.trim() !== '') hookEntryAnnotationCode = '(' + (hookEntryAnnotationCode.trim().endsWith(',') ? hookEntryAnnotationCode.trim().substring(0, hookEntryAnnotationCode.trim().lastIndexOf(',')) : hookEntryAnnotationCode.trim()) + ')'; let hookEntryConfigsCode = ''; if (configs.yukiHookApiConfig.debugLogTagName !== '' || configs.yukiHookApiConfig.enableDebugLog !== 0) { hookEntryConfigsCode += 'debugLog {\n'; hookEntryConfigsCode += codeFiles.space(12); if (configs.yukiHookApiConfig.debugLogTagName !== '') { hookEntryConfigsCode = codeFiles.append(hookEntryConfigsCode, codeFiles['HookEntry.kt'].configs.debugLog.tagName(configs.yukiHookApiConfig.debugLogTagName)); if (configs.yukiHookApiConfig.enableDebugLog !== 0) hookEntryConfigsCode += codeFiles.space(4); } if (configs.yukiHookApiConfig.enableDebugLog !== 0) hookEntryConfigsCode = codeFiles.append(hookEntryConfigsCode, codeFiles['HookEntry.kt'].configs.debugLog.enable(configs.yukiHookApiConfig.enableDebugLog === 1)); hookEntryConfigsCode += '}\n'; hookEntryConfigsCode += codeFiles.space(8); } if (configs.yukiHookApiConfig.enableDebug !== 0) hookEntryConfigsCode = codeFiles.append(hookEntryConfigsCode, codeFiles['HookEntry.kt'].configs.enableDebug(configs.yukiHookApiConfig.enableDebug === 1)); if (configs.yukiHookApiConfig.enableResourcesCache !== 0) hookEntryConfigsCode = codeFiles.append(hookEntryConfigsCode, codeFiles['HookEntry.kt'].configs.enableResourcesCache(configs.yukiHookApiConfig.enableResourcesCache === 1)); if (configs.yukiHookApiConfig.enableModuleStatus !== 0) hookEntryConfigsCode = codeFiles.append(hookEntryConfigsCode, codeFiles['HookEntry.kt'].configs.enableModuleStatus(configs.yukiHookApiConfig.enableModuleStatus === 1)); if (configs.yukiHookApiConfig.enableYChannel !== 0) hookEntryConfigsCode = codeFiles.append(hookEntryConfigsCode, codeFiles['HookEntry.kt'].configs.enableYChannel(configs.yukiHookApiConfig.enableYChannel === 1)); hookEntryConfigsCode = hookEntryConfigsCode.trim(); if (hookEntryConfigsCode === '') hookEntryConfigsCode = '// Your code here.'; fileSystem.replaces(targetPath, ['gradle.kts', 'properties', 'kt', 'xml', 'yaml'], [ { placeholder: '{GRADLE_PAPER}', value: configs.projectDependencies.gradlePaper }, { placeholder: '{GRADLE_PROPERTIES_DATE}', value: dateTime.cstTime() }, { placeholder: '{SWEETDEPENDENCY_VERSION}', value: settingsPlugins.sweetDependencyVersion }, { placeholder: '{SWEETPROPERTY_VERSION}', value: settingsPlugins.sweetPropertyVersion }, { placeholder: '{AGP_VERSION}', value: configs.projectDependencies.androidGradlePluginVersion }, { placeholder: '{KOTLIN_VERSION}', value: configs.projectDependencies.kotlinVersion }, { placeholder: '{KOTLIN_KSP_VERSION}', value: configs.projectDependencies.kotlinKspVersion }, { placeholder: '{YUKIHOOKAPI_VERSION}', value: configs.projectDependencies.yukiHookApiVersion }, { placeholder: '{PROJECT_NAME}', value: configs.basicConfig.projectName }, { placeholder: '{PACKAGE_NAME}', value: configs.basicConfig.packageName }, { placeholder: '{APP_NAME}', value: configs.basicConfig.appName }, { placeholder: '{MODULE_DESCRIPTION}', value: configs.basicConfig.moduleDescription }, { placeholder: '{APP_MIN_API}', value: configs.basicConfig.appMinApi }, { placeholder: '{APP_TARGET_API}', value: configs.basicConfig.appTargetApi }, { placeholder: '{XPOSED_MIN_API}', value: configs.basicConfig.xposedMinApi }, { placeholder: '{META_DATA}', value: manifestMetaDataCode }, { placeholder: '{SCOPE_ITEMS}', value: moduleScopesCode }, { placeholder: '{YUKIHOOKAPI_MODULE_ACTIVE_STATUS}', value: moduleActiveStatusCode }, { placeholder: '{YUKIHOOKAPI_ANNOTATION}', value: hookEntryAnnotationCode }, { placeholder: '{YUKIHOOKAPI_CONFIGS}', value: hookEntryConfigsCode } ], () => { moveToPackageName(); }); } /** * 格式化为 main 目录路径 * @param name 目录名称 * @return string */ function parseMainPath(name) { return fileSystem.path(targetPath, 'app', 'src', name); } /** 开始创建项目 */ function preToBuild() { changeProgress(1); if (fileSystem.exists(parseMainPath('main-fully'), parseMainPath('main-blank'), parseMainPath('main-nogui'))) switch (configs.basicConfig.moduleCompoment) { case 0: fileSystem.rename(parseMainPath('main-fully'), 'main-fully', 'main', () => { fileSystem.deletes(() => { replaceFiles(); }, parseMainPath('main-blank'), parseMainPath('main-nogui')); }); break; case 1: fileSystem.rename(parseMainPath('main-blank'), 'main-blank', 'main', () => { fileSystem.deletes(() => { replaceFiles(); }, parseMainPath('main-fully'), parseMainPath('main-nogui')); }); break; case 2: fileSystem.rename(parseMainPath('main-nogui'), 'main-nogui', 'main', () => { fileSystem.deletes(() => { replaceFiles(); }, parseMainPath('main-fully'), parseMainPath('main-blank')); }); break; default: appWindow.webContents.methods.build.failure(locale.i18n.buildingFailureTip); break; } else appWindow.webContents.methods.build.failure(locale.i18n.buildDependProblemTip); } fileSystem.copy(ossPath, outPath, () => { if (fileSystem.exists(outPath)) { unzip.extract(outPath, appConfig.dataPath); unzip.progress((fileIndex, fileCount) => { appWindow.webContents.methods.build.changeStatus(locale.format(locale.i18n.unpackingTemplateTip, fileIndex, fileCount)); }); unzip.success(() => { preToBuild(); }); unzip.failure((error) => { appWindow.webContents.methods.build.failure(locale.i18n.buildingFailureTip + '
' + error.toString()); }); } else appWindow.webContents.methods.build.failure(locale.i18n.buildingFailureTip); }); }, /** * 完成任务 * @param configs 项目配置 */ finish: (configs) => { appWindow.webContents.methods.build.complete( (locale.i18n.buildCompleteTip + '

' + locale.i18n.projectName + ': ' + configs.basicConfig.projectName + '
' + locale.i18n.moduleName + ': ' + configs.basicConfig.appName + '
' + locale.i18n.moduleDescription + ': ' + configs.basicConfig.moduleDescription.replace(/\n/g, ' ') + '
' + locale.i18n.gradlePaper + ': ' + configs.projectDependencies.gradlePaper + '
' + locale.i18n.agpVersion + ': ' + configs.projectDependencies.androidGradlePluginVersion + '
' + locale.i18n.kotlinVersion + ': ' + configs.projectDependencies.kotlinVersion + '
' + locale.i18n.yukiHookApiVersion + ': ' + configs.projectDependencies.yukiHookApiVersion + '

' + locale.i18n.buildPathNoticeTip + '

' + transaction.projectPath.replace(/\\/g, '\') ) ); } }; module.exports = {transaction};