diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000000000000000000000000000000000000..48439ed68073b99be118d374d1ebd43d04d4c4eb --- /dev/null +++ b/.clang-format @@ -0,0 +1,64 @@ +Language: Cpp +# BasedOnStyle: LLVM +ColumnLimit: 120 +SortIncludes: CaseSensitive +TabWidth: 4 +IndentWidth: 4 +UseTab: Never +AccessModifierOffset: -4 +ContinuationIndentWidth: 4 +IndentCaseBlocks: false +IndentCaseLabels: false +IndentGotoLabels: true +IndentWrappedFunctionNames: false +SortUsingDeclarations: false +NamespaceIndentation: None +SpaceAfterCStyleCast: false +SpaceAfterLogicalNot: false +SpaceAfterTemplateKeyword: true +SpaceBeforeAssignmentOperators: true +SpaceBeforeCaseColon: false +SpaceBeforeCpp11BracedList: false +SpaceBeforeCtorInitializerColon: true +SpaceBeforeInheritanceColon: true +SpaceBeforeRangeBasedForLoopColon: true +SpaceBeforeSquareBrackets: false +SpaceInEmptyBlock: false +SpaceInEmptyParentheses: false +SpacesInAngles: false +SpacesInCStyleCastParentheses: false +SpacesInConditionalStatement: false +SpacesInParentheses: false +SpacesInSquareBrackets: false +AlignTrailingComments: true +AlignAfterOpenBracket: true +AllowShortCaseLabelsOnASingleLine: false +AllowShortEnumsOnASingleLine: true +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: Never +AllowShortLambdasOnASingleLine: All +AllowShortLoopsOnASingleLine: false +AlwaysBreakTemplateDeclarations: MultiLine +BinPackArguments: true +BinPackParameters: true +BreakBeforeTernaryOperators: true +BreakConstructorInitializers: BeforeColon +BreakInheritanceList: BeforeColon +BreakStringLiterals: true +InsertBraces: false +IndentExternBlock: NoIndent +BreakBeforeBraces: Custom +BraceWrapping: + AfterCaseLabel: false + AfterClass: false + AfterControlStatement: Never + AfterEnum: false + AfterFunction: false + AfterNamespace: false + AfterStruct: false + AfterUnion: false + AfterExternBlock: false + BeforeCatch: false + BeforeElse: false +ReflowComments: true +MaxEmptyLinesToKeep: 2 \ No newline at end of file diff --git a/AppScope/app.json5 b/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..a2c38ed7b6e5dbfa42457fd229affceeee919400 --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.multidevicecamera", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/AppScope/resources/base/element/string.json b/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..9578586c7a2e96d8015973a5ef7a048c9a12196a --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "MultiDeviceCamera" + } + ] +} diff --git a/AppScope/resources/base/media/background.png b/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/AppScope/resources/base/media/background.png differ diff --git a/AppScope/resources/base/media/foreground.png b/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/AppScope/resources/base/media/foreground.png differ diff --git a/AppScope/resources/base/media/layered_image.json b/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/README.md b/README.md index 3e8a76400563db6b7a564e36b3287385bcccdc71..b8e847ed23a85ddb315309146a01105fa656ecb3 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,76 @@ -# MutilDeviceCamera +# 基于相机开放能力和一多能力实现多设备相机 -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} +## 介绍 -#### 软件架构 -软件架构说明 +本示例展示了如何使用HarmonyOS提供的相机开放能力和一多能力,主要包括使用Camera Kit预览拍照、photoAccessHelper进行保存图片和一多断点实现多设备页面及功能差异。本示例主要用于如何在多设备(手机、大折叠、阔折叠、三折叠、平板)上实现正常状态和折叠状态切换时的预览旋转、拍照旋转、切换镜头等功能。 +## 效果展示 -#### 安装教程 +手机运行效果图: -1. xxxx -2. xxxx -3. xxxx +![](screenshots/device/mate60.png) -#### 使用说明 +阔折叠运行效果图: -1. xxxx -2. xxxx -3. xxxx +![](screenshots/device/purax.png) -#### 参与贡献 +大折叠运行效果图: -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request +![](screenshots/device/matex5.png) +三折叠运行效果图: -#### 特技 +![](screenshots/device/matext.png) -1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md -2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) -3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 -4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 -5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) -6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) +平板运行效果图: + +![](screenshots/device/matepadpro.png) + +## 使用说明 + +应用可以点击底部按钮拍摄照片,同时可以实现旋转相机角度、切换前后摄像头、切换折叠状态完成拍摄的操作,拍摄完成后可以预览照片。 + +## 工程目录 + +``` +├──entry/src/main/ets/ +│ ├──entryability +│ │ └──EntryAbility.ets +│ ├──entrybackupability +│ │ └──EntryBackupAbility.ets +│ ├──pages +│ │ └──Index.ets // 主页 +│ ├──utils +│ │ ├──BreakpointType.ets // 一多断点工具类 +│ │ ├──CameraUtil.ets // 相机工具类 +│ │ └──WindowUtil.ets // 窗口工具类 +│ └──views +│ └──CommonView.ets // 公共视图类 +└──entry/src/main/resource // 应用静态资源目录 +``` + +## 实现思路 + +1. 使用Camera Kit预览拍照。 +2. 使用photoAccessHelper保存图片。 +3. 使用一多断点能力实现多设备页面和功能差异。 + +## 相关权限 + +1. 相机权限:ohos.permission.CAMERA,用于相机开发场景。 +2. 受限开放权限-媒体库权限:ohos.permission.READ_IMAGEVIDEO,用于读取图库文件。可申请此权限的特殊场景与功能: 应用需要克隆、备份或同步图片/视频类文件。 +3. 受限开放权限-媒体库权限:ohos.permission.WRITE_IMAGEVIDEO,用于保存文件至图库。可申请此权限的特殊场景与功能: 应用需要克隆、备份或同步图片/视频类文件。 + +## 依赖 + +不涉及。 + +## 约束与限制 + +1.本示例仅支持标准系统上运行,支持设备:华为手机。 + +2.HarmonyOS系统:HarmonyOS 5.0.2 Release及以上。 + +3.DevEco Studio版本:DevEco Studio 5.0.2 Release及以上。 + +4.HarmonyOS SDK版本:HarmonyOS 5.0.2 Release SDK及以上。 diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..2844c740f45df0bfbe0cc3e3472327be382fc1db --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,41 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.0.2(14)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/code-linter.json5 b/code-linter.json5 new file mode 100644 index 0000000000000000000000000000000000000000..073990fa45394e1f8e85d85418ee60a8953f9b99 --- /dev/null +++ b/code-linter.json5 @@ -0,0 +1,32 @@ +{ + "files": [ + "**/*.ets" + ], + "ignore": [ + "**/src/ohosTest/**/*", + "**/src/test/**/*", + "**/src/mock/**/*", + "**/node_modules/**/*", + "**/oh_modules/**/*", + "**/build/**/*", + "**/.preview/**/*" + ], + "ruleSet": [ + "plugin:@performance/recommended", + "plugin:@typescript-eslint/recommended" + ], + "rules": { + "@security/no-unsafe-aes": "error", + "@security/no-unsafe-hash": "error", + "@security/no-unsafe-mac": "warn", + "@security/no-unsafe-dh": "error", + "@security/no-unsafe-dsa": "error", + "@security/no-unsafe-ecdsa": "error", + "@security/no-unsafe-rsa-encrypt": "error", + "@security/no-unsafe-rsa-sign": "error", + "@security/no-unsafe-rsa-key": "error", + "@security/no-unsafe-dsa-key": "error", + "@security/no-unsafe-dh-key": "error", + "@security/no-unsafe-3des": "error" + } +} \ No newline at end of file diff --git a/entry/build-profile.json5 b/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4d611879c7913fb0610c686e2399258ab3a6dad1 --- /dev/null +++ b/entry/build-profile.json5 @@ -0,0 +1,28 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/entry/hvigorfile.ts b/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6edcd90486dd5a853cf7d34c8647f08414ca7a3 --- /dev/null +++ b/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/entry/obfuscation-rules.txt b/entry/obfuscation-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..272efb6ca3f240859091bbbfc7c5802d52793b0b --- /dev/null +++ b/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/entry/oh-package.json5 b/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..248c3b7541a589682a250f86a6d3ecf7414d2d6a --- /dev/null +++ b/entry/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {} +} + diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..eda7630558c3dc53bea1b4e17d3ff88330cee397 --- /dev/null +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { display, window } from '@kit.ArkUI'; +import { BusinessError, deviceInfo } from '@kit.BasicServicesKit'; +import { camera } from '@kit.CameraKit'; +import { CameraUtil } from '../utils/CameraUtil'; +import { WindowUtil } from '../utils/WindowUtil'; + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + windowData?: window.Window; + uiContext?: UIContext; + cameraUtil?: CameraUtil = CameraUtil.getInstance(); + windowUtil?: WindowUtil = WindowUtil.getInstance(); + isFirstCreated: boolean = true; + onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => { + this.setOrientation(this.uiContext!.px2vp(windowSize.width), this.uiContext!.px2vp(windowSize.height)); + AppStorage.setOrCreate('windowSize', windowSize); + let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint(); + AppStorage.setOrCreate('widthBp', widthBp); + let heightBp: HeightBreakpoint = this.uiContext!.getWindowHeightBreakpoint(); + AppStorage.setOrCreate('heightBp', heightBp); + let displayOrientation: display.Orientation = display.getDefaultDisplaySync().orientation; + AppStorage.setOrCreate('displayOrientation', displayOrientation); + let isFront: boolean | undefined = AppStorage.get('isFront'); + let surfaceId: string | undefined = AppStorage.get('surfaceId'); + if (widthBp === WidthBreakpoint.WIDTH_MD && heightBp === HeightBreakpoint.HEIGHT_MD && + deviceInfo.productSeries === 'GRL') { + this.cameraUtil?.cameraShooting(surfaceId!, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + return; + } + if (widthBp === WidthBreakpoint.WIDTH_SM && heightBp === HeightBreakpoint.HEIGHT_MD) { + // When switching to a wide folding external screen, onForeground() will be triggered. + return; + } + if (isFront) { + this.cameraUtil?.cameraShooting(surfaceId!, this.context!, camera.CameraPosition.CAMERA_POSITION_FRONT); + } else { + this.cameraUtil?.cameraShooting(surfaceId!, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + } + } + + setOrientation(width: number, height: number): void { + // When the minimum value of window width and height is greater than the md breakpoint threshold, rotation is supported. + if (Math.min(width, height) >= 600) { + this.windowData?.setPreferredOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED); + } else { + this.windowData?.setPreferredOrientation(window.Orientation.PORTRAIT); + } + } + + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + this.windowUtil!.setWindowStage(windowStage); + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + windowStage.getMainWindow().then((data: window.Window) => { + this.windowData = data; + data.setWindowLayoutFullScreen(true); + data.setWindowSystemBarEnable([]); + this.uiContext = data.getUIContext(); + // Obtain the crease area of the folding screen. + this.windowUtil!.getFoldCreaseRegion(this.uiContext); + // First time obtaining breakpoint and screen display orientation. + let widthBp: WidthBreakpoint = this.uiContext.getWindowWidthBreakpoint(); + let heightBp: HeightBreakpoint = this.uiContext.getWindowHeightBreakpoint(); + AppStorage.setOrCreate('widthBp', widthBp); + AppStorage.setOrCreate('heightBp', heightBp); + let displayOrientation: display.Orientation = display.getDefaultDisplaySync().orientation; + AppStorage.setOrCreate('displayOrientation', displayOrientation); + // Monitor window size changes and update breakpoints. + data.on('windowSizeChange', this.onWindowSizeChange); + let rect: window.Rect = data.getWindowProperties().windowRect; + this.setOrientation(this.uiContext.px2vp(rect.width), this.uiContext.px2vp(rect.height)); + AppStorage.setOrCreate('isBackground', false); + }).catch((err: BusinessError) => { + hilog.error(0x0000, 'testTag', `Error occured, error code: ${err.code}, error message: ${err.message}`); + }) + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + if (this.isFirstCreated) { + this.isFirstCreated = false; + return; + } + // When the front-end displays the camera application, regenerate the preview stream. + if (AppStorage.get('isBackground')) { + this.cameraUtil?.fromBack(); + AppStorage.setOrCreate('isBackground', false); + } + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + // Release camera session and streams when the application is hidden in the background. + this.cameraUtil?.releaseCamera(); + AppStorage.setOrCreate('isBackground', true); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..0a97e21bd7a15599af76a806695860ff1eb0ebfe --- /dev/null +++ b/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +const DOMAIN = 0x0000; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(DOMAIN, 'testTag', 'onBackup ok'); + await Promise.resolve(); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + await Promise.resolve(); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/pages/Index.ets b/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..418c208bb2ef61f29e3a4585fc7692336ef07121 --- /dev/null +++ b/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,314 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { display, window } from '@kit.ArkUI'; +import { camera } from '@kit.CameraKit'; +import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit'; +import { deviceInfo } from '@kit.BasicServicesKit'; +import { ChooseMusic, SettingButton, ShotArea, ShotAreaHalfFolded, ShotAreaSm } from '../views/CommonView'; +import { CameraUtil } from '../utils/CameraUtil'; +import { WindowUtil } from '../utils/WindowUtil'; + +@Entry +@Component +struct Index { + @StorageLink('widthBp') widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM; + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + @StorageLink('displayOrientation') displayOrientation: display.Orientation = display.getDefaultDisplaySync().orientation; + @StorageLink('photoUri') photoUri: string | Resource | PixelMap = ''; + @StorageLink('surfaceId') surfaceId: string = ''; + @StorageLink('rotation') rotation: number = 0; + @StorageLink('isFront') isFront: boolean = false; + @StorageLink('isHalfFolded') isHalfFolded: boolean = false; + @StorageLink('creaseRegion') creaseRegion: number[] = []; + context?: Context = this.getUIContext().getHostContext(); + xComponentController: XComponentController = new XComponentController(); + cameraUtil?: CameraUtil = CameraUtil.getInstance(); + windowUtil?: WindowUtil = WindowUtil.getInstance(); + permissions: Array = [ + 'ohos.permission.CAMERA', + 'ohos.permission.READ_IMAGEVIDEO', + 'ohos.permission.WRITE_IMAGEVIDEO' + ]; + onFoldStatusChange: (foldStatus: display.FoldStatus) => void = (foldStatus: display.FoldStatus) => { + if (foldStatus === display.FoldStatus.FOLD_STATUS_HALF_FOLDED) { + let orientation: display.Orientation = display.getDefaultDisplaySync().orientation; + // Determine the page layout that has entered half folded status and prohibit portrait orientation. + if (this.widthBp === WidthBreakpoint.WIDTH_MD && (orientation === display.Orientation.LANDSCAPE || + orientation === display.Orientation.LANDSCAPE_INVERTED)) { + this.isHalfFolded = true; + this.cameraUtil!.setXComponentRect(this.windowUtil!.getWindowSize()); + this.windowUtil?.setMainWindowOrientation(window.Orientation.LANDSCAPE); + } + } + // Exit the half folded status page. + else { + if (this.isHalfFolded) { + this.isHalfFolded = false; + this.cameraUtil!.setXComponentRect(this.windowUtil!.getWindowSize()); + } else { + this.isHalfFolded = false; + } + if (this.widthBp !== WidthBreakpoint.WIDTH_SM) { + this.windowUtil?.setMainWindowOrientation(window.Orientation.AUTO_ROTATION_RESTRICTED); + } + } + }; + + aboutToAppear(): void { + display.on('foldStatusChange', this.onFoldStatusChange); + // Apply to the user for permission to access the camera and gallery. + abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, this.permissions).then(() => { + setTimeout(async () => { + // After obtaining permission, load the camera preview stream and ensure it is consistent with the aspect ratio of the surface. + this.cameraUtil?.setSurfaceProfile(this.xComponentController, this.getUIContext()); + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + }, 200); + }) + } + + aboutToDisappear(): void { + display.off('foldStatusChange'); + this.windowUtil!.offWindowSizeChange(); + } + + build() { + Navigation() { + Stack() { + // Camera View. + Column() { + XComponent({ + type: XComponentType.SURFACE, + controller: this.xComponentController + }) {} + .onLoad(async () => { + this.surfaceId = this.xComponentController.getXComponentSurfaceId(); + // Set surface to lock direction when screen rotates. + this.xComponentController.setXComponentSurfaceRotation({ lock: true }); + }) + .onDestroy(() => { + // Release camera session and streams. + this.cameraUtil?.releaseCamera(); + }) + .width('100%') + .height(this.isHalfFolded ? this.creaseRegion[0] : '100%') + } + .width('100%') + .height(this.isHalfFolded ? this.creaseRegion[0] : '') + .layoutWeight(this.isHalfFolded ? 0 : 1) + .margin({ bottom: deviceInfo.productSeries !== 'VDE' && this.widthBp === WidthBreakpoint.WIDTH_SM ? 156 : 0 }) + + // Shooting button view. + Stack() { + // Setting view for sm. + Column() { + SettingButton({ + imageButton: $r('app.media.icon_lighting'), + text: $r('app.string.flashlight') + }) + SettingButton({ + imageButton: $r('app.media.icon_filters'), + text: $r('app.string.filter') + }) + SettingButton({ + imageButton: $r('app.media.icon_setting'), + text: $r('app.string.setting') + }) + } + .width(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 30 : 48) + .height('100%') + .margin({ right: 16 }) + .padding({ top: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 16 : 108 }) + .visibility(this.widthBp === WidthBreakpoint.WIDTH_SM ? Visibility.Visible : Visibility.None) + + // Choose music for sm. + Row() { + Image($r('app.media.icon_close')) + .width(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) + .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) + .position({ x: 16, y: 0 }) + + ChooseMusic() + } + .width('100%') + .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 28 : 40) + .position({ + x: 0, + y: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 16 : 28 + }) + .justifyContent(FlexAlign.Center) + .visibility(this.widthBp === WidthBreakpoint.WIDTH_SM ? Visibility.Visible : Visibility.None) + + // Shooting button for sm. + Column() { + ShotAreaSm() + } + .visibility(this.widthBp === WidthBreakpoint.WIDTH_SM ? Visibility.Visible : Visibility.None) + .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 96 : 132) + .width('100%') + .margin({ bottom: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 16 : 84 }) + + // Setting view for md/lg. + Column() { + Column() { + Image($r('app.media.icon_close')) + .width(40) + .height(40) + } + .width(40) + .layoutWeight(1) + .padding({ top: this.widthBp === WidthBreakpoint.WIDTH_MD ? 24 : 32 }) + + Column() { + Blank() + SettingButton({ + imageButton: $r('app.media.icon_lighting'), + text: $r('app.string.flashlight') + }) + SettingButton({ + imageButton: $r('app.media.icon_filters'), + text: $r('app.string.filter') + }) + SettingButton({ + imageButton: $r('app.media.icon_setting'), + text: $r('app.string.setting') + }) + Blank() + } + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + .alignItems(HorizontalAlign.Start) + .width(48) + + Column() { + ChooseMusic() + } + .layoutWeight(1) + .width('100%') + .padding({ top: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 72 : 60 }) + } + .width(this.widthBp === WidthBreakpoint.WIDTH_MD ? 144 : 152 ) + .height('100%') + .justifyContent(FlexAlign.Start) + .padding({ left: this.heightBp === HeightBreakpoint.HEIGHT_MD ? 24 : 32 }) + .position({ x: 0, y: 0 }) + .alignItems(HorizontalAlign.Start) + .visibility(this.widthBp === WidthBreakpoint.WIDTH_MD || this.widthBp === WidthBreakpoint.WIDTH_LG ? + Visibility.Visible : Visibility.None) + + // Shooting button for md/lg. + Column() { + ShotArea() + } + .width(this.widthBp === WidthBreakpoint.WIDTH_LG && deviceInfo.productSeries === 'GRL' ? 132 : 92) + .height('100%') + .justifyContent(FlexAlign.Center) + .padding({ right: this.widthBp === WidthBreakpoint.WIDTH_LG && deviceInfo.productSeries === 'GRL' ? 56 : 16 }) + .visibility(this.widthBp === WidthBreakpoint.WIDTH_MD || this.widthBp === WidthBreakpoint.WIDTH_LG ? + Visibility.Visible : Visibility.None) + } + .height('100%') + .width('100%') + .alignContent(Alignment.BottomEnd) + .visibility(this.isHalfFolded ? Visibility.None : Visibility.Visible) + + // Half folded view. + Stack() { + Row() { + SettingButton({ + imageButton: $r('app.media.icon_lighting'), + text: $r('app.string.flashlight') + }) + SettingButton({ + imageButton: $r('app.media.icon_filters'), + text: $r('app.string.filter') + }) + .margin({ + left: 16, + right: 16 + }) + SettingButton({ + imageButton: $r('app.media.icon_setting'), + text: $r('app.string.setting') + }) + } + .width('100%') + .height(72) + .padding({ top: 6 }) + .justifyContent(FlexAlign.Center) + .position({ x: 0, y: this.creaseRegion[0] + this.creaseRegion[1] }) + + Row() { + ChooseMusic() + } + .width('100%') + .justifyContent(FlexAlign.Center) + .padding({ bottom: 32 }) + + Column() { + Image($r('app.media.icon_close')) + .width(40) + .height(40) + .position({ x: 0, y: this.creaseRegion[0] + this.creaseRegion[1] }) + + Column() { + Row() { + Text($r('app.string.photo')) + .fontSize(14) + .fontColor(Color.White) + .fontWeight(700) + Blank() + Image($r('app.media.icon_red_circle')) + .height(6) + .width(6) + } + .height(20) + .width(40) + .margin({ bottom: 16 }) + + Text($r('app.string.video')) + .fontSize(14) + .fontColor('#80FFFFFF') + .width(40) + } + .width(40) + .height(56) + .position({ x: 0, y: this.creaseRegion[0] + this.creaseRegion[1] + 108 }) + } + .width(64) + .padding({ left: 24 }) + .position({ x: 0, y: 0 }) + + ShotAreaHalfFolded() + .position({ x: this.creaseRegion[2] - 100, y: this.creaseRegion[0] + this.creaseRegion[1] + 24 }) + } + .width('100%') + .height('100%') + .alignContent(Alignment.BottomEnd) + .visibility(this.isHalfFolded ? Visibility.Visible : Visibility.None) + } + .height('100%') + .width('100%') + .alignContent(this.widthBp === WidthBreakpoint.WIDTH_MD ? (this.isHalfFolded ? Alignment.Top : Alignment.Start) : + Alignment.Center) + } + .height('100%') + .width('100%') + .mode(NavigationMode.Stack) + .hideTitleBar(true) + .hideToolBar(true) + .backgroundColor(Color.Black) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/BreakpointType.ets b/entry/src/main/ets/utils/BreakpointType.ets new file mode 100644 index 0000000000000000000000000000000000000000..bd2aab03a76db57a6318911db698bdbc6ad653ea --- /dev/null +++ b/entry/src/main/ets/utils/BreakpointType.ets @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export class BreakpointType { + sm: T; + md: T; + lg: T; + + constructor(sm: T, md: T, lg: T) { + this.sm = sm; + this.md = md; + this.lg = lg; + } + + getValue(widthBp: WidthBreakpoint): T { + if (widthBp === WidthBreakpoint.WIDTH_SM) { + return this.sm; + } + if (widthBp === WidthBreakpoint.WIDTH_MD) { + return this.md; + } + if (widthBp === WidthBreakpoint.WIDTH_LG || widthBp === WidthBreakpoint.WIDTH_XL) { + return this.lg; + } + return this.sm; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/CameraUtil.ets b/entry/src/main/ets/utils/CameraUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..75f05b37bbedeeec70f2f88ab291ae87a386f652 --- /dev/null +++ b/entry/src/main/ets/utils/CameraUtil.ets @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { camera } from '@kit.CameraKit'; +import { common, Context } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BusinessError, deviceInfo } from '@kit.BasicServicesKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { colorSpaceManager } from '@kit.ArkGraphics2D'; +import { sensor } from '@kit.SensorServiceKit'; +import { display, window } from '@kit.ArkUI'; +import { WindowUtil } from './WindowUtil'; + +export class CameraUtil { + previewOutput?: camera.PreviewOutput; + cameraInput?: camera.CameraInput; + photoSession?: camera.PhotoSession; + photoOutput?: camera.PhotoOutput; + uri: string = ''; + currentContext?: Context; + surfaceId: string = ''; + xComponentController?: XComponentController; + uiContext?: UIContext; + windowUtil?: WindowUtil = WindowUtil.getInstance(); + + static getInstance(): CameraUtil | undefined { + if (!AppStorage.get('cameraUtil')) { + AppStorage.setOrCreate('cameraUtil', new CameraUtil()); + } + return AppStorage.get('cameraUtil'); + } + + async cameraShooting(surfaceId: string, context: Context, cameraPosition: camera.CameraPosition): Promise { + this.surfaceId = surfaceId; + this.currentContext = context; + // Release camera session and streams, to avoid conflicts in camera resource usage. + this.releaseCamera(); + let cameraManager: camera.CameraManager = camera.getCameraManager(context); + if (!cameraManager) { + return; + } + // Obtaining the camera list. + let cameraArray: camera.CameraDevice[] = cameraManager.getSupportedCameras(); + if (cameraArray.length <= 0) { + return; + } + // Select front and rear camera. + let cameraIndex: number = this.getCamera(cameraArray, cameraPosition); + // Create a camera input stream based on the camera. + this.cameraInput = cameraManager.createCameraInput(cameraArray[cameraIndex]); + // Open the camera. + await this.cameraInput.open(); + // Get the supported modes of the camera and select the photo mode. + let sceneModes: camera.SceneMode[] = cameraManager.getSupportedSceneModes(cameraArray[cameraIndex]); + let cameraOutputCap: camera.CameraOutputCapability = + cameraManager.getSupportedOutputCapability(cameraArray[cameraIndex], camera.SceneMode.NORMAL_PHOTO); + let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0; + if (!isSupportPhotoMode) { + return; + } + if (!cameraOutputCap) { + return; + } + + // Get camera preview and photo configuration. + let previewProfileArray: camera.Profile[] = cameraOutputCap.previewProfiles; + let previewProfile: camera.Profile = this.getProfile(previewProfileArray); + let photoProfileArray: camera.Profile[] = cameraOutputCap.photoProfiles; + let photoProfile: camera.Profile = this.getProfile(photoProfileArray); + this.previewOutput = cameraManager.createPreviewOutput(previewProfile, surfaceId); + if (this.previewOutput === undefined) { + return; + } + + this.photoOutput = cameraManager.createPhotoOutput(photoProfile); + if (this.photoOutput === undefined) { + return; + } + // Save photo. + this.setPhotoOutputCb(); + // Configure the camera preview stream to match the aspect ratio of the surface, otherwise the preview page will be compressed or stretched. + this.setXComponentRect(this.windowUtil!.getWindowSize()); + // Create the camera session and config. + this.photoSession = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO); + if (this.photoSession === undefined) { + return; + } + this.photoSession.beginConfig(); + this.photoSession.addInput(this.cameraInput); + this.photoSession.addOutput(this.previewOutput); + this.photoSession.addOutput(this.photoOutput); + this.photoSession.setColorSpace(colorSpaceManager.ColorSpace.DISPLAY_P3); + await this.photoSession.commitConfig(); + await this.photoSession.start(); + return; + } + + getCamera(cameras: Array, cameraPosition: camera.CameraPosition): number { + let widthBp: WidthBreakpoint | undefined = AppStorage.get('widthBp'); + let heightBp: HeightBreakpoint | undefined = AppStorage.get('heightBp'); + // Three fold M-mode cannot use the front camera. + if (widthBp === WidthBreakpoint.WIDTH_MD && heightBp === HeightBreakpoint.HEIGHT_MD && + deviceInfo.productSeries === 'GRL') { + cameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK; + } + // Choose front or rear camera. + for (let i: number = 0; i < cameras.length; ++i) { + if (cameras[i].cameraPosition === cameraPosition) { + if (cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK) { + AppStorage.setOrCreate('isFront', false); + } + if (cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT) { + AppStorage.setOrCreate('isFront', true); + } + return i; + } + } + hilog.error(0x0000, 'testLog', `Failed to find the camera with the corresponding position.`); + if (cameras[0].cameraPosition === camera.CameraPosition.CAMERA_POSITION_BACK) { + AppStorage.setOrCreate('isFront', false); + } + if (cameras[0].cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT) { + AppStorage.setOrCreate('isFront', true); + } + return 0; + } + + getProfile(profileArray: camera.Profile[]): camera.Profile { + // Return profile based on breakpoint. + let widthBp: WidthBreakpoint | undefined = AppStorage.get('widthBp'); + let heightBp: HeightBreakpoint | undefined = AppStorage.get('heightBp'); + // Choose a preview or camera configuration with an aspect ratio of 4:3, and select a 1:1 configuration on the Purax external screen. + let aspectRatio: number = 4 / 3; + if (widthBp === WidthBreakpoint.WIDTH_SM && heightBp === HeightBreakpoint.HEIGHT_MD) { + aspectRatio = 1; + } + let maxWidth: number = 0; + let maxHeight: number = 0; + for (let i: number = 0; i < profileArray.length; ++i) { + if (profileArray[i].size.width / profileArray[i].size.height === aspectRatio && + profileArray[i].size.width > maxWidth) { + maxWidth = profileArray[i].size.width; + maxHeight = profileArray[i].size.height; + } + } + let resProfile: undefined | camera.Profile = profileArray.find((profile: camera.Profile) => { + return profile.size.width === maxWidth && profile.size.height === maxHeight; + }) + if (resProfile === undefined) { + hilog.error(0x0000, 'testLog', `Failed to get the profile.`); + return profileArray[0]; + } + return resProfile; + } + + setPhotoOutputCb(): void { + this.photoOutput!.on('photoAssetAvailable', async (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset): + Promise => { + if (err) { + hilog.error(0x0000, 'testLog', `Failed to save photo.`); + return; + } + let accessHelper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.currentContext); + let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = + new photoAccessHelper.MediaAssetChangeRequest(photoAsset); + assetChangeRequest.saveCameraPhoto(); + await accessHelper.applyChanges(assetChangeRequest); + // Save the address of the latest photo. + this.uri = photoAsset.uri; + AppStorage.setOrCreate('photoUri', await photoAsset.getThumbnail()); + }) + } + + capture(): void { + let rotation: number = -1; + // Obtain the angle of the gravity sensor during shooting and set the shooting rotation angle. + sensor.once(sensor.SensorId.GRAVITY, async (data: sensor.GravityResponse) => { + let degree: number = this.getCalDegree(data.x, data.y, data.z); + let isFront: boolean | undefined = AppStorage.get('isFront'); + if (degree >= 0 && (degree <= 30 || degree >= 300)) { + rotation = camera.ImageRotation.ROTATION_0; + } else if (degree > 30 && degree <= 120) { + if (isFront) { + // Use ROTATION_270 when degree range is (30, 120] for front camera. + rotation = camera.ImageRotation.ROTATION_270; + } else { + // Use ROTATION_90 when degree range is (30, 120] for back camera. + rotation = camera.ImageRotation.ROTATION_90; + } + } else if (degree > 120 && degree <= 210) { + // Use ROTATION_180 when degree range is (120, 210]. + rotation = camera.ImageRotation.ROTATION_180; + } else if (degree > 210 && degree <= 300) { + if (isFront) { + // Use ROTATION_90 when degree range is (210, 300] for front camera. + rotation = camera.ImageRotation.ROTATION_90; + } else { + // Use ROTATION_270 when degree range is (210, 300] for back camera. + rotation = camera.ImageRotation.ROTATION_270; + } + }; + + let setting: camera.PhotoCaptureSetting = { + quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, + rotation: rotation, + mirror: isFront + } + this.photoOutput?.capture(setting); + }) + } + + async releaseCamera(): Promise { + if (this.photoSession) { + this.photoSession.stop(); + } + if (this.cameraInput) { + this.cameraInput.close(); + } + if (this.previewOutput) { + this.previewOutput.release(); + } + if (this.photoSession) { + this.photoSession.stop(); + } + if (this.photoOutput) { + this.photoOutput.release(); + } + } + + async fromBack(): Promise { + let isFront: boolean | undefined = AppStorage.get('isFront'); + if (isFront) { + this.cameraShooting(this.surfaceId, this.currentContext!, camera.CameraPosition.CAMERA_POSITION_FRONT); + return; + } + this.cameraShooting(this.surfaceId, this.currentContext!, camera.CameraPosition.CAMERA_POSITION_BACK); + } + + previewPhoto(): void { + let photoContext: common.UIAbilityContext = this.currentContext as common.UIAbilityContext; + // Start the gallery application to preview photo. + photoContext.startAbility({ + parameters: { uri: this.uri }, + action: 'ohos.want.action.viewData', + bundleName: 'com.huawei.hmos.photos', + abilityName: 'com.huawei.hmos.photos.MainAbility' + }).then(() => { + hilog.info(0x0000, 'testLog', `Start ability successed.`); + }).catch((err: BusinessError) => { + hilog.error(0x0000, 'testLog', `Strat ability failed with err: ${err.code}, ${err.message}`); + }) + } + + getCalDegree(x: number, y: number, z: number): number { + let degree: number = -1; + // three is Effective Delta Angle Threshold Coefficient. + if ((x * x + y * y) * 3 < z * z) { + return degree; + } + degree = 90 - (Number)(Math.round(Math.atan2(y, -x) / Math.PI * 180)); + return degree >= 0 ? degree % 360 : degree % 360 + 360; + } + + setSurfaceProfile(xComponentController: XComponentController, uiContext: UIContext): void { + this.xComponentController = xComponentController; + this.uiContext = uiContext; + } + + setXComponentRect(windowSize: window.Size): void { + let creaseRegion: number[] | undefined = AppStorage.get('creaseRegion'); + // Initialize the width and height of the surface to match the full screen of the window. + let rect: SurfaceRect = { + surfaceWidth: windowSize.width, + surfaceHeight: windowSize.height + }; + // Set the width and height of the half folded page surface. + if (AppStorage.get('isHalfFolded')) { + rect.surfaceHeight = this.uiContext!.vp2px(creaseRegion![0]); + rect.surfaceWidth = this.uiContext!.vp2px(creaseRegion![0]) / 3 * 4; + this.xComponentController!.setXComponentSurfaceRect(rect); + return; + } + let widthBp: WidthBreakpoint = this.uiContext!.getWindowWidthBreakpoint(); + let heightBp: HeightBreakpoint = this.uiContext!.getWindowHeightBreakpoint(); + let displayOrientation: display.Orientation = display.getDefaultDisplaySync().orientation; + // Landscape for Mate X5 fold status expand, or Mate XT M-mode. + if (widthBp === WidthBreakpoint.WIDTH_MD && heightBp === HeightBreakpoint.HEIGHT_MD && (displayOrientation === + display.Orientation.LANDSCAPE || displayOrientation === display.Orientation.LANDSCAPE_INVERTED)) { + rect.surfaceHeight = windowSize.width / 4 * 3; + } + // Portrait for Mate X5 fold status expand, or Mate XT M-mode. + else if(widthBp === WidthBreakpoint.WIDTH_MD && heightBp === HeightBreakpoint.HEIGHT_MD && (displayOrientation === + display.Orientation.PORTRAIT || displayOrientation === display.Orientation.PORTRAIT_INVERTED)) { + rect.offsetX = 0; + rect.offsetY = 0; + rect.surfaceWidth = windowSize.height / 4 * 3; + } + // Landscape for tablet. + else if (widthBp === WidthBreakpoint.WIDTH_LG && heightBp === HeightBreakpoint.HEIGHT_SM && + deviceInfo.productSeries !== 'GRL') { + rect.surfaceWidth = windowSize.height / 3 * 4; + } + // Portrait for Mate XT G-mode. + else if (widthBp === WidthBreakpoint.WIDTH_MD && heightBp === HeightBreakpoint.HEIGHT_LG && + deviceInfo.productSeries === 'GRL') { + rect.surfaceHeight = windowSize.width / 4 * 3; + } + // Landscape for Mate XT G-mode. + else if (widthBp === WidthBreakpoint.WIDTH_LG && heightBp === HeightBreakpoint.HEIGHT_SM && + deviceInfo.productSeries === 'GRL') { + rect.surfaceWidth = windowSize.height / 4 * 3; + } + // Pura X external screen. + else if (widthBp === WidthBreakpoint.WIDTH_SM && heightBp === HeightBreakpoint.HEIGHT_MD) {} + // Portrait for phone and tablet. + else { + rect.surfaceHeight = windowSize.width / 3 * 4; + } + this.xComponentController!.setXComponentSurfaceRect(rect); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/utils/WindowUtil.ets b/entry/src/main/ets/utils/WindowUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..635df82a44628bb9e51c39fc335b1e5f22a88905 --- /dev/null +++ b/entry/src/main/ets/utils/WindowUtil.ets @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { display, window } from '@kit.ArkUI'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { BusinessError } from '@kit.BasicServicesKit'; + +export class WindowUtil { + windowStage?: window.WindowStage; + mainWindow?: window.Window; + + static getInstance(): WindowUtil | undefined { + if (!AppStorage.get('windowUtil')) { + AppStorage.setOrCreate('windowUtil', new WindowUtil()); + } + return AppStorage.get('windowUtil'); + } + + setWindowStage(windowStage: window.WindowStage): void { + this.windowStage = windowStage; + this.windowStage.getMainWindow((err, windowClass: window.Window) => { + this.mainWindow = windowClass; + if (err.code) { + hilog.error(0x0000, 'testTag', `Failed to obtain the main window. Code:${err.code}, message:${err.message}`, + JSON.stringify(err) ?? ''); + return; + } + }); + } + + setMainWindowOrientation(orientation: window.Orientation): void { + // Setting orientation. + this.mainWindow!.setPreferredOrientation(orientation) + .then(() => { + hilog.info(0x0000, 'testTag', '%{public}s', `Succeed in setting the orientation.`); + }) + .catch((err: BusinessError) => { + hilog.error(0x0000, 'testTag', `Failed to set the orientation. Code: ${err.code}, message: ${err.message}`, + JSON.stringify(err) ?? ''); + }); + } + + offWindowSizeChange(): void { + this.mainWindow!.off('windowSizeChange'); + } + + getFoldCreaseRegion(context: UIContext): void { + if (display.isFoldable()) { + let foldRegion: display.FoldCreaseRegion = display.getCurrentFoldCreaseRegion(); + let rect: display.Rect = foldRegion.creaseRects[0]; + // Height of the avoidance area in the upper half screen and height of the avoidance area. + let creaseRegion: number[] = [context.px2vp(rect.top), context.px2vp(rect.height), context.px2vp(rect.width)]; + AppStorage.setOrCreate('creaseRegion', creaseRegion); + } + } + + getWindowSize(): window.Size { + let rect: window.Rect = this.mainWindow!.getWindowProperties().windowRect; + let result: window.Size = { + width: rect.width, + height: rect.height + }; + return result; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/views/CommonView.ets b/entry/src/main/ets/views/CommonView.ets new file mode 100644 index 0000000000000000000000000000000000000000..a6fc0a0752a0bed8894dd55eb6ca154ed568a178 --- /dev/null +++ b/entry/src/main/ets/views/CommonView.ets @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { curves } from '@kit.ArkUI'; +import { camera } from '@kit.CameraKit'; +import { BreakpointType } from '../utils/BreakpointType'; +import { CameraUtil } from '../utils/CameraUtil'; + +@Component +export struct ChooseMusic { + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + + build() { + Row() { + Image($r('app.media.icon_music')) + .width(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 12 : 16) + .height(this.heightBp === HeightBreakpoint.HEIGHT_MD ? 12 : 16) + .margin({ right: 10 }) + Text($r('app.string.choose_music')) + .fontSize(16) + .lineHeight(21) + .fontColor(Color.White) + } + .width(120) + .height(40) + .backgroundColor('#1AFFFFFF') + .justifyContent(FlexAlign.Center) + .borderRadius(8) + } +} + +@Component +export struct SettingButton { + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + @StorageLink('widthBp') widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM; + imageButton?: Resource; + text?: Resource; + + build() { + Column() { + Image(this.imageButton) + .height(new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 48 : 30), 48, 48).getValue(this.widthBp)) + .width(new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 48 : 30), 48, 48).getValue(this.widthBp)) + .margin({ bottom: 2 }) + Text(this.text) + .lineHeight(new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 16 : 14), 16, 16).getValue(this.widthBp)) + .fontSize(new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 12 : 10), 12, 12).getValue(this.widthBp)) + .fontColor(Color.White) + } + .width(40) + .margin({ + bottom: new BreakpointType((this.heightBp === HeightBreakpoint.HEIGHT_LG ? 16 : 8), 16, 16).getValue(this.widthBp)}) + } +} + +@Component +export struct ShotAreaSm { + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + @StorageLink('photoUri') photoUri: string | Resource | PixelMap = ''; + @StorageLink('surfaceId') surfaceId: string = ''; + cameraUtil?: CameraUtil = CameraUtil.getInstance(); + context?: Context = this.getUIContext().getHostContext(); + + build() { + Column() { + // Photo and video mode. + Column() { + Row() { + Column() { + Text($r('app.string.photo')) + .fontSize(14) + .lineHeight(20) + .fontColor(Color.White) + .fontWeight(700) + .margin({ bottom: 6 }) + Image($r('app.media.icon_red_circle')) + .height(6) + .width(6) + } + .height('100%') + .width(28) + + Blank() + + Text($r('app.string.video')) + .fontSize(14) + .lineHeight(20) + .fontColor(Color.White) + } + .height(32) + .width(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 80 : 72) + .alignItems(VerticalAlign.Top) + } + .width(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 80 : 72) + .height(32) + .margin({ bottom: this.heightBp === HeightBreakpoint.HEIGHT_LG ? 24 : 16 }) + + // Shooting button. + Row() { + Image(this.photoUri) + .width(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 44 : 28) + .height(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 44 : 28) + .borderWidth(this.photoUri === '' ? 0 : 1) + .borderColor(Color.White) + .borderRadius(44) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + if (this.photoUri !== '') { + this.cameraUtil?.previewPhoto(); + } + }) + Image($r('app.media.icon_shoot')) + .width(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 76 : 46) + .height(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 76 : 46) + .onClick(() => { + this.cameraUtil?.capture(); + }) + Image($r('app.media.icon_flip')) + .width(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 44 : 28) + .height(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 44 : 28) + .onClick(() => { + let isFront: boolean | undefined = AppStorage.get('isFront'); + if (isFront) { + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + return; + } + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_FRONT); + }) + } + .width(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 256 : 158) + .height(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 76 : 46) + .justifyContent(FlexAlign.SpaceBetween) + } + .width('100%') + .height(this.heightBp === HeightBreakpoint.HEIGHT_LG ? 132 : 94) + .margin({ bottom: this.heightBp === HeightBreakpoint.HEIGHT_LG ? 84 : 16 }) + } +} + +@Component +export struct ShotArea { + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + @StorageLink('widthBp') widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM; + @StorageLink('photoUri') photoUri: string | Resource | PixelMap = ''; + @StorageLink('surfaceId') surfaceId: string = ''; + cameraUtil?: CameraUtil = CameraUtil.getInstance(); + context?: Context = this.getUIContext().getHostContext(); + + build() { + Stack() { + Column() { + Image($r('app.media.icon_flip')) + .width(44) + .height(44) + .onClick(() => { + let isFront: boolean | undefined = AppStorage.get('isFront'); + if (isFront) { + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + return; + } + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_FRONT); + }) + Image($r('app.media.icon_shoot')) + .width(76) + .height(76) + .onClick(() => { + this.cameraUtil?.capture(); + }) + Image(this.photoUri) + .width(44) + .height(44) + .borderWidth(this.photoUri === '' ? 0 : 1) + .borderColor(Color.White) + .borderRadius(22) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + if (this.photoUri !== '') { + this.cameraUtil?.previewPhoto(); + } + }) + } + .height(288) + .width('100%') + .justifyContent(FlexAlign.SpaceBetween) + + Column() { + Row() { + Text($r('app.string.photo')) + .fontSize(14) + .fontColor(Color.White) + .fontWeight(700) + Blank() + Image($r('app.media.icon_red_circle')) + .height(6) + .width(6) + } + .height(20) + .width(40) + .margin({ bottom: 16 }) + + Text($r('app.string.video')) + .fontSize(14) + .fontColor('#80FFFFFF') + .width(40) + } + .width('100%') + .height(56) + .alignItems(HorizontalAlign.Center) + .position({ x: 0, y: 416 }) + } + .height(472) + .width('100%') + .alignContent(Alignment.Center) + } +} + +@Component +export struct ShotAreaHalfFolded { + @StorageLink('heightBp') heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM; + @StorageLink('widthBp') widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_SM; + @StorageLink('photoUri') photoUri: string | Resource | PixelMap = ''; + @StorageLink('surfaceId') surfaceId: string = ''; + cameraUtil?: CameraUtil = CameraUtil.getInstance(); + context?: Context = this.getUIContext().getHostContext(); + + build() { + Column() { + Image($r('app.media.icon_flip')) + .width(44) + .height(44) + .onClick(() => { + let isFront: boolean | undefined = AppStorage.get('isFront'); + if (isFront) { + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_BACK); + return; + } + this.cameraUtil?.cameraShooting(this.surfaceId, this.context!, camera.CameraPosition.CAMERA_POSITION_FRONT); + }) + Image($r('app.media.icon_shoot')) + .width(76) + .height(76) + .onClick(() => { + this.cameraUtil?.capture(); + }) + Image(this.photoUri) + .width(44) + .height(44) + .borderWidth(this.photoUri === '' ? 0 : 1) + .borderColor(Color.White) + .borderRadius(22) + .animation({ curve: curves.springMotion() }) + .onClick(() => { + if (this.photoUri !== '') { + this.cameraUtil?.previewPhoto(); + } + }) + } + .height(236) + .width(76) + .justifyContent(FlexAlign.SpaceBetween) + .margin({ right: 24 }) + } +} \ No newline at end of file diff --git a/entry/src/main/module.json5 b/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..190d4fae9255a9dc0203d063640879d05280d625 --- /dev/null +++ b/entry/src/main/module.json5 @@ -0,0 +1,85 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "supportWindowMode": ["fullscreen"], + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.CAMERA", + "reason": "$string:reason_camera", + "usedScene": { + "when": "inuse", + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.WRITE_IMAGEVIDEO", + "reason": "$string:reason_write_image_video", + "usedScene": { + "when": "inuse", + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.READ_IMAGEVIDEO", + "reason": "$string:reason_read_image_video", + "usedScene": { + "when": "inuse", + "abilities": [ + "EntryAbility" + ] + } + } + ] + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/color.json b/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/element/float.json b/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..33ea22304f9b1485b5f22d811023701b5d4e35b6 --- /dev/null +++ b/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..581eb8611b94aa3ef2bf21b718a67fda4ba3dc10 --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "Multi-device camera application." + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "MultiDeviceCamera" + }, + { + "name": "reason_camera", + "value": "To access the system camera" + }, + { + "name": "reason_write_image_video", + "value": "To write the album" + }, + { + "name": "reason_read_image_video", + "value": "To read the album" + }, + { + "name": "flashlight", + "value": "flashlight" + }, + { + "name": "filter", + "value": "filter" + }, + { + "name": "setting", + "value": "setting" + }, + { + "name": "photo", + "value": "photo" + }, + { + "name": "video", + "value": "video" + }, + { + "name": "choose_music", + "value": "choose music" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/1x.png b/entry/src/main/resources/base/media/1x.png new file mode 100644 index 0000000000000000000000000000000000000000..3fc8a051a6fd8f54d3a8c20f8d45ebe3b2fc94ba Binary files /dev/null and b/entry/src/main/resources/base/media/1x.png differ diff --git a/entry/src/main/resources/base/media/background.png b/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/entry/src/main/resources/base/media/background.png differ diff --git a/entry/src/main/resources/base/media/foreground.png b/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/entry/src/main/resources/base/media/foreground.png differ diff --git a/entry/src/main/resources/base/media/icon_close.png b/entry/src/main/resources/base/media/icon_close.png new file mode 100644 index 0000000000000000000000000000000000000000..adb796b355341e8282fd8b107ca0978cef86a120 Binary files /dev/null and b/entry/src/main/resources/base/media/icon_close.png differ diff --git a/entry/src/main/resources/base/media/icon_filters.png b/entry/src/main/resources/base/media/icon_filters.png new file mode 100644 index 0000000000000000000000000000000000000000..79e0d2372b2ed4f7b365fbb32316f9a60839a025 Binary files /dev/null and b/entry/src/main/resources/base/media/icon_filters.png differ diff --git a/entry/src/main/resources/base/media/icon_flip.png b/entry/src/main/resources/base/media/icon_flip.png new file mode 100644 index 0000000000000000000000000000000000000000..addf340daf016e9c3816ed1fc15a70cacf9e20c0 Binary files /dev/null and b/entry/src/main/resources/base/media/icon_flip.png differ diff --git a/entry/src/main/resources/base/media/icon_lighting.png b/entry/src/main/resources/base/media/icon_lighting.png new file mode 100644 index 0000000000000000000000000000000000000000..b523ebdf7d2e4a728f4f866ee0e88a30f5785dad Binary files /dev/null and b/entry/src/main/resources/base/media/icon_lighting.png differ diff --git a/entry/src/main/resources/base/media/icon_music.png b/entry/src/main/resources/base/media/icon_music.png new file mode 100644 index 0000000000000000000000000000000000000000..98202621bf34745a28efb53319e9d5f9fbe8f984 Binary files /dev/null and b/entry/src/main/resources/base/media/icon_music.png differ diff --git a/entry/src/main/resources/base/media/icon_red_circle.png b/entry/src/main/resources/base/media/icon_red_circle.png new file mode 100644 index 0000000000000000000000000000000000000000..09e304c892373103a7f582231baaa741263bc8d4 Binary files /dev/null and b/entry/src/main/resources/base/media/icon_red_circle.png differ diff --git a/entry/src/main/resources/base/media/icon_setting.png b/entry/src/main/resources/base/media/icon_setting.png new file mode 100644 index 0000000000000000000000000000000000000000..0cb1c041c926f5ff7543ff281c5fa3d02a09b2a5 Binary files /dev/null and b/entry/src/main/resources/base/media/icon_setting.png differ diff --git a/entry/src/main/resources/base/media/icon_shoot.png b/entry/src/main/resources/base/media/icon_shoot.png new file mode 100644 index 0000000000000000000000000000000000000000..56581426afa95fc51429effd88bd66a888f06caa Binary files /dev/null and b/entry/src/main/resources/base/media/icon_shoot.png differ diff --git a/entry/src/main/resources/base/media/layered_image.json b/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/startIcon.png b/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/entry/src/main/resources/base/media/startIcon.png differ diff --git a/entry/src/main/resources/base/profile/backup_config.json b/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/entry/src/main/resources/base/profile/main_pages.json b/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce --- /dev/null +++ b/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/entry/src/main/resources/dark/element/color.json b/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..79b11c2747aec33e710fd3a7b2b3c94dd9965499 --- /dev/null +++ b/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/en_US/element/string.json b/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..581eb8611b94aa3ef2bf21b718a67fda4ba3dc10 --- /dev/null +++ b/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "Multi-device camera application." + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "MultiDeviceCamera" + }, + { + "name": "reason_camera", + "value": "To access the system camera" + }, + { + "name": "reason_write_image_video", + "value": "To write the album" + }, + { + "name": "reason_read_image_video", + "value": "To read the album" + }, + { + "name": "flashlight", + "value": "flashlight" + }, + { + "name": "filter", + "value": "filter" + }, + { + "name": "setting", + "value": "setting" + }, + { + "name": "photo", + "value": "photo" + }, + { + "name": "video", + "value": "video" + }, + { + "name": "choose_music", + "value": "choose music" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/zh_CN/element/string.json b/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..55fe2004709298a95652f8bea414edb0222cf9fc --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "多设备相机应用" + }, + { + "name": "EntryAbility_desc", + "value": "描述" + }, + { + "name": "EntryAbility_label", + "value": "多设备相机" + }, + { + "name": "reason_camera", + "value": "访问相机权限" + }, + { + "name": "reason_write_image_video", + "value": "写入图库权限" + }, + { + "name": "reason_read_image_video", + "value": "读取图库权限" + }, + { + "name": "flashlight", + "value": "闪光灯" + }, + { + "name": "filter", + "value": "滤镜" + }, + { + "name": "setting", + "value": "设置" + }, + { + "name": "photo", + "value": "照片" + }, + { + "name": "video", + "value": "视频" + }, + { + "name": "choose_music", + "value": "选择音乐" + } + ] +} \ No newline at end of file diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..fe83c3e838abfb3629eed84f6c7092f306d818f9 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.2", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/hvigorfile.ts b/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3cb9f1a87a81687554a76283af8df27d8bda775 --- /dev/null +++ b/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/local.properties b/local.properties new file mode 100644 index 0000000000000000000000000000000000000000..20f608699522ce5b5f170457336fbfbf96165c62 --- /dev/null +++ b/local.properties @@ -0,0 +1,9 @@ +# This file is automatically generated by DevEco Studio. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file should *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# For customization when using a Version Control System, please read the header note. + + diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..b185185d31c2454034ed6fbd80bcc73beea65fc6 --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,8 @@ +{ + "modelVersion": "5.0.2", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + } +} diff --git a/screenshots/device/mate60.png b/screenshots/device/mate60.png new file mode 100644 index 0000000000000000000000000000000000000000..8b97cd0987d7fcc306d35dbb0eb8bfc99199e3aa Binary files /dev/null and b/screenshots/device/mate60.png differ diff --git a/screenshots/device/matepadpro.png b/screenshots/device/matepadpro.png new file mode 100644 index 0000000000000000000000000000000000000000..b11841b70e0d11aebf64f8098a6e506f30959ead Binary files /dev/null and b/screenshots/device/matepadpro.png differ diff --git a/screenshots/device/matex5.png b/screenshots/device/matex5.png new file mode 100644 index 0000000000000000000000000000000000000000..72800662fb76fe4c6c62b549bd8bd66405bce474 Binary files /dev/null and b/screenshots/device/matex5.png differ diff --git a/screenshots/device/matext.png b/screenshots/device/matext.png new file mode 100644 index 0000000000000000000000000000000000000000..108fd5eea0c81a33a27ae08363b7d9c74045d260 Binary files /dev/null and b/screenshots/device/matext.png differ diff --git a/screenshots/device/purax.png b/screenshots/device/purax.png new file mode 100644 index 0000000000000000000000000000000000000000..d45c0f8ea977e9002a0fb2af73b5312b8b9e00f5 Binary files /dev/null and b/screenshots/device/purax.png differ