diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/AppScope/app.json5 b/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4ed317c0a385e739eb244e31fb2038ceec68a848 --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.puretabs", + "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..0de36ad409aa741e52c885c8842ac4d1a1babfbc --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "PureTabs" + } + ] +} 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/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..18795a48d6b12fcdc1aa7bac9a9cb99f83815267 --- /dev/null +++ b/LICENSE @@ -0,0 +1,78 @@ + Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved. + + 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. + +Apache License, Version 2.0 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +1.You must give any other recipients of the Work or Derivative Works a copy of this License; and +2.You must cause any modified files to carry prominent notices stating that You changed the files; and +3.You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +4.If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/README.md b/README.md index 691b4cc34d97caf1eca71a9999d240342af1ba55..baef6ad2b526c3f1440cba1c6a7d93adb851a65f 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,90 @@ -# PureTabs +# Tabs开发实践案例 +### 简介 + 本示例主要展示了HarmonyOS中ArkUI框架下的Tabs组件在日常开发中常用的场景实践,将各子场景结合设计出的一个综合场景,并使用MVVM模式组织代码,示例包含如下场景 +- 实现双层嵌套Tabs +- 实现tabs滑动吸顶 +- 实现底部自定义变化页签 +- 实现顶部可滑动标签 +- 实现增删现实页签项 +- 实现Tabs切换动效 +### 效果预览 +| 双层Tabs嵌套,自定义页签样式 | 页签上滑吸顶 | 可滑动页签,增删显示页签 | Tabs切换动效 | +|---------------------------------------|---------------------------------------|---------------------------------------|---------------------------------------| +| ![image](screenshots/device/tab1.gif) | ![image](screenshots/device/tab2.gif) | ![image](screenshots/device/tab3.gif) | ![image](screenshots/device/tab4.gif) | +### 工程目录 +``` +├──entry/src/main/ets // 代码区 +│ ├──common // +│ │ ├──constant +│ │ │ └──Constants.ets // 常量类 +│ │ └──utils +│ │ └──StringUtil.ets // 字符串工具类 +│ ├──entryability +│ │ └──EntryAbility.ets +│ ├──entrybackupability +│ │ └──EntryBackupAbility.ets +│ ├──model +│ │ ├──ContentItemModel.ets // 展示内容项数据类 +│ │ ├──InTabsModel.ets // 展示内容列表数据类 +│ │ ├──SelectTabsModel.ets // 选择显示页签数据类 +│ │ └──TabItemModel.ets // 页签项数据类 +│ ├──pages +│ │ └──Index.ets // 页面入口 +│ ├──view +│ │ ├──BannerComponent.ets // 广告栏组件 +│ │ ├──ContentItemComponent.ets // 内容项组件 +│ │ ├──InTabsComponent.ets // 内层tabs组件 +│ │ ├──OtherTabComponent.ets // 其他外层tabs内容组件,展示不同背景颜色 +│ │ ├──OutTabsComponent.ets // 外层tabs组件 +│ │ ├──SearchBarComponent.ets // 搜索栏组件 +│ │ └──SelectTabsComponent.ets // 内层tabs增删控制组件 +│ └──viewmodel +│ ├──ContentItemViewModel.ets // 内容项业务数据类 +│ ├──InTabsViewModel.ets // 内层tabs业务数据类 +│ ├──SelectTabsViewModel.ets // 增删页签业务数据类 +│ └──TabItemViewModel.ets // 单个页签业务数据类 +└──entry/src/main/resources // 应用资源目录 +``` +### 具体实现 +- 实现双层嵌套Tabs + - 外层Tabs和内层Tabs均可滑动切换页签,内层滑到尽头触发外层滑动 + - 在内层Tabs最后一个TabContent上监听滑动手势,通过@Link传递变量到父组件的外层Tabs,然后通过外层Tabs的TabController控制其滑动 +- 实现Tabs滑动吸顶 + - Tabs父组件外及Tabs的TabContent组件内嵌套可滑动组件 + - 在TabContent内可滑动组件上设置滑动行为属性nestedScroll,使其往上滑动时,父组件先动,往下滑动时自己先动 +- 实现底部自定义变化页签 + - @Builder装饰器修饰的自动逸builder函数,传递给TabBar,实现自定义样式 + - 设置currentIndex属性,记录当前选择的页签,并且@Builder修饰的TabBar构建函数中利用其值来区分当前页签是否被选中,以呈现不同的样式 +- 实现顶部可滑动标签 + - 设置Tabs组件属性barMode(BarMode.Scrollable),页签显示不下的时候就可滑动 +- 实现增删现实页签项 + - 利用@Link双向绑定selectTabsViewModel到InTabsComponent和SelectTabsComponent + - SelectTabsComponent选中需要显示的页签项,在退出模态框时调用selectTabsViewModel.updateSelectedTabs,更新可显示页签 + - 更新后通过@Link的机制传递到InTabsComponent,触发UI刷新,显示新选择的页签 +- 实现Tabs切换动效 + - 在Tabs上注册动画方法customContentTransition(this.customContentTransition) + - 在动画方法中修改TabContent的尺寸属性和透明属性,并通过@State修饰后传递给TabContent,来实现动画 +### 相关权限 +本示例中部分图片使用了网络资源,因此本示例需要申请系统网络权限,配置如下 -#### 介绍 -{**以下是 Gitee 平台说明,您可以替换此简介** -Gitee 是 OSCHINA 推出的基于 Git 的代码托管平台(同时支持 SVN)。专为开发者提供稳定、高效、安全的云端软件开发协作平台 -无论是个人、团队、或是企业,都能够用 Gitee 实现代码托管、项目管理、协作开发。企业项目请看 [https://gitee.com/enterprises](https://gitee.com/enterprises)} - -#### 软件架构 -软件架构说明 - - -#### 安装教程 - -1. xxxx -2. xxxx -3. xxxx - -#### 使用说明 - -1. xxxx -2. xxxx -3. xxxx - -#### 参与贡献 - -1. Fork 本仓库 -2. 新建 Feat_xxx 分支 -3. 提交代码 -4. 新建 Pull Request - - -#### 特技 - -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/) +src/main/module.json5 +``` +{ + "module": { + //... + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + } + ] + } +} +``` +### 约束与限制 +- API: API 17 +- SDK版本:5.0.4(17) +- 兼容最低版本:5.0.3(15) +- 单双框架:单框架 +- FA/Stage模型:Stage模型 + +注意:本示例中使用了tabs的barModifier特性,该特性在5.0.3(15)及以上的SDK版本中才受支持,所以该示例在5.0.3(15)版本以下,会出现内层页签无法靠左对齐 \ No newline at end of file diff --git a/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..d9db0842893a336a228e6380c112f0f49b53116e --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,56 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\tao\\.ohos\\config\\default_PureTabs_6hla1It1o6OhqKS-pFkQj7g6coHhsp0sSEEKiewDGwg=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B3CEBD37B5A231B8AF3A82870A3D498C97DE2E83EB76A756186B35940DF908741C43AF5A6C54E73", + "profile": "C:\\Users\\tao\\.ohos\\config\\default_PureTabs_6hla1It1o6OhqKS-pFkQj7g6coHhsp0sSEEKiewDGwg=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\tao\\.ohos\\config\\default_PureTabs_6hla1It1o6OhqKS-pFkQj7g6coHhsp0sSEEKiewDGwg=.p12", + "storePassword": "0000001BC66CCE002573BE4C47635C24C4967599E965528545173A36A7155378550EAC27CE73AB053FC8DB" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "5.0.5(17)", + "compatibleSdkVersion": "5.0.5(17)", + "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/.gitignore b/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ 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/common/constant/Constants.ets b/entry/src/main/ets/common/constant/Constants.ets new file mode 100644 index 0000000000000000000000000000000000000000..1cb70164f485e52a87dd96b5058a1d0c75c0776c --- /dev/null +++ b/entry/src/main/ets/common/constant/Constants.ets @@ -0,0 +1,25 @@ +/* + * 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 Constants { + /** + * Full screen width. + */ + static readonly FULL_WIDTH: string = '100%'; + /** + * Full screen height. + */ + static readonly FULL_HEIGHT: string = '100%'; +} \ No newline at end of file diff --git a/entry/src/main/ets/common/utils/StringUtil.ets b/entry/src/main/ets/common/utils/StringUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..3320f0a887c6ac81d87bb7475f9b2ecf5cdc2248 --- /dev/null +++ b/entry/src/main/ets/common/utils/StringUtil.ets @@ -0,0 +1,35 @@ +/* + * 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 { util } from "@kit.ArkTS"; +import { BusinessError } from "@kit.BasicServicesKit"; +import { hilog } from "@kit.PerformanceAnalysisKit"; + +export default class StringUtil { + static async getStringFromRawFile(ctx: Context, source: string) { + try { + let getJson = await ctx.resourceManager.getRawFileContent(source); + let textDecoder = util.TextDecoder.create('utf-8', { ignoreBOM: true }); + let result = textDecoder.decodeToString(getJson); + return Promise.resolve(result); + } catch (error) { + let code = (error as BusinessError).code; + let message = (error as BusinessError).message; + hilog.error(0x0000, 'StringUtil', 'getStringSync failed,error code: %{code}s,message: %{message}s.', code, + message); + return Promise.reject(error); + } + } +} \ No newline at end of file diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..d3ee54d2beea61d38866762868bd056d3e39e3c9 --- /dev/null +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,59 @@ +/* + * 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 { window } from '@kit.ArkUI'; + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + 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'); + + 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.'); + }); + } + + 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'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ 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/model/ContentItemModel.ets b/entry/src/main/ets/model/ContentItemModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..f44ca8cbfdf6d5c5dd6b2d82f6d004255d57ca59 --- /dev/null +++ b/entry/src/main/ets/model/ContentItemModel.ets @@ -0,0 +1,25 @@ +/* + * 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 default class ContentItemModel { + username: string | Resource = ''; + publishTime: string | Resource = ''; + rawTitle: string | Resource = ''; + title: string | Resource = ''; + imgUrl1: string | Resource = ''; + imgUrl2: string | Resource = ''; + imgUrl3: string | Resource = ''; + imgUrl4: string | Resource = ''; +} diff --git a/entry/src/main/ets/model/InTabsModel.ets b/entry/src/main/ets/model/InTabsModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..7351052b2a7e65d4453cdfffe051ef1b8004ead8 --- /dev/null +++ b/entry/src/main/ets/model/InTabsModel.ets @@ -0,0 +1,52 @@ +/* + * 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 ContentItemModel from "./ContentItemModel"; +import StringUtil from "../common/utils/StringUtil"; + +export default class InTabsModel { + contentItems: ContentItemModel[] = []; + + async loadContentItems(ctx: Context) { + let filename = await ctx.resourceManager.getStringValue($r('app.string.default_content_items_file')); + + let res = await StringUtil.getStringFromRawFile(ctx, filename); + + this.contentItems = JSON.parse(res).map((item: ContentItemModel) => { + + let img1 = item.imgUrl1 as string; + if (img1.indexOf('app.media') === 0) { + item.imgUrl1 = $r(img1); + } + + let img2 = item.imgUrl2 as string; + if (img2.indexOf('app.media') === 0) { + item.imgUrl2 = $r(img2); + } + + let img3 = item.imgUrl3 as string; + if (img3.indexOf('app.media') === 0) { + item.imgUrl3 = $r(img3); + } + + let img4 = item.imgUrl4 as string; + if (img4.indexOf('app.media') === 0) { + item.imgUrl4 = $r(img4); + } + + return item; + }); + } +} diff --git a/entry/src/main/ets/model/SelectTabsModel.ets b/entry/src/main/ets/model/SelectTabsModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..e286276b17d3e15d5436ac72d6422d7fa0a924ad --- /dev/null +++ b/entry/src/main/ets/model/SelectTabsModel.ets @@ -0,0 +1,27 @@ +/* + * 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 TabItemModel from "./TabItemModel"; +import StringUtil from "../common/utils/StringUtil"; + +export default class SelectTabsModel { + allTabs: TabItemModel[] = []; + + async loadAllTabs(ctx: Context) { + let filename = await ctx.resourceManager.getStringValue($r('app.string.default_all_tabs_file')); + let result = await StringUtil.getStringFromRawFile(ctx, filename); + this.allTabs = JSON.parse(result); + } +} \ No newline at end of file diff --git a/entry/src/main/ets/model/TabItemModel.ets b/entry/src/main/ets/model/TabItemModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..dc69da643d96d90f67c3c6d92bcec4f91d6a216f --- /dev/null +++ b/entry/src/main/ets/model/TabItemModel.ets @@ -0,0 +1,20 @@ +/* + * 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 default class TabItemModel { + id: number = 0; + name: string | Resource = ''; + isChecked: boolean = true; +} \ 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..12b9c6dd664e7a7cef14bebf8bd404733643da0b --- /dev/null +++ b/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,35 @@ +/* + * 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 { Constants } from '../common/constant/Constants'; +import OutTabsComponent from '../view/OutTabsComponent'; +import SearchBarComponent from '../view/SearchBarComponent'; + +@Entry +@Component +struct Index { + build() { + Column() { + // searchBar + SearchBarComponent() + + //outerTabs + OutTabsComponent() + } + .height(Constants.FULL_HEIGHT) + .width(Constants.FULL_WIDTH) + .expandSafeArea([SafeAreaType.SYSTEM]) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/BannerComponent.ets b/entry/src/main/ets/view/BannerComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..7724f18b15063f9e7321c1b25792aa7339aeaf6b --- /dev/null +++ b/entry/src/main/ets/view/BannerComponent.ets @@ -0,0 +1,33 @@ +/* + * 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 { Constants } from "../common/constant/Constants"; + +@Component +export default struct BannerComponent { + build() { + Column() { + Image($r('app.media.pic5')) + .width(Constants.FULL_WIDTH) + .height(186) + .borderRadius(16) + } + .margin({ + left: 10, + right: 10, + top: 10, + bottom: 2 + }) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/ContentItemComponent.ets b/entry/src/main/ets/view/ContentItemComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..63a0aff0bae28e4c28c2cb4e7c7aa04a7d7e0e3c --- /dev/null +++ b/entry/src/main/ets/view/ContentItemComponent.ets @@ -0,0 +1,69 @@ +/* + * 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 ContentItemViewModel from "../viewmodel/ContentItemViewModel"; + + +@Component +export default struct ContentItemComponent { + @Prop contentItemViewModel: ContentItemViewModel; + + build() { + Flex({ direction: FlexDirection.Column }) { + Row() { + Image(this.contentItemViewModel.imgUrl1) + .width(30) + .borderRadius(15) + Column() { + Text(this.contentItemViewModel.username) + .fontSize(15) + Text(this.contentItemViewModel.publishTime) + .fontSize(12) + .fontColor($r('app.color.content_item_text_color')) + } + .margin({ left: 5 }) + .justifyContent(FlexAlign.Start) + .alignItems(HorizontalAlign.Start) + } + .margin(10) + + Column() { + Text() { + Span(this.contentItemViewModel.title) + } + .fontSize(16) + .id('title') + .width(350) + } + .margin({ left: 20, bottom: 20, right: 20 }) + + Row() { + Image(this.contentItemViewModel.imgUrl2) + .width(110) + .height(110) + .margin({ right: 10 }) + Image(this.contentItemViewModel.imgUrl3) + .width(110) + .height(110) + .margin({ right: 10 }) + Image(this.contentItemViewModel.imgUrl4) + .width(110) + .height(110) + .margin({ right: 10 }) + } + } + + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/InTabsComponent.ets b/entry/src/main/ets/view/InTabsComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..df199d8207f2ada9b05b92514d0b3be23eb61671 --- /dev/null +++ b/entry/src/main/ets/view/InTabsComponent.ets @@ -0,0 +1,263 @@ +/* + * 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 { CommonModifier } from "@kit.ArkUI"; +import { EnvironmentCallback, Configuration, AbilityConstant } from "@kit.AbilityKit"; +import SelectTabsViewModel from "../viewmodel/SelectTabsViewModel"; +import BannerComponent from "./BannerComponent"; +import TabItemViewModel from "../viewmodel/TabItemViewModel"; +import ContentItemViewModel from "../viewmodel/ContentItemViewModel"; +import ContentItemComponent from "./ContentItemComponent"; +import SelectTabsComponent from "./SelectTabsComponent"; +import InTabsViewModel from "../viewmodel/InTabsViewModel"; +import { Constants } from "../common/constant/Constants"; + +@Component +export default struct InTabsComponent { + @State selectTabsViewModel: SelectTabsViewModel = new SelectTabsViewModel(); + @State inTabsViewModel: InTabsViewModel = new InTabsViewModel(); + @State tabBarModifier: CommonModifier = new CommonModifier(); + @State focusIndex: number = 0; + @State scaleList: number[] = []; + @State opacityList: number[] = []; + @State durationList: number[] = []; + @State timeoutList: number[] = []; + @State showSelectTabsComponent: boolean = false; + @State selectTabsComponentZIndex: number = -1; + @Link switchNext: boolean; + private ctx: Context = this.getUIContext().getHostContext() as Context; + private subsController: TabsController = new TabsController(); + private tabBarItemScroller: Scroller = new Scroller(); + private customContentTransition: (from: number, to: number) => TabContentAnimatedTransition = + (from: number, to: number) => { + let tabContentAnimatedTransition = { + timeout: this.timeoutList[from], + transition: (proxy: TabContentTransitionProxy) => { + this.scaleList[from] = 1.0; + this.scaleList[to] = 0.5; + this.opacityList[from] = 1.0; + this.opacityList[to] = 0.5; + this.getUIContext().animateTo({ + duration: this.durationList[from], + onFinish: () => { + proxy.finishTransition(); + } + }, () => { + this.scaleList[from] = 0.5; + this.scaleList[to] = 1.0; + this.opacityList[from] = 0.5; + this.opacityList[to] = 1.0; + }); + } + } as TabContentAnimatedTransition; + return tabContentAnimatedTransition; + }; + + subscribeSystemLanguageUpdate() { + let systemLanguage: string | undefined; + let inTabsViewModel = this.inTabsViewModel; + let selectTabsViewModel = this.selectTabsViewModel; + + let applicationContext = this.ctx.getApplicationContext(); + + let environmentCallback: EnvironmentCallback = { + async onConfigurationUpdated(newConfig: Configuration) { + if (systemLanguage !== newConfig.language) { + await inTabsViewModel.loadContentData(applicationContext); + + await selectTabsViewModel.loadTabs(applicationContext); + + systemLanguage = newConfig.language; + } + }, + onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { + // do nothing + } + }; + applicationContext.on('environment', environmentCallback); + } + + async aboutToAppear() { + await this.inTabsViewModel.loadContentData(this.ctx); + + await this.selectTabsViewModel.loadTabs(this.ctx); + + this.tabBarModifier.margin({ right: 56 }).align(Alignment.Start); + + for (let i = 1; i <= this.selectTabsViewModel.selectedTabs.length; i++) { + this.scaleList.push(1.0); + this.opacityList.push(1.0); + this.durationList.push(1000); + this.timeoutList.push(1000); + } + + this.subscribeSystemLanguageUpdate(); + } + + @Builder + tabBuilder(index: number, tab: TabItemViewModel) { + Row() { + Text(tab.name) + .fontSize(14) + .fontWeight(this.focusIndex === index ? FontWeight.Medium : FontWeight.Regular) + .fontColor(this.focusIndex === index ? Color.White : $r('app.color.in_tab_bar_text_normal_color')) + } + .justifyContent(FlexAlign.Center) + .backgroundColor(this.focusIndex === index + ? $r('app.color.in_tab_bar_background_active_color') + : $r('app.color.in_tab_bar_background_inactive_color')) + .borderRadius(20) + .height(40) + .margin({ left: 4, right: 4 }) + .padding({ left: 18, right: 18 }) + .onClick(() => { + this.focusIndex = index; + this.subsController.changeIndex(index); + this.tabBarItemScroller.scrollToIndex(index, true, ScrollAlign.CENTER); + }) + } + + @Builder + sheetBuilder() { + //select tabs to show + SelectTabsComponent({ selectTabsViewModel: this.selectTabsViewModel }) + } + + build() { + Scroll() { + Column() { + BannerComponent() + + Stack({ alignContent: Alignment.TopEnd }) { + Row() { + Image($r('app.media.more')) + .width(20) + .height(20) + .margin({ left: 10 }) + .onClick(() => { + this.showSelectTabsComponent = !this.showSelectTabsComponent; + }) + } + .margin({ top: 8, bottom: 8, right: 8 }) + .backgroundColor($r('app.color.in_tab_bar_background_inactive_color')) + .width(40) + .height(40) + .borderRadius(20) + .zIndex(1) + .bindSheet($$this.showSelectTabsComponent, this.sheetBuilder(), { + detents: [SheetSize.MEDIUM, SheetSize.MEDIUM, 500], + preferType: SheetType.BOTTOM, + title: { title: $r('app.string.bind_sheet_title') }, + onWillDismiss: (dismissSheetAction: DismissSheetAction) => { + this.selectTabsViewModel.updateSelectedTabs(); + if (this.selectTabsViewModel.selectedTabs.length > 0) { + this.subsController.changeIndex(0); + } + dismissSheetAction.dismiss(); + } + }) + + + Column() { + Tabs({ + barPosition: BarPosition.Start, + controller: this.subsController, + barModifier: this.tabBarModifier + }) { + ForEach(this.selectTabsViewModel.selectedTabs, (tab: TabItemViewModel, index: number) => { + if (index === this.selectTabsViewModel.selectedTabs.length - 1) { + TabContent() { + List({ space: 10 }) { + ForEach(this.inTabsViewModel.contentItems, (item: ContentItemViewModel, index: number) => { + ContentItemComponent({ + contentItemViewModel: item, + }) + }, (item: ContentItemViewModel, index: number) => index + '_' + JSON.stringify(item)) + } + .padding({ left: 10, right: 10 }) + .width(Constants.FULL_WIDTH) + .height(Constants.FULL_HEIGHT) + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.PARENT_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + } + .tabBar(this.tabBuilder(index, tab)) + .gesture(PanGesture(new PanGestureOptions({ direction: PanDirection.Left })).onActionStart(() => { + this.switchNext = true; + })) + // bind animation properties + .opacity(this.opacityList[index]) + .scale({ + x: this.scaleList[index], y: this.scaleList[index] + }) + } else { + TabContent() { + List({ space: 10 }) { + ForEach(this.inTabsViewModel.contentItems, (item: ContentItemViewModel, index: number) => { + ContentItemComponent({ + contentItemViewModel: item, + }) + }, (item: ContentItemViewModel, index: number) => index + '_' + JSON.stringify(item)) + } + .padding({ left: 10, right: 10 }) + .width(Constants.FULL_WIDTH) + .height(Constants.FULL_HEIGHT) + .scrollBar(BarState.Off) + .nestedScroll({ + scrollForward: NestedScrollMode.PARENT_FIRST, + scrollBackward: NestedScrollMode.SELF_FIRST + }) + } + .tabBar(this.tabBuilder(index, tab)) + // bind animation properties + .opacity(this.opacityList[index]) + .scale({ + x: this.scaleList[index], y: this.scaleList[index] + }) + } + }, (tab: TabItemViewModel, index: number) => index + '_' + JSON.stringify(tab)) + } + .customContentTransition(this.customContentTransition) + .barMode(BarMode.Scrollable) + .width(Constants.FULL_WIDTH) + .height(Constants.FULL_HEIGHT) + .scrollable(true) + .onChange((index: number) => { + this.focusIndex = index; + this.tabBarItemScroller.scrollToIndex(index, true, ScrollAlign.CENTER); + let preloadItems: number[] = []; + if (index - 1 >= 0) { + preloadItems.push(index - 1); + } + if (index + 1 < this.selectTabsViewModel.selectedTabs.length) { + preloadItems.push(index + 1); + } + this.subsController.preloadItems(preloadItems); + }) + } + .width(Constants.FULL_WIDTH) + .height(Constants.FULL_HEIGHT) + .backgroundColor(Color.White) + } + } + } + .scrollBar(BarState.Off) + .width(Constants.FULL_WIDTH) + .height(Constants.FULL_HEIGHT) + .backgroundColor(Color.White) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/OtherTabComponent.ets b/entry/src/main/ets/view/OtherTabComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..65db1fb7fc2570228cf87e2f00d1d5482d50bf4b --- /dev/null +++ b/entry/src/main/ets/view/OtherTabComponent.ets @@ -0,0 +1,27 @@ +/* + * 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 { Constants } from "../common/constant/Constants"; + +@Component +export default struct OtherTabContentComponent { + @State bgColor: ResourceColor = $r('app.color.other_tab_content_default_color'); + + build() { + Column() + .width(Constants.FULL_WIDTH) + .height(Constants.FULL_HEIGHT) + .backgroundColor(this.bgColor) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/OutTabsComponent.ets b/entry/src/main/ets/view/OutTabsComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..436e4f756f1f77950cddfbf94661b470bd3c52ca --- /dev/null +++ b/entry/src/main/ets/view/OutTabsComponent.ets @@ -0,0 +1,96 @@ +/* + * 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 { Constants } from "../common/constant/Constants"; +import InTabsComponent from "./InTabsComponent"; +import OtherTabContentComponent from "./OtherTabComponent"; + +@Component +export default struct OutTabsComponent { + @State currentIndex: number = 0; + @State @Watch('onchangeSwitchNext') switchNext: boolean = false; + private tabsController: TabsController = new TabsController(); + + onchangeSwitchNext() { + if (this.switchNext) { + this.switchNext = false; + this.tabsController.changeIndex(1); + } + } + + @Builder + tabBuilder(index: number, name: string | Resource, normalIcon: string | Resource, selectedIcon: string | Resource) { + Column() { + Image(this.currentIndex === index ? selectedIcon : normalIcon) + .width(24) + .height(24) + .objectFit(ImageFit.Contain) + + Text(name) + .margin({ top: 4 }) + .fontSize(10) + .fontColor(this.currentIndex === index + ? $r('app.color.out_tab_bar_font_active_color') + : $r('app.color.out_tab_bar_font_inactive_color')) + } + .justifyContent(FlexAlign.Center) + .height(60) + .width(Constants.FULL_WIDTH) + .margin({ bottom: 60 }) + .backgroundColor($r('app.color.out_tab_bar_background_color')) + + } + + build() { + Tabs({ + barPosition: BarPosition.End, + index: this.currentIndex, + controller: this.tabsController, + }) { + TabContent() { + InTabsComponent({ switchNext: this.switchNext }) + }.tabBar(this.tabBuilder(0, $r('app.string.out_bar_text_home'), $r('app.media.home'), $r('app.media.activeHome'))) + + TabContent() { + OtherTabContentComponent({ bgColor: Color.Blue }) + } + .tabBar(this.tabBuilder(1, $r('app.string.out_bar_text_quotes'), $r('app.media.home'), + $r('app.media.activeHome'))) + + TabContent() { + OtherTabContentComponent({ bgColor: Color.Yellow }) + } + .tabBar(this.tabBuilder(2, $r('app.string.out_bar_text_watchlist'), $r('app.media.home'), + $r('app.media.activeHome'))) + + TabContent() { + OtherTabContentComponent({ bgColor: Color.Orange }) + } + .tabBar(this.tabBuilder(3, $r('app.string.out_bar_text_trade'), $r('app.media.home'), $r('app.media.activeHome'))) + + } + .vertical(false) + .barMode(BarMode.Fixed) + .scrollable(true) + .onChange((index: number) => { + this.currentIndex = index; + }) + .height(Constants.FULL_HEIGHT) + .width(Constants.FULL_WIDTH) + .backgroundColor($r('app.color.out_tab_bar_background_color')) + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM]) + .barHeight(120) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/SearchBarComponent.ets b/entry/src/main/ets/view/SearchBarComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..ea885c53fbef8905ff8d5b55a8b80d5fa00cb7dd --- /dev/null +++ b/entry/src/main/ets/view/SearchBarComponent.ets @@ -0,0 +1,58 @@ +/* + * 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 { Constants } from "../common/constant/Constants"; + +@Component +export default struct SearchBarComponent { + @State changeValue: string = ''; + + build() { + Row() { + Stack() { + TextInput({ placeholder: $r('app.string.search_placeholder') }) + .height(40) + .width(Constants.FULL_WIDTH) + .fontSize(16) + .placeholderColor(Color.Grey) + .placeholderFont({ size: 16, weight: FontWeight.Normal }) + .borderStyle(BorderStyle.Solid) + .backgroundColor($r('app.color.search_bar_input_color')) + .padding({ left: 35, right: 66 }) + .onChange((currentContent) => { + this.changeValue = currentContent; + }) + Row() { + Image($r('app.media.ic_search')).width(20).height(20) + Button($r('app.string.search')) + .padding({ left: 20, right: 20 }) + .height(36) + .fontColor($r('app.color.search_bar_button_color')) + .fontSize(16) + .backgroundColor($r('app.color.search_bar_input_color')) + + }.width(Constants.FULL_WIDTH) + .hitTestBehavior(HitTestMode.None) + .justifyContent(FlexAlign.SpaceBetween) + .padding({ left: 10, right: 2 }) + }.alignContent(Alignment.Start) + .width(Constants.FULL_WIDTH) + } + .justifyContent(FlexAlign.SpaceBetween) + .padding(16) + .width(Constants.FULL_WIDTH) + .backgroundColor($r('app.color.out_tab_bar_background_color')) + .expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.TOP]) + } +} \ No newline at end of file diff --git a/entry/src/main/ets/view/SelectTabsComponent.ets b/entry/src/main/ets/view/SelectTabsComponent.ets new file mode 100644 index 0000000000000000000000000000000000000000..8fc3b2bd09b2ceabd31c323357b095db77b7f13a --- /dev/null +++ b/entry/src/main/ets/view/SelectTabsComponent.ets @@ -0,0 +1,62 @@ +/* + * 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 SelectTabsViewModel from "../viewmodel/SelectTabsViewModel" +import TabItemViewModel from "../viewmodel/TabItemViewModel"; + +@Component +export default struct SelectTabsComponent { + @State checkedChange: boolean = false; + @Link selectTabsViewModel: SelectTabsViewModel; + + build() { + Grid() { + ForEach(this.selectTabsViewModel.allTabs, (tab: TabItemViewModel) => { + GridItem() { + Row() { + Toggle({ type: ToggleType.Button, isOn: tab.isChecked }) { + if (this.checkedChange) { + Text(tab.name) + .fontColor(tab.isChecked ? Color.White : $r('app.color.in_tab_bar_text_normal_color')) + .fontSize(14) + } else { + Text(tab.name) + .fontColor(tab.isChecked ? Color.White : $r('app.color.in_tab_bar_text_normal_color')) + .fontSize(14) + } + } + .width($r('app.integer.in_tab_bar_width')) + .borderRadius(20) + .height(40) + .margin({ + left: 4, + right: 4, + top: 10, + bottom: 10 + }) + .padding({ left: 12, right: 12 }) + .selectedColor($r('app.color.in_tab_bar_background_active_color')) + .onChange((isOn: boolean) => { + tab.isChecked = isOn; + this.checkedChange = !this.checkedChange; + }) + } + } + }, (tab: TabItemViewModel, index: number) => index + '_' + JSON.stringify(tab)) + } + .columnsTemplate(('1fr 1fr 1fr 1fr') as string) + .height('100%') + } +} \ No newline at end of file diff --git a/entry/src/main/ets/viewmodel/ContentItemViewModel.ets b/entry/src/main/ets/viewmodel/ContentItemViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..d63d78b33c64a92f3156e1007d31e36327078386 --- /dev/null +++ b/entry/src/main/ets/viewmodel/ContentItemViewModel.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. + */ + +import ContentItemModel from "../model/ContentItemModel"; + +@Observed +export default class ContentItemViewModel { + username: string | Resource = ''; + publishTime: string | Resource = ''; + rawTitle: string | Resource = ''; + title: string | Resource = ''; + imgUrl1: string | Resource = ''; + imgUrl2: string | Resource = ''; + imgUrl3: string | Resource = ''; + imgUrl4: string | Resource = ''; + + updateContentItem(contentItemModel: ContentItemModel) { + this.username = contentItemModel.username; + this.publishTime = contentItemModel.publishTime; + this.rawTitle = contentItemModel.rawTitle; + this.title = contentItemModel.title; + this.imgUrl1 = contentItemModel.imgUrl1; + this.imgUrl2 = contentItemModel.imgUrl2; + this.imgUrl3 = contentItemModel.imgUrl3; + this.imgUrl4 = contentItemModel.imgUrl4; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/viewmodel/InTabsViewModel.ets b/entry/src/main/ets/viewmodel/InTabsViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..4cfaf3545d842f40cb026fbeb880aad3fc4abd25 --- /dev/null +++ b/entry/src/main/ets/viewmodel/InTabsViewModel.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. + */ + +import ContentItemViewModel from "./ContentItemViewModel"; +import InTabsModel from "../model/InTabsModel"; + +@Observed +class ContentItemArray extends Array { +} + +@Observed +export default class InTabsViewModel { + private inTabsModel: InTabsModel = new InTabsModel(); + contentItems: ContentItemArray = new ContentItemArray(); + + async loadContentData(ctx: Context) { + await this.inTabsModel.loadContentItems(ctx); + + let tempItems: ContentItemArray = []; + for (let item of this.inTabsModel.contentItems) { + let contentItemViewModel = new ContentItemViewModel(); + contentItemViewModel.updateContentItem(item); + tempItems.push(contentItemViewModel); + } + this.contentItems = tempItems; + } +} diff --git a/entry/src/main/ets/viewmodel/SelectTabsViewModel.ets b/entry/src/main/ets/viewmodel/SelectTabsViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..9fa3064c976116ffb272e2ec9ecbfe4039209e84 --- /dev/null +++ b/entry/src/main/ets/viewmodel/SelectTabsViewModel.ets @@ -0,0 +1,52 @@ +/* + * 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 TabItemViewModel from "./TabItemViewModel"; +import SelectTabsModel from "../model/SelectTabsModel"; + +@Observed +class TabItemArray extends Array { +} + +@Observed +export default class SelectTabsViewModel { + allTabs: TabItemArray = new TabItemArray(); + selectedTabs: TabItemArray = new TabItemArray(); + private selectTabsModel: SelectTabsModel = new SelectTabsModel(); + + async loadTabs(ctx: Context) { + await this.selectTabsModel.loadAllTabs(ctx); + + let tempTabs: TabItemViewModel[] = []; + for (let tab of this.selectTabsModel.allTabs) { + let tabItemViewModel = new TabItemViewModel(); + tabItemViewModel.updateTab(tab); + tempTabs.push(tabItemViewModel); + } + this.allTabs = tempTabs; + + this.updateSelectedTabs(); + } + + updateSelectedTabs() { + let tempTabs: TabItemViewModel[] = []; + for (let tab of this.allTabs) { + if (tab.isChecked) { + tempTabs.push(tab); + } + } + this.selectedTabs = tempTabs; + } +} \ No newline at end of file diff --git a/entry/src/main/ets/viewmodel/TabItemViewModel.ets b/entry/src/main/ets/viewmodel/TabItemViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..0aacf87d31483a3d9c9149ed0ca7613a8d64b47e --- /dev/null +++ b/entry/src/main/ets/viewmodel/TabItemViewModel.ets @@ -0,0 +1,29 @@ +/* + * 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 TabItemModel from "../model/TabItemModel"; + +@Observed +export default class TabItemViewModel { + id: number = 0; + name: string | Resource = ''; + isChecked: boolean = true; + + updateTab(tabItemModel: TabItemModel) { + this.id = tabItemModel.id; + this.name = tabItemModel.name; + this.isChecked = tabItemModel.isChecked; + } +} \ 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..045c6883f7e00a3fcf60dd3a0fcb5558f5e5baa2 --- /dev/null +++ b/entry/src/main/module.json5 @@ -0,0 +1,57 @@ +{ + "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, + "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.INTERNET" + } + ] + } +} \ 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..c6cbbecc37eafd07bd11cae0a2f9d151ed7e4080 --- /dev/null +++ b/entry/src/main/resources/base/element/color.json @@ -0,0 +1,49 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + }, + { + "name": "content_item_text_color", + "value": "#FFCAC8C8" + }, + { + "name": "in_tab_bar_text_normal_color", + "value": "#E6000000" + }, + { + "name": "in_tab_bar_background_active_color", + "value": "#0A59F7" + }, + { + "name": "in_tab_bar_background_inactive_color", + "value": "#F2F2F2" + }, + { + "name": "out_tab_bar_font_active_color", + "value": "#3388FF" + }, + { + "name": "out_tab_bar_font_inactive_color", + "value": "#E6000000" + }, + { + "name": "other_tab_content_default_color", + "value": "#E67C92" + }, + { + "name": "search_bar_input_color", + "value": "#E6E8E9" + }, + { + "name": "search_bar_button_color", + "value": "#0A59F7" + }, + { + "name": "out_tab_bar_background_color", + "value": "#F1F3F5" + } + + ] +} \ 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/integer.json b/entry/src/main/resources/base/element/integer.json new file mode 100644 index 0000000000000000000000000000000000000000..a7f12a80b7992ea2e4d3088df88068a14fbdcc15 --- /dev/null +++ b/entry/src/main/resources/base/element/integer.json @@ -0,0 +1,15 @@ +{ + "integer": [ + { + "name": "in_tab_bar_width", + "value": 64 + }, + { + "name": "select_tabs_component_width", + "value": 280 + }, + {"name": "search_bar_button_width", + "value": 67 + } + ] +} \ No newline at end of file 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..00a27f596ff00bf7734a7662910abd843f49e9d3 --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "label" + }, + { + "name": "search_placeholder", + "value": "请输入搜索内容" + }, + { + "name": "search", + "value": "搜索" + }, + { + "name":"out_bar_text_home", + "value": "首页" + }, + { + "name":"out_bar_text_quotes", + "value": "行情" + }, + { + "name":"out_bar_text_watchlist", + "value": "自选" + }, + { + "name":"out_bar_text_trade", + "value": "交易" + }, + { + "name": "default_all_tabs_file", + "value": "default_all_tabs.json" + }, + { + "name": "default_content_items_file", + "value": "default_content_items.json" + }, + { + "name":"bind_sheet_title", + "value": "选择页签" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/media/activeHome.svg b/entry/src/main/resources/base/media/activeHome.svg new file mode 100644 index 0000000000000000000000000000000000000000..e01e6b346c171e8b89cb56094d674a0c885cc807 --- /dev/null +++ b/entry/src/main/resources/base/media/activeHome.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/entry/src/main/resources/base/media/avatar.jpg b/entry/src/main/resources/base/media/avatar.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff310f1df9f218959bc35827feb86b32c55ee259 Binary files /dev/null and b/entry/src/main/resources/base/media/avatar.jpg 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/content.jpg b/entry/src/main/resources/base/media/content.jpg new file mode 100644 index 0000000000000000000000000000000000000000..fde288507361351a9450ec78eadf1454c9533f98 Binary files /dev/null and b/entry/src/main/resources/base/media/content.jpg 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/home.svg b/entry/src/main/resources/base/media/home.svg new file mode 100644 index 0000000000000000000000000000000000000000..2f00512afca0df8ea106166eafd26325215c1335 --- /dev/null +++ b/entry/src/main/resources/base/media/home.svg @@ -0,0 +1,14 @@ + + + Created with Pixso. + + + + + + + + + + + diff --git a/entry/src/main/resources/base/media/ic_search.png b/entry/src/main/resources/base/media/ic_search.png new file mode 100644 index 0000000000000000000000000000000000000000..8abc20b7292bc44398c15f5fb137db44a00cfb53 Binary files /dev/null and b/entry/src/main/resources/base/media/ic_search.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/more.svg b/entry/src/main/resources/base/media/more.svg new file mode 100644 index 0000000000000000000000000000000000000000..1d60dfcaf07a9cd20872241806365641eb464195 --- /dev/null +++ b/entry/src/main/resources/base/media/more.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/pic5.jpg b/entry/src/main/resources/base/media/pic5.jpg new file mode 100644 index 0000000000000000000000000000000000000000..77dc10a304b04460668f1d23ab3b5d7da9e0657a Binary files /dev/null and b/entry/src/main/resources/base/media/pic5.jpg differ 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/integer.json b/entry/src/main/resources/en_US/element/integer.json new file mode 100644 index 0000000000000000000000000000000000000000..a095a64891e945920e710a21e8840979c1f76839 --- /dev/null +++ b/entry/src/main/resources/en_US/element/integer.json @@ -0,0 +1,16 @@ +{ + "integer": [ + { + "name": "in_tab_bar_width", + "value": 84 + }, + { + "name": "select_tabs_component_width", + "value": 320 + }, + { + "name": "search_bar_button_width", + "value": 90 + } + ] +} \ 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..51f4d4e9a3cc9aaf484dc96d90ef99c3ec5415bd --- /dev/null +++ b/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,40 @@ +{ + "string": [ + { + "name": "default_all_tabs_file", + "value": "default_all_tabs_en.json" + }, + { + "name": "default_content_items_file", + "value": "default_content_items_en.json" + }, + { + "name": "search_placeholder", + "value": "Enter search content" + }, + { + "name": "search", + "value": "Search" + }, + { + "name":"out_bar_text_home", + "value": "Home" + }, + { + "name":"out_bar_text_quotes", + "value": "Quotes" + }, + { + "name":"out_bar_text_watchlist", + "value": "Watchlist" + }, + { + "name":"out_bar_text_trade", + "value": "Trade" + }, + { + "name":"bind_sheet_title", + "value": "Select Tab" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/default_all_tabs.json b/entry/src/main/resources/rawfile/default_all_tabs.json new file mode 100644 index 0000000000000000000000000000000000000000..934160599b8a46233dfd9aedbf60d13635686c6f --- /dev/null +++ b/entry/src/main/resources/rawfile/default_all_tabs.json @@ -0,0 +1,52 @@ +[ + { + "id": 0, + "name": "关注", + "isChecked": true + }, + { + "id": 1, + "name": "推荐", + "isChecked": true + }, + { + "id": 2, + "name": "热度", + "isChecked": true + }, + { + "id": 3, + "name": "视频", + "isChecked": true + }, + { + "id": 4, + "name": "体育", + "isChecked": true + }, + { + "id": 5, + "name": "财经", + "isChecked": true + }, + { + "id": 6, + "name": "投资", + "isChecked": true + }, + { + "id": 7, + "name": "电影", + "isChecked": true + }, + { + "id": 8, + "name": "音乐", + "isChecked": true + }, + { + "id": 9, + "name": "明星", + "isChecked": false + } +] \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/default_all_tabs_en.json b/entry/src/main/resources/rawfile/default_all_tabs_en.json new file mode 100644 index 0000000000000000000000000000000000000000..a9fefc8e3f4b6826df02f6562bb19eac577a63b9 --- /dev/null +++ b/entry/src/main/resources/rawfile/default_all_tabs_en.json @@ -0,0 +1,52 @@ +[ + { + "id": 0, + "name": "Follows", + "isChecked": true + }, + { + "id": 1, + "name": "For You", + "isChecked": true + }, + { + "id": 2, + "name": "Popular", + "isChecked": true + }, + { + "id": 3, + "name": "Video", + "isChecked": true + }, + { + "id": 4, + "name": "Sports", + "isChecked": true + }, + { + "id": 5, + "name": "Finance", + "isChecked": true + }, + { + "id": 6, + "name": "Investing", + "isChecked": true + }, + { + "id": 7, + "name": "Movies", + "isChecked": true + }, + { + "id": 8, + "name": "Music", + "isChecked": true + }, + { + "id": 9, + "name": "Stars", + "isChecked": false + } +] \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/default_content_items.json b/entry/src/main/resources/rawfile/default_content_items.json new file mode 100644 index 0000000000000000000000000000000000000000..1283807d562b679f20c73109d94b60ddd8170de8 --- /dev/null +++ b/entry/src/main/resources/rawfile/default_content_items.json @@ -0,0 +1,42 @@ +[ + { + "username": "皮特", + "publishTime": "3-17 15:00", + "rawTitle": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "title": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + }, + { + "username": "皮特", + "publishTime": "3-17 15:00", + "rawTitle": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "title": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + }, + { + "username": "皮特", + "publishTime": "3-17 15:00", + "rawTitle": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "title": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + }, + { + "username": "皮特", + "publishTime": "3-17 15:00", + "rawTitle": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "title": "春天啦~周末两天天气都不错\n 大家有没有出门去看花?!", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + } +] \ No newline at end of file diff --git a/entry/src/main/resources/rawfile/default_content_items_en.json b/entry/src/main/resources/rawfile/default_content_items_en.json new file mode 100644 index 0000000000000000000000000000000000000000..bc6453e32b28923d8df846856adcb0d90eba3784 --- /dev/null +++ b/entry/src/main/resources/rawfile/default_content_items_en.json @@ -0,0 +1,42 @@ +[ + { + "username": "Peter", + "publishTime": "3-17 15:00", + "rawTitle": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "title": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + }, + { + "username": "Peter", + "publishTime": "3-17 15:00", + "rawTitle": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "title": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + }, + { + "username": "Peter", + "publishTime": "3-17 15:00", + "rawTitle": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "title": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + }, + { + "username": "Peter", + "publishTime": "3-17 15:00", + "rawTitle": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "title": "Spring vibes! Perfect weekend for flower adventures - did you go? ", + "imgUrl1": "app.media.avatar", + "imgUrl2": "app.media.content", + "imgUrl3": "app.media.content", + "imgUrl4": "app.media.content" + } +] \ No newline at end of file diff --git a/entry/src/main/resources/zh_CN/element/integer.json b/entry/src/main/resources/zh_CN/element/integer.json new file mode 100644 index 0000000000000000000000000000000000000000..97edae3fd77bb7e45f7794868f41764c0c59ba06 --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/integer.json @@ -0,0 +1,16 @@ +{ + "integer": [ + { + "name": "in_tab_bar_width", + "value": 64 + }, + { + "name": "select_tabs_component_width", + "value": 280 + }, + { + "name": "search_bar_button_width", + "value": 67 + } + ] +} \ 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..13435134eedfd43d1cf8dabd1191f211786f7ef4 --- /dev/null +++ b/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,40 @@ +{ + "string": [ + { + "name": "default_all_tabs_file", + "value": "default_all_tabs.json" + }, + { + "name": "default_content_items_file", + "value": "default_content_items.json" + }, + { + "name": "search_placeholder", + "value": "请输入搜索内容" + }, + { + "name": "search", + "value": "搜索" + }, + { + "name":"out_bar_text_home", + "value": "首页" + }, + { + "name":"out_bar_text_quotes", + "value": "行情" + }, + { + "name":"out_bar_text_watchlist", + "value": "自选" + }, + { + "name":"out_bar_text_trade", + "value": "交易" + }, + { + "name":"bind_sheet_title", + "value": "选择页签" + } + ] +} \ No newline at end of file diff --git a/entry/src/mock/mock-config.json5 b/entry/src/mock/mock-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..7a73a41bfdf76d6f793007240d80983a52f15f97 --- /dev/null +++ b/entry/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/Ability.test.ets b/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..85c78f67579d6e31b5f5aeea463e216b9b141048 --- /dev/null +++ b/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,35 @@ +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }) + }) +} \ No newline at end of file diff --git a/entry/src/ohosTest/ets/test/List.test.ets b/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..794c7dc4ed66bd98fa3865e07922906e2fcef545 --- /dev/null +++ b/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,5 @@ +import abilityTest from './Ability.test'; + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/entry/src/ohosTest/module.json5 b/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..55725a929993a8a18b3808d41ef037759440488b --- /dev/null +++ b/entry/src/ohosTest/module.json5 @@ -0,0 +1,13 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "phone", + "tablet", + "2in1" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/entry/src/test/List.test.ets b/entry/src/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..bb5b5c3731e283dd507c847560ee59bde477bbc7 --- /dev/null +++ b/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/entry/src/test/LocalUnit.test.ets b/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..165fc1615ee8618b4cb6a622f144a9a707eee99f --- /dev/null +++ b/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..5bebc9755447385d82ce4138f54d991b1f85f348 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.5", + "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/oh-package-lock.json5 b/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..7fcf818273347b97063c0c0a151bb14770ca1c79 --- /dev/null +++ b/oh-package-lock.json5 @@ -0,0 +1,27 @@ +{ + "meta": { + "stableOrder": true + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", + "@ohos/hypium@1.0.21": "@ohos/hypium@1.0.21" + }, + "packages": { + "@ohos/hamock@1.0.0": { + "name": "@ohos/hamock", + "version": "1.0.0", + "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", + "registryType": "ohpm" + }, + "@ohos/hypium@1.0.21": { + "name": "@ohos/hypium", + "version": "1.0.21", + "integrity": "sha512-iyKGMXxE+9PpCkqEwu0VykN/7hNpb+QOeIuHwkmZnxOpI+dFZt6yhPB7k89EgV1MiSK/ieV/hMjr5Z2mWwRfMQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.21.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..a8aff0c5aff22d78aa26fd19c3861f4320e951ff --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "5.0.5", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.21", + "@ohos/hamock": "1.0.0" + } +} diff --git a/screenshots/device/tab0.gif b/screenshots/device/tab0.gif new file mode 100644 index 0000000000000000000000000000000000000000..d87b2f5414b4f87d5e9ac9ad60f4653010132d45 Binary files /dev/null and b/screenshots/device/tab0.gif differ diff --git a/screenshots/device/tab1.gif b/screenshots/device/tab1.gif new file mode 100644 index 0000000000000000000000000000000000000000..afb7353201c85a3a3a0cf743ca24090a8341e89b Binary files /dev/null and b/screenshots/device/tab1.gif differ diff --git a/screenshots/device/tab2.gif b/screenshots/device/tab2.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a5505ca23218869d458364dd4623d4695ae347d Binary files /dev/null and b/screenshots/device/tab2.gif differ diff --git a/screenshots/device/tab3.gif b/screenshots/device/tab3.gif new file mode 100644 index 0000000000000000000000000000000000000000..a0df9966b6f386dc97ccdc2bb86927a08796a13a Binary files /dev/null and b/screenshots/device/tab3.gif differ diff --git a/screenshots/device/tab4.gif b/screenshots/device/tab4.gif new file mode 100644 index 0000000000000000000000000000000000000000..0a2e5ea452fba9a7aff7105372d0a43c2ddb460c Binary files /dev/null and b/screenshots/device/tab4.gif differ diff --git a/screenshots/device/tab5.gif b/screenshots/device/tab5.gif new file mode 100644 index 0000000000000000000000000000000000000000..8747c7c833244041c8918c759c95660bdf675dc2 Binary files /dev/null and b/screenshots/device/tab5.gif differ diff --git a/screenshots/device/tab_en_0.gif b/screenshots/device/tab_en_0.gif new file mode 100644 index 0000000000000000000000000000000000000000..f85905cbd5101565f023361061a67621451892cd Binary files /dev/null and b/screenshots/device/tab_en_0.gif differ diff --git a/screenshots/device/tab_en_1.gif b/screenshots/device/tab_en_1.gif new file mode 100644 index 0000000000000000000000000000000000000000..4d59b5d1e2f63838bb0d328a96bc72e9cc0a866c Binary files /dev/null and b/screenshots/device/tab_en_1.gif differ diff --git a/screenshots/device/tab_en_2.gif b/screenshots/device/tab_en_2.gif new file mode 100644 index 0000000000000000000000000000000000000000..194281dc42b2aa3527a7c37b7f1ef789976d7b36 Binary files /dev/null and b/screenshots/device/tab_en_2.gif differ diff --git a/screenshots/device/tab_en_3.gif b/screenshots/device/tab_en_3.gif new file mode 100644 index 0000000000000000000000000000000000000000..a2b8dd2a1c898f52ece44426404040a86bd063f3 Binary files /dev/null and b/screenshots/device/tab_en_3.gif differ diff --git a/screenshots/device/tab_en_4.gif b/screenshots/device/tab_en_4.gif new file mode 100644 index 0000000000000000000000000000000000000000..2b8bdbf4918363e262da3d9c1585d7416f503cec Binary files /dev/null and b/screenshots/device/tab_en_4.gif differ diff --git a/screenshots/device/tab_en_5.gif b/screenshots/device/tab_en_5.gif new file mode 100644 index 0000000000000000000000000000000000000000..fd8e401540771fecc2a1e873ce84d68ab7ea3e2a Binary files /dev/null and b/screenshots/device/tab_en_5.gif differ