diff --git a/src/core/builder/platforms/ohos/i18n/en.js b/src/core/builder/platforms/ohos/i18n/en.js index 820ace2a1..7de7a4e3e 100644 --- a/src/core/builder/platforms/ohos/i18n/en.js +++ b/src/core/builder/platforms/ohos/i18n/en.js @@ -21,6 +21,14 @@ module.exports = { google_play_instant: 'Google Play Instant', input_sdk: 'Input SDK', remoteUrl: 'Remote URL', + JobSystem: 'Job System', + none: 'None', + }, + encrypt: { + title: 'Encrypt JS', + encrypt_key: 'JS Encryption Key', + compress_zip: 'Zip Compress', + disable_tips: 'In debug mode, the Encrypt JS is invalid', }, tips: { not_empty: 'Can not be empty!', @@ -32,6 +40,8 @@ module.exports = { orientation_portrait: 'The screen is upright and the Home button is down', orientation_landscape_left: 'The screen is horizontal, the Home button is on the left side of the screen', orientation_landscape_right: 'The screen is horizontal, the Home button is on the right side of the screen', + JobSystemTaskFlow: 'TaskFlow needs C++17 support.', + JobSystemOther: 'C++17 will be enabled to support compilation.', }, make: { label: 'Make', diff --git a/src/core/builder/platforms/ohos/i18n/zh.js b/src/core/builder/platforms/ohos/i18n/zh.js index 6d6af867d..3f9a6883a 100644 --- a/src/core/builder/platforms/ohos/i18n/zh.js +++ b/src/core/builder/platforms/ohos/i18n/zh.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - // !专用名词,不要翻译 + // 专用名词,不要翻译 title: 'HarmonyOS', options: { package_name: '应用 ID 名称', @@ -15,14 +15,24 @@ module.exports = { portrait: '竖屏', render_back_end: '渲染后端', + JobSystem: '任务调度系统', + none: '不开启', + }, + encrypt: { + title: '加密 JS', + encrypt_key: 'JS 加密密钥', + compress_zip: 'Zip 压缩', + disable_tips: '调试模式下,JS 加密无效', }, tips: { not_empty: '不能为空!', at_least_one: '请至少选择一项', - package_name_error: '请输入正确的应用 ID:必须至少包含两段(一个或多个圆点),每段必须以字母开头, 包名中只能包含数字、字母和下划线。', + package_name_error: '请输入正确的应用 ID:必须至少包含两段(一个或多个圆点),每段必须以字母开头,包名中只能包含数字、字母和下划线。', ohos_sdk_error: '找不到 HarmonyOS NDK/SDK 路径,请到 偏好设置 -> 外部程序 中设置', set_ohos_sdk: '设置 HarmonyOS SDK', - apilevel_empty: '获取 apiLevel 失败,请点击旁边的 \'设置 HarmonyOS SDK\' 按钮到 \'偏好设置 -> 外部程序\' 中检查 \'HarmonyOS NDK/SDK\' 路径配置', + apilevel_empty: '获取 apiLevel 失败,请点击旁边的“设置 HarmonyOS SDK”按钮到“偏好设置 -> 外部程序”中检查“HarmonyOS NDK/SDK”路径配置', + JobSystemTaskFlow: 'TaskFlow 需要启用 C++17', + JobSystemOther: '将会自动启用 C++17 以支持编译', }, make: { label: '生成', diff --git a/src/core/builder/platforms/ohos/package.json b/src/core/builder/platforms/ohos/package.json new file mode 100644 index 000000000..06d52693e --- /dev/null +++ b/src/core/builder/platforms/ohos/package.json @@ -0,0 +1,31 @@ +{ + "name": "ohos", + "version": "1.0.0", + "author": "Cocos", + "description": "OHOS platform plugin", + "license": "ISC", + "type": "commonjs", + "contributes": { + "builder": { + "register": true, + "platform": "ohos", + "config": "./src/config", + "hooks": "./src/hooks" + }, + "pinkBuilder": { + "platform": "ohos", + "customView": "./dist/view/build-config.js", + "customExtensionHost": "./dist/view/build-config-host.js", + "customFields": [ + "encrypted", + "xxteaKey", + "compressZip", + "JobSystem" + ] + } + }, + "scripts": { + "build-view": "node scripts/build-view.js", + "watch-view": "node scripts/build-view.js --watch" + } +} \ No newline at end of file diff --git a/src/core/builder/platforms/ohos/scripts/build-view.js b/src/core/builder/platforms/ohos/scripts/build-view.js new file mode 100644 index 000000000..cc6d93765 --- /dev/null +++ b/src/core/builder/platforms/ohos/scripts/build-view.js @@ -0,0 +1,57 @@ +const ps = require('path'); +const esbuild = require('esbuild'); + +const watch = process.argv.includes('--watch'); + +const viewOptions = { + entryPoints: [ps.join(__dirname, '../src/view/build-config.tsx')], + outfile: ps.join(__dirname, '../dist/view/build-config.js'), + bundle: true, + format: 'esm', + platform: 'browser', + target: 'es2022', + jsx: 'automatic', + external: [ + 'react', + 'react-dom', + 'react/jsx-runtime', + 'react-dom/client', + 'react/compiler-runtime', + '@pink/ui-kit', + ], + logLevel: 'info', +}; + +const hostOptions = { + entryPoints: [ps.join(__dirname, '../src/view/build-config-host.ts')], + outfile: ps.join(__dirname, '../dist/view/build-config-host.js'), + bundle: true, + format: 'cjs', + platform: 'node', + target: 'node18', + external: ['vscode'], + logLevel: 'info', +}; + +const allOptions = [viewOptions, hostOptions]; + +(async function buildView() { + try { + if (watch) { + for (const options of allOptions) { + const ctx = await esbuild.context(options); + await ctx.rebuild(); + await ctx.watch(); + } + console.log('[ohos build-view] watching src/view for changes...'); + } else { + console.time('Bundle OHOS View'); + await Promise.all(allOptions.map((options) => esbuild.build(options))); + console.timeEnd('Bundle OHOS View'); + process.exit(0); + } + } catch (error) { + console.error(error); + process.exit(1); + } +}()); diff --git a/src/core/builder/platforms/ohos/config.ts b/src/core/builder/platforms/ohos/src/config.ts similarity index 86% rename from src/core/builder/platforms/ohos/config.ts rename to src/core/builder/platforms/ohos/src/config.ts index 8b99a2293..c22baa60e 100644 --- a/src/core/builder/platforms/ohos/config.ts +++ b/src/core/builder/platforms/ohos/src/config.ts @@ -1,7 +1,7 @@ 'use strict'; -import { IPlatformBuildPluginConfig } from '../../@types/protected'; -import { commonOptions, baseNativeCommonOptions } from '../native-common'; +import { IPlatformBuildPluginConfig } from '../../../@types/protected'; +import { commonOptions, baseNativeCommonOptions } from '../../native-common'; const config: IPlatformBuildPluginConfig = { ...commonOptions, @@ -28,7 +28,7 @@ const config: IPlatformBuildPluginConfig = { message: 'Invalid package name specified', }, }, - hooks: './hooks', + hooks: './src/hooks', options: { ...baseNativeCommonOptions, @@ -54,11 +54,6 @@ const config: IPlatformBuildPluginConfig = { type: 'boolean', default: false, }, - upsideDown: { - label: 'i18n:ohos.options.upsideDown', - type: 'boolean', - default: false, - }, landscapeRight: { label: 'i18n:ohos.options.landscape_right', type: 'boolean', @@ -72,7 +67,6 @@ const config: IPlatformBuildPluginConfig = { }, default: { portrait: false, - upsideDown: false, landscapeRight: true, landscapeLeft: true, }, diff --git a/src/core/builder/platforms/ohos/hooks.ts b/src/core/builder/platforms/ohos/src/hooks.ts similarity index 93% rename from src/core/builder/platforms/ohos/hooks.ts rename to src/core/builder/platforms/ohos/src/hooks.ts index dad6f4e3e..158d48db0 100644 --- a/src/core/builder/platforms/ohos/hooks.ts +++ b/src/core/builder/platforms/ohos/src/hooks.ts @@ -3,8 +3,8 @@ import path from 'path'; import os from 'os'; import { accessSync, existsSync, constants, outputJSON } from 'fs-extra'; -import * as nativeCommonHook from '../native-common/hooks'; -import { BuilderCache, IBuilder } from '../../@types/protected'; +import * as nativeCommonHook from '../../native-common/hooks'; +import { BuilderCache, IBuilder } from '../../../@types/protected'; export const throwError = true; import { IBuildResult, IOhosInternalBuildOptions } from './type'; import { generateOptions } from './utils'; @@ -45,4 +45,4 @@ export function onAfterBundleInit(options: IOhosInternalBuildOptions) { options.assetSerializeOptions!['cc.EffectAsset'].glsl1 = false; options.assetSerializeOptions!['cc.EffectAsset'].glsl3 = true; options.assetSerializeOptions!['cc.EffectAsset'].glsl4 = false; -} \ No newline at end of file +} diff --git a/src/core/builder/platforms/ohos/type.ts b/src/core/builder/platforms/ohos/src/type.ts similarity index 88% rename from src/core/builder/platforms/ohos/type.ts rename to src/core/builder/platforms/ohos/src/type.ts index 9b7825d00..45a975968 100644 --- a/src/core/builder/platforms/ohos/type.ts +++ b/src/core/builder/platforms/ohos/src/type.ts @@ -1,7 +1,7 @@ -import { IInternalBuildOptions, InternalBuildResult } from '../../@types/protected'; -import { CocosParams } from '../native-common/pack-tool/base/default'; -import { ICustomBuildScriptParam, IOptions as INativeOption } from '../native-common/type'; +import { IInternalBuildOptions, InternalBuildResult } from '../../../@types/protected'; +import { CocosParams } from '../../native-common/pack-tool/base/default'; +import { ICustomBuildScriptParam, IOptions as INativeOption } from '../../native-common/type'; export type IOrientation = 'landscape' | 'portrait'; diff --git a/src/core/builder/platforms/ohos/utils.ts b/src/core/builder/platforms/ohos/src/utils.ts similarity index 98% rename from src/core/builder/platforms/ohos/utils.ts rename to src/core/builder/platforms/ohos/src/utils.ts index 6fcbfb858..ec904c4da 100644 --- a/src/core/builder/platforms/ohos/utils.ts +++ b/src/core/builder/platforms/ohos/src/utils.ts @@ -10,7 +10,6 @@ import { platform } from 'os'; */ export async function generateOptions(options: IOhosInternalBuildOptions) { const ohos = options.packages.ohos; - ohos.orientation = ohos.orientation || {}; if(!ohos.sdkPath) { ohos.sdkPath = process.env.OHOS_HOME || process.env.OHOS_SDK_ROOT || ''; @@ -66,4 +65,4 @@ export async function generateOptions(options: IOhosInternalBuildOptions) { console.log(`[OHOS] Using SDK at: ${ohos.sdkPath}, Using NDK at: ${ohos.ndkPath}`); return ohos; -} \ No newline at end of file +} diff --git a/src/core/builder/platforms/ohos/src/view/build-config-host.ts b/src/core/builder/platforms/ohos/src/view/build-config-host.ts new file mode 100644 index 000000000..3eafed73f --- /dev/null +++ b/src/core/builder/platforms/ohos/src/view/build-config-host.ts @@ -0,0 +1,67 @@ +import * as path from 'node:path'; + +type Bundle = Record; + +interface HostContext { + registerMethod(name: string, handler: (...args: any[]) => unknown | Promise): void; +} + +function currentLang(): 'zh' | 'en' { + let locale = 'en'; + try { + const cfg = process.env.VSCODE_NLS_CONFIG; + if (cfg) { + locale = (JSON.parse(cfg) as { locale?: string }).locale || locale; + } + } catch { + // Fallback to English. + } + return locale.toLowerCase().startsWith('zh') ? 'zh' : 'en'; +} + +let cache: { lang: string; bundle: Bundle } | undefined; + +function loadBundle(): Bundle { + const lang = currentLang(); + if (cache?.lang === lang) { + return cache.bundle; + } + + let bundle: Bundle = {}; + try { + const file = path.join(__dirname, '..', '..', 'i18n', `${lang}.js`); + delete require.cache[require.resolve(file)]; + bundle = (require(file) as Bundle) ?? {}; + } catch { + bundle = {}; + } + cache = { lang, bundle }; + return bundle; +} + +function lookup(bundle: Bundle, key: string): string | undefined { + let cur: unknown = bundle; + for (const seg of key.split('.')) { + if (cur && typeof cur === 'object' && seg in (cur as Bundle)) { + cur = (cur as Bundle)[seg]; + } else { + return undefined; + } + } + return typeof cur === 'string' ? cur : undefined; +} + +function substitute(text: string, sub?: Record): string { + if (!sub) { + return text; + } + return text.replace(/%?\{(\w+)\}/g, (match, key: string) => (key in sub ? String(sub[key]) : match)); +} + +export function activate(context: HostContext): void { + context.registerMethod('getI18nBundle', () => loadBundle()); + context.registerMethod('t', (key: string, sub?: Record) => { + const text = lookup(loadBundle(), key); + return text === undefined ? key : substitute(text, sub); + }); +} diff --git a/src/core/builder/platforms/ohos/src/view/build-config.tsx b/src/core/builder/platforms/ohos/src/view/build-config.tsx new file mode 100644 index 000000000..8f525c693 --- /dev/null +++ b/src/core/builder/platforms/ohos/src/view/build-config.tsx @@ -0,0 +1,210 @@ +import { useEffect, useMemo, useState, type ChangeEvent, type CSSProperties } from 'react'; +import { Checkbox, TypedField } from '@pink/ui-kit'; + +export interface PlatformBuildViewProps { + value: Record; + onChange: (path: string[], value: unknown) => void; + host?: unknown; + bridge?: { + invoke(method: string, ...args: unknown[]): Promise; + on(event: string, listener: (params: unknown) => void): () => void; + }; + commonValue?: Record; +} + +function createEncryptionKey(): string { + return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join(''); +} + +const DEFAULTS: Record = { + encrypted: false, + xxteaKey: createEncryptionKey(), + compressZip: false, + JobSystem: 'none', +}; + +const ROW: CSSProperties = { padding: '2px 16px 6px 0px' }; +const INPUT: CSSProperties = { + width: '100%', + minWidth: 0, + boxSizing: 'border-box', + height: 26, + padding: '0 8px', + border: '1px solid var(--vscode-input-border, transparent)', + color: 'var(--vscode-input-foreground)', + background: 'var(--vscode-input-background)', + outline: 'none', +}; +const SELECT: CSSProperties = { ...INPUT, padding: '0 6px' }; +const ERROR: CSSProperties = { + paddingTop: 3, + fontSize: 11, + lineHeight: '16px', + color: 'var(--vscode-errorForeground, #f14c4c)', +}; +const INFO: CSSProperties = { + paddingTop: 3, + fontSize: 11, + lineHeight: '16px', + color: 'var(--vscode-descriptionForeground)', +}; +const DISABLED_BLOCK: CSSProperties = { + opacity: 0.55, + pointerEvents: 'none', +}; + +function translate(bundle: Record, key: string): string { + let cur: unknown = bundle; + for (const seg of key.split('.')) { + if (cur && typeof cur === 'object' && seg in (cur as Record)) { + cur = (cur as Record)[seg]; + } else { + return key; + } + } + return typeof cur === 'string' ? cur : key; +} + +function formatMessage(text: string, sub?: Record): string { + if (!sub) { + return text; + } + return text.replace(/\{(\w+)\}/g, (match, key) => (key in sub ? String(sub[key]) : match)); +} + +function stringValue(value: unknown): string { + return typeof value === 'string' ? value : value === undefined || value === null ? '' : String(value); +} + +function boolValue(value: unknown, fallback = false): boolean { + return typeof value === 'boolean' ? value : fallback; +} + +function TextField({ + label, + value, + disabled, + error, + onChange, +}: { + label: string; + value: unknown; + disabled?: boolean; + error?: string; + onChange: (value: string) => void; +}) { + return ( +
+ + ) => onChange(event.target.value)} + /> + + {error &&
{error}
} +
+ ); +} + +export default function OHOSBuildView({ value, onChange, bridge, commonValue }: PlatformBuildViewProps) { + const [bundle, setBundle] = useState>({}); + const t = (key: string, sub?: Record) => formatMessage(translate(bundle, key), sub); + const current = useMemo(() => ({ ...DEFAULTS, ...value }), [value]); + const encrypted = boolValue(current.encrypted); + const compressZip = boolValue(current.compressZip); + const isDebugMode = boolValue(commonValue?.debug); + const jobSystem = stringValue(current.JobSystem) || 'none'; + + const set = (key: string, next: unknown) => onChange([key], next); + + useEffect(() => { + if (!bridge) { + return; + } + + let cancelled = false; + bridge.invoke>('getI18nBundle') + .then((data) => { + if (!cancelled) { + setBundle(data ?? {}); + } + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [bridge]); + + useEffect(() => { + for (const [key, defaultValue] of Object.entries(DEFAULTS)) { + if (!(key in value)) { + onChange([key], defaultValue); + } + } + }, []); + + const errors = useMemo(() => { + const next: Record = {}; + if (encrypted && !stringValue(current.xxteaKey)) { + next.xxteaKey = t('tips.not_empty'); + } + return next; + }, [current.xxteaKey, encrypted, t]); + + const setEncrypted = (checked: boolean) => { + set('encrypted', checked); + if (!checked) { + set('compressZip', false); + } + }; + + return ( +
+
+ +
+ setEncrypted(!!checked)} /> +
+
+ {isDebugMode && encrypted &&
{t('encrypt.disable_tips')}
} +
+ + {encrypted && ( + <> + set('xxteaKey', next)} + /> +
+ +
+ set('compressZip', !!checked)} /> +
+
+
+ + )} + +
+ + + + {jobSystem === 'taskFlow' &&
{t('tips.JobSystemTaskFlow')}
} +
+
+ ); +} diff --git a/src/core/builder/platforms/ohos/static/card@2x.png b/src/core/builder/platforms/ohos/static/card@2x.png index e3b994883..92310bb11 100644 Binary files a/src/core/builder/platforms/ohos/static/card@2x.png and b/src/core/builder/platforms/ohos/static/card@2x.png differ diff --git a/src/core/builder/platforms/ohos/static/icon@2x.png b/src/core/builder/platforms/ohos/static/icon@2x.png index 92310bb11..e3b994883 100644 Binary files a/src/core/builder/platforms/ohos/static/icon@2x.png and b/src/core/builder/platforms/ohos/static/icon@2x.png differ diff --git a/src/core/builder/platforms/ohos/tsconfig.view.json b/src/core/builder/platforms/ohos/tsconfig.view.json new file mode 100644 index 000000000..d26f303d0 --- /dev/null +++ b/src/core/builder/platforms/ohos/tsconfig.view.json @@ -0,0 +1,28 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "es2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "lib": [ + "esnext", + "dom" + ], + "typeRoots": [ + "../types", + "../../../../../node_modules/@types" + ], + "types": [ + "node" + ] + }, + "include": [ + "src/view", + "../types" + ] +}