├── entry ├── src │ ├── mock │ │ └── mock-config.json5 │ ├── main │ │ ├── resources │ │ │ ├── base │ │ │ │ ├── profile │ │ │ │ │ ├── backup_config.json │ │ │ │ │ └── main_pages.json │ │ │ │ ├── media │ │ │ │ │ ├── startIcon.png │ │ │ │ │ ├── background.png │ │ │ │ │ ├── foreground.png │ │ │ │ │ └── layered_image.json │ │ │ │ └── element │ │ │ │ │ ├── float.json │ │ │ │ │ ├── color.json │ │ │ │ │ └── string.json │ │ │ ├── dark │ │ │ │ └── element │ │ │ │ │ └── color.json │ │ │ └── zh_CN │ │ │ │ └── element │ │ │ │ └── string.json │ │ ├── ets │ │ │ ├── components │ │ │ │ ├── LinysText.ets │ │ │ │ ├── LinysButton.ets │ │ │ │ └── GridSpace.ets │ │ │ ├── objects │ │ │ │ ├── DisboardLayoutObject.ets │ │ │ │ └── FileObject.ets │ │ │ ├── hosts │ │ │ │ └── defaults.ets │ │ │ ├── entrybackupability │ │ │ │ └── EntryBackupAbility.ets │ │ │ ├── utils │ │ │ │ ├── clipboardTools.ets │ │ │ │ └── storageTools.ets │ │ │ ├── workers │ │ │ │ └── Scanner.ets │ │ │ ├── entryability │ │ │ │ └── EntryAbility.ets │ │ │ └── pages │ │ │ │ └── Index.ets │ │ └── module.json5 │ ├── test │ │ ├── List.test.ets │ │ └── LocalUnit.test.ets │ └── ohosTest │ │ ├── ets │ │ └── test │ │ │ ├── List.test.ets │ │ │ └── Ability.test.ets │ │ └── module.json5 ├── .gitignore ├── oh-package.json5 ├── hvigorfile.ts ├── build-profile.json5 └── obfuscation-rules.txt ├── .gitattributes ├── previews ├── Preview-1.jpg └── Preview-2.jpg ├── AppScope ├── resources │ └── base │ │ ├── media │ │ ├── background.png │ │ ├── foreground.png │ │ └── layered_image.json │ │ └── element │ │ └── string.json └── app.json5 ├── .gitignore ├── oh-package.json5 ├── hvigorfile.ts ├── README.md ├── code-linter.json5 ├── oh-package-lock.json5 ├── LICENSE └── hvigor └── hvigor-config.json5 /entry/src/mock/mock-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /entry/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /.preview 4 | /build 5 | /.cxx 6 | /.test -------------------------------------------------------------------------------- /entry/src/main/resources/base/profile/backup_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "allowToBackupRestore": true 3 | } -------------------------------------------------------------------------------- /previews/Preview-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/previews/Preview-1.jpg -------------------------------------------------------------------------------- /previews/Preview-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/previews/Preview-2.jpg -------------------------------------------------------------------------------- /entry/src/main/resources/base/profile/main_pages.json: -------------------------------------------------------------------------------- 1 | { 2 | "src": [ 3 | "pages/Index" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /AppScope/resources/base/media/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/AppScope/resources/base/media/background.png -------------------------------------------------------------------------------- /AppScope/resources/base/media/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/AppScope/resources/base/media/foreground.png -------------------------------------------------------------------------------- /entry/src/test/List.test.ets: -------------------------------------------------------------------------------- 1 | import localUnitTest from './LocalUnit.test'; 2 | 3 | export default function testsuite() { 4 | localUnitTest(); 5 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/startIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/entry/src/main/resources/base/media/startIcon.png -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/test/List.test.ets: -------------------------------------------------------------------------------- 1 | import abilityTest from './Ability.test'; 2 | 3 | export default function testsuite() { 4 | abilityTest(); 5 | } -------------------------------------------------------------------------------- /AppScope/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "app_name", 5 | "value": "Spaceow" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/entry/src/main/resources/base/media/background.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awaLiny2333/Spaceow_NEXT/HEAD/entry/src/main/resources/base/media/foreground.png -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/float.json: -------------------------------------------------------------------------------- 1 | { 2 | "float": [ 3 | { 4 | "name": "page_text_font_size", 5 | "value": "50fp" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /AppScope/resources/base/media/layered_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "layered-image": 3 | { 4 | "background" : "$media:background", 5 | "foreground" : "$media:foreground" 6 | } 7 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/media/layered_image.json: -------------------------------------------------------------------------------- 1 | { 2 | "layered-image": 3 | { 4 | "background" : "$media:background", 5 | "foreground" : "$media:foreground" 6 | } 7 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /oh_modules 3 | /local.properties 4 | /.idea 5 | **/build 6 | /.hvigor 7 | .cxx 8 | /.clangd 9 | /.clang-format 10 | /.clang-tidy 11 | **/.test 12 | /.appanalyzer -------------------------------------------------------------------------------- /entry/oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "name": "entry", 3 | "version": "1.0.0", 4 | "description": "Please describe the basic information.", 5 | "main": "", 6 | "author": "", 7 | "license": "", 8 | "dependencies": {} 9 | } 10 | 11 | -------------------------------------------------------------------------------- /entry/src/main/ets/components/LinysText.ets: -------------------------------------------------------------------------------- 1 | @Component 2 | export struct LinysText { 3 | @Prop text: string = ''; 4 | build() { 5 | Text(this.text) 6 | .fontColor($r('app.color.accent')) 7 | .fontWeight(FontWeight.Normal) 8 | } 9 | } -------------------------------------------------------------------------------- /oh-package.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.5", 3 | "description": "Please describe the basic information.", 4 | "dependencies": { 5 | }, 6 | "devDependencies": { 7 | "@ohos/hypium": "1.0.21", 8 | "@ohos/hamock": "1.0.0" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { appTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 6 | } 7 | -------------------------------------------------------------------------------- /entry/hvigorfile.ts: -------------------------------------------------------------------------------- 1 | import { hapTasks } from '@ohos/hvigor-ohos-plugin'; 2 | 3 | export default { 4 | system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ 5 | plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ 6 | } 7 | -------------------------------------------------------------------------------- /AppScope/app.json5: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "bundleName": "com.next.liny.spaceow", 4 | "vendor": "Liny with Cats&Love!", 5 | "versionCode": 2, 6 | "versionName": "0.0.2", 7 | "icon": "$media:layered_image", 8 | "label": "$string:app_name" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spaceow 2 | Space analysis tool on HarmonyOS / OpenHarmony! 3 | 4 | ## Previews 5 | Preview builds are available at [build_auto](build_auto). 6 | 7 | Preview screenshots: 8 | 9 | ![Preview.jpg](previews/Preview-1.jpg) 10 | 11 | ![Preview-2.jpg](previews/Preview-2.jpg) -------------------------------------------------------------------------------- /entry/src/ohosTest/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "entry_test", 4 | "type": "feature", 5 | "deviceTypes": [ 6 | "phone", 7 | "tablet", 8 | "2in1" 9 | ], 10 | "deliveryWithInstall": true, 11 | "installationFree": false 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FFFFFF" 6 | }, 7 | { 8 | "name": "supplementary", 9 | "value": "#FFCDD1D7" 10 | }, 11 | { 12 | "name": "accent", 13 | "value": "#FF11182F" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/main/resources/dark/element/color.json: -------------------------------------------------------------------------------- 1 | { 2 | "color": [ 3 | { 4 | "name": "start_window_background", 5 | "value": "#FF11182F" 6 | }, 7 | { 8 | "name": "supplementary", 9 | "value": "#FF29365A" 10 | }, 11 | { 12 | "name": "accent", 13 | "value": "#FFCDD1D7" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /entry/src/main/ets/components/LinysButton.ets: -------------------------------------------------------------------------------- 1 | import { animationDefault } from '../hosts/defaults'; 2 | 3 | @Component 4 | export default struct LinysButton { 5 | @Prop text: ResourceStr = ''; 6 | 7 | build() { 8 | Button(this.text) 9 | .fontColor($r('app.color.accent')) 10 | .fontWeight(FontWeight.Bold) 11 | .backgroundColor($r('app.color.supplementary')) 12 | .animation(animationDefault()) 13 | } 14 | } -------------------------------------------------------------------------------- /entry/src/main/ets/objects/DisboardLayoutObject.ets: -------------------------------------------------------------------------------- 1 | import { FileObject } from './FileObject'; 2 | 3 | export class DisboardLayoutObject { 4 | fileObject: FileObject; 5 | sideLength: number; 6 | row: number; 7 | col: number; 8 | 9 | constructor(fo: FileObject, sideLength: number, row?: number, col?: number) { 10 | this.fileObject = fo; 11 | this.sideLength = sideLength; 12 | this.row = row || 0; 13 | this.col = col || 0; 14 | } 15 | } -------------------------------------------------------------------------------- /entry/src/main/ets/hosts/defaults.ets: -------------------------------------------------------------------------------- 1 | import Curves from '@ohos.curves'; 2 | 3 | /** 4 | * Default animation params. 5 | * @returns An AnimateParam, reading the arguments from app settings. 6 | * */ 7 | export function animationDefault() { 8 | let ap: AnimateParam = { curve: Curves.springMotion(0.36, 0.8) }; 9 | return ap; 10 | } 11 | 12 | /** 13 | * Default Click effect. 14 | * @returns ClickEffect = { level: ClickEffectLevel.LIGHT } 15 | * */ 16 | export function clickEffectDefault() { 17 | let ce: ClickEffect = { level: ClickEffectLevel.LIGHT }; 18 | return ce; 19 | } -------------------------------------------------------------------------------- /entry/src/main/ets/entrybackupability/EntryBackupAbility.ets: -------------------------------------------------------------------------------- 1 | import { hilog } from '@kit.PerformanceAnalysisKit'; 2 | import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; 3 | 4 | const DOMAIN = 0x0000; 5 | 6 | export default class EntryBackupAbility extends BackupExtensionAbility { 7 | async onBackup() { 8 | hilog.info(DOMAIN, 'testTag', 'onBackup ok'); 9 | await Promise.resolve(); 10 | } 11 | 12 | async onRestore(bundleVersion: BundleVersion) { 13 | hilog.info(DOMAIN, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); 14 | await Promise.resolve(); 15 | } 16 | } -------------------------------------------------------------------------------- /entry/build-profile.json5: -------------------------------------------------------------------------------- 1 | { 2 | "apiType": "stageMode", 3 | "buildOption": { 4 | "sourceOption": { 5 | "workers": [ 6 | './src/main/ets/workers/Scanner.ets' 7 | ] 8 | } 9 | }, 10 | "buildOptionSet": [ 11 | { 12 | "name": "release", 13 | "arkOptions": { 14 | "obfuscation": { 15 | "ruleOptions": { 16 | "enable": false, 17 | "files": [ 18 | "./obfuscation-rules.txt" 19 | ] 20 | } 21 | } 22 | } 23 | }, 24 | ], 25 | "targets": [ 26 | { 27 | "name": "default" 28 | }, 29 | { 30 | "name": "ohosTest", 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /entry/src/main/resources/zh_CN/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "喵喵主模块!" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "扫描磁盘!" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "盘扫喵" 14 | }, 15 | { 16 | "name": "main_back", 17 | "value": "上一级 ↑" 18 | }, 19 | { 20 | "name": "main_select", 21 | "value": "选择 \uDB80\uDCBA" 22 | }, 23 | { 24 | "name": "main_go_to_directory", 25 | "value": "前往文件夹 \uDB83\uDC70" 26 | }, 27 | { 28 | "name": "main_open_as_new", 29 | "value": "在新窗口中打开 \uDB80\uDCCD" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /entry/src/main/resources/base/element/string.json: -------------------------------------------------------------------------------- 1 | { 2 | "string": [ 3 | { 4 | "name": "module_desc", 5 | "value": "Meow main module!" 6 | }, 7 | { 8 | "name": "EntryAbility_desc", 9 | "value": "To scan disk!" 10 | }, 11 | { 12 | "name": "EntryAbility_label", 13 | "value": "Spaceow" 14 | }, 15 | { 16 | "name": "main_back", 17 | "value": "Back ↑" 18 | }, 19 | { 20 | "name": "main_select", 21 | "value": "Select \uDB80\uDCBA" 22 | }, 23 | { 24 | "name": "main_go_to_directory", 25 | "value": "Go to Directory \uDB83\uDC70" 26 | }, 27 | { 28 | "name": "main_open_as_new", 29 | "value": "Open as New \uDB80\uDCCD" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /entry/src/main/ets/utils/clipboardTools.ets: -------------------------------------------------------------------------------- 1 | import { BusinessError, pasteboard } from '@kit.BasicServicesKit'; 2 | 3 | export function copy(content: string) { 4 | let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); 5 | let pasteData: pasteboard.PasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, content); 6 | 7 | systemPasteboard.setData(pasteData).then(() => { 8 | console.info('Succeeded in setting PasteData. Copied \"' + content + "\""); 9 | }).catch((err: BusinessError) => { 10 | console.error('Failed to set PasteData. Cause: ' + err.message); 11 | }); 12 | } 13 | 14 | // export async function read_first_of_board() { 15 | // let systemPasteboard: pasteboard.SystemPasteboard = pasteboard.getSystemPasteboard(); 16 | // let text: string = (await systemPasteboard.getData()).getPrimaryText(); 17 | // return text; 18 | // } -------------------------------------------------------------------------------- /code-linter.json5: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "**/*.ets" 4 | ], 5 | "ignore": [ 6 | "**/src/ohosTest/**/*", 7 | "**/src/test/**/*", 8 | "**/src/mock/**/*", 9 | "**/node_modules/**/*", 10 | "**/oh_modules/**/*", 11 | "**/build/**/*", 12 | "**/.preview/**/*" 13 | ], 14 | "ruleSet": [ 15 | "plugin:@performance/recommended", 16 | "plugin:@typescript-eslint/recommended" 17 | ], 18 | "rules": { 19 | "@security/no-unsafe-aes": "error", 20 | "@security/no-unsafe-hash": "error", 21 | "@security/no-unsafe-mac": "warn", 22 | "@security/no-unsafe-dh": "error", 23 | "@security/no-unsafe-dsa": "error", 24 | "@security/no-unsafe-ecdsa": "error", 25 | "@security/no-unsafe-rsa-encrypt": "error", 26 | "@security/no-unsafe-rsa-sign": "error", 27 | "@security/no-unsafe-rsa-key": "error", 28 | "@security/no-unsafe-dsa-key": "error", 29 | "@security/no-unsafe-dh-key": "error", 30 | "@security/no-unsafe-3des": "error" 31 | } 32 | } -------------------------------------------------------------------------------- /oh-package-lock.json5: -------------------------------------------------------------------------------- 1 | { 2 | "meta": { 3 | "stableOrder": true, 4 | "enableUnifiedLockfile": false 5 | }, 6 | "lockfileVersion": 3, 7 | "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", 8 | "specifiers": { 9 | "@ohos/hamock@1.0.0": "@ohos/hamock@1.0.0", 10 | "@ohos/hypium@1.0.21": "@ohos/hypium@1.0.21" 11 | }, 12 | "packages": { 13 | "@ohos/hamock@1.0.0": { 14 | "name": "@ohos/hamock", 15 | "version": "1.0.0", 16 | "integrity": "sha512-K6lDPYc6VkKe6ZBNQa9aoG+ZZMiwqfcR/7yAVFSUGIuOAhPvCJAo9+t1fZnpe0dBRBPxj2bxPPbKh69VuyAtDg==", 17 | "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hamock/-/hamock-1.0.0.har", 18 | "registryType": "ohpm" 19 | }, 20 | "@ohos/hypium@1.0.21": { 21 | "name": "@ohos/hypium", 22 | "version": "1.0.21", 23 | "integrity": "sha512-iyKGMXxE+9PpCkqEwu0VykN/7hNpb+QOeIuHwkmZnxOpI+dFZt6yhPB7k89EgV1MiSK/ieV/hMjr5Z2mWwRfMQ==", 24 | "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.21.har", 25 | "registryType": "ohpm" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /entry/obfuscation-rules.txt: -------------------------------------------------------------------------------- 1 | # Define project specific obfuscation rules here. 2 | # You can include the obfuscation configuration files in the current module's build-profile.json5. 3 | # 4 | # For more details, see 5 | # https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 6 | 7 | # Obfuscation options: 8 | # -disable-obfuscation: disable all obfuscations 9 | # -enable-property-obfuscation: obfuscate the property names 10 | # -enable-toplevel-obfuscation: obfuscate the names in the global scope 11 | # -compact: remove unnecessary blank spaces and all line feeds 12 | # -remove-log: remove all console.* statements 13 | # -print-namecache: print the name cache that contains the mapping from the old names to new names 14 | # -apply-namecache: reuse the given cache file 15 | 16 | # Keep options: 17 | # -keep-property-name: specifies property names that you want to keep 18 | # -keep-global-name: specifies names that you want to keep in the global scope 19 | 20 | -enable-property-obfuscation 21 | -enable-toplevel-obfuscation 22 | -enable-filename-obfuscation 23 | -enable-export-obfuscation -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 awa Liny 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /entry/src/main/ets/utils/storageTools.ets: -------------------------------------------------------------------------------- 1 | import { fileIo, picker } from "@kit.CoreFileKit"; 2 | 3 | export async function selectDirectory() { 4 | let DocumentSelectOptions = new picker.DocumentSelectOptions(); 5 | DocumentSelectOptions.selectMode = picker.DocumentSelectMode.FOLDER; 6 | let documentPicker = new picker.DocumentViewPicker(); 7 | try { 8 | let uris = await documentPicker.select(DocumentSelectOptions); 9 | let path = fileIo.openSync(uris[0]).path; 10 | return path; 11 | } catch (e) { 12 | console.error(e); 13 | return ''; 14 | } 15 | } 16 | 17 | 18 | /** 19 | * Adds a unit to a size in bytes. 20 | * @param size_in_bytes A number or bigint, the size in bytes. 21 | * @returns A string, the connected result of a number of size and its unit. 22 | * */ 23 | export function addSizeUnits(size_in_bytes: number | bigint) { 24 | let units = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] 25 | let result = BigInt(size_in_bytes); 26 | let unit = 0; 27 | while (result > 1000 * 1000 && unit < units.length - 2) { 28 | result /= 1000n; 29 | unit += 1; 30 | } 31 | // To keep decimal places 32 | let numberResult = Number(result); 33 | numberResult /= 1000; 34 | unit += 1; 35 | return numberResult.toFixed(2) + " " + units[unit]; 36 | } -------------------------------------------------------------------------------- /hvigor/hvigor-config.json5: -------------------------------------------------------------------------------- 1 | { 2 | "modelVersion": "5.0.5", 3 | "dependencies": { 4 | }, 5 | "execution": { 6 | // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ 7 | // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ 8 | // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ 9 | // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ 10 | // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ 11 | }, 12 | "logging": { 13 | // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ 14 | }, 15 | "debugging": { 16 | // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ 17 | }, 18 | "nodeOptions": { 19 | // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ 20 | // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /entry/src/main/ets/workers/Scanner.ets: -------------------------------------------------------------------------------- 1 | import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS'; 2 | import { FileObject } from '../objects/FileObject'; 3 | 4 | const workerPort: ThreadWorkerGlobalScope = worker.workerPort; 5 | 6 | /** 7 | * Defines the event handler to be called when the worker thread receives a message sent by the host thread. 8 | * The event handler is executed in the worker thread. 9 | * 10 | * @param event message data 11 | */ 12 | workerPort.onmessage = (event: MessageEvents) => { 13 | if (typeof event.data == "string") { 14 | let msg = event.data as string; 15 | // msg is the path 16 | console.log('[Scanner] Received path: ' + msg); 17 | let fileObject: FileObject = new FileObject(msg); 18 | fileObject.update(workerPort); 19 | workerPort.postMessageWithSharedSendable(fileObject); 20 | } 21 | }; 22 | 23 | /** 24 | * Defines the event handler to be called when the worker receives a message that cannot be deserialized. 25 | * The event handler is executed in the worker thread. 26 | * 27 | * @param event message data 28 | */ 29 | workerPort.onmessageerror = (event: MessageEvents) => { 30 | }; 31 | 32 | /** 33 | * Defines the event handler to be called when an exception occurs during worker execution. 34 | * The event handler is executed in the worker thread. 35 | * 36 | * @param event error message 37 | */ 38 | workerPort.onerror = (event: ErrorEvent) => { 39 | }; -------------------------------------------------------------------------------- /entry/src/main/module.json5: -------------------------------------------------------------------------------- 1 | { 2 | "module": { 3 | "name": "entry", 4 | "type": "entry", 5 | "description": "$string:module_desc", 6 | "mainElement": "EntryAbility", 7 | "deviceTypes": [ 8 | "2in1", 9 | "default" 10 | ], 11 | "deliveryWithInstall": true, 12 | "installationFree": false, 13 | "pages": "$profile:main_pages", 14 | "abilities": [ 15 | { 16 | "name": "EntryAbility", 17 | "srcEntry": "./ets/entryability/EntryAbility.ets", 18 | "description": "$string:EntryAbility_desc", 19 | "icon": "$media:layered_image", 20 | "launchType": "multiton", 21 | "label": "$string:EntryAbility_label", 22 | "startWindowIcon": "$media:startIcon", 23 | "startWindowBackground": "$color:start_window_background", 24 | "exported": true, 25 | "skills": [ 26 | { 27 | "entities": [ 28 | "entity.system.home" 29 | ], 30 | "actions": [ 31 | "action.system.home" 32 | ] 33 | } 34 | ] 35 | } 36 | ], 37 | "extensionAbilities": [ 38 | { 39 | "name": "EntryBackupAbility", 40 | "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", 41 | "type": "backup", 42 | "exported": false, 43 | "metadata": [ 44 | { 45 | "name": "ohos.extension.backup", 46 | "resource": "$profile:backup_config" 47 | } 48 | ], 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /entry/src/test/LocalUnit.test.ets: -------------------------------------------------------------------------------- 1 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; 2 | 3 | export default function localUnitTest() { 4 | describe('localUnitTest', () => { 5 | // Defines a test suite. Two parameters are supported: test suite name and test suite function. 6 | beforeAll(() => { 7 | // Presets an action, which is performed only once before all test cases of the test suite start. 8 | // This API supports only one parameter: preset action function. 9 | }); 10 | beforeEach(() => { 11 | // Presets an action, which is performed before each unit test case starts. 12 | // The number of execution times is the same as the number of test cases defined by **it**. 13 | // This API supports only one parameter: preset action function. 14 | }); 15 | afterEach(() => { 16 | // Presets a clear action, which is performed after each unit test case ends. 17 | // The number of execution times is the same as the number of test cases defined by **it**. 18 | // This API supports only one parameter: clear action function. 19 | }); 20 | afterAll(() => { 21 | // Presets a clear action, which is performed after all test cases of the test suite end. 22 | // This API supports only one parameter: clear action function. 23 | }); 24 | it('assertContain', 0, () => { 25 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. 26 | let a = 'abc'; 27 | let b = 'b'; 28 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions. 29 | expect(a).assertContain(b); 30 | expect(a).assertEqual(a); 31 | }); 32 | }); 33 | } -------------------------------------------------------------------------------- /entry/src/ohosTest/ets/test/Ability.test.ets: -------------------------------------------------------------------------------- 1 | import { hilog } from '@kit.PerformanceAnalysisKit'; 2 | import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; 3 | 4 | export default function abilityTest() { 5 | describe('ActsAbilityTest', () => { 6 | // Defines a test suite. Two parameters are supported: test suite name and test suite function. 7 | beforeAll(() => { 8 | // Presets an action, which is performed only once before all test cases of the test suite start. 9 | // This API supports only one parameter: preset action function. 10 | }) 11 | beforeEach(() => { 12 | // Presets an action, which is performed before each unit test case starts. 13 | // The number of execution times is the same as the number of test cases defined by **it**. 14 | // This API supports only one parameter: preset action function. 15 | }) 16 | afterEach(() => { 17 | // Presets a clear action, which is performed after each unit test case ends. 18 | // The number of execution times is the same as the number of test cases defined by **it**. 19 | // This API supports only one parameter: clear action function. 20 | }) 21 | afterAll(() => { 22 | // Presets a clear action, which is performed after all test cases of the test suite end. 23 | // This API supports only one parameter: clear action function. 24 | }) 25 | it('assertContain', 0, () => { 26 | // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. 27 | hilog.info(0x0000, 'testTag', '%{public}s', 'it begin'); 28 | let a = 'abc'; 29 | let b = 'b'; 30 | // Defines a variety of assertion methods, which are used to declare expected boolean conditions. 31 | expect(a).assertContain(b); 32 | expect(a).assertEqual(a); 33 | }) 34 | }) 35 | } -------------------------------------------------------------------------------- /entry/src/main/ets/objects/FileObject.ets: -------------------------------------------------------------------------------- 1 | import { fileIo } from '@kit.CoreFileKit'; 2 | import { collections, ThreadWorkerGlobalScope } from '@kit.ArkTS'; 3 | 4 | export enum FileObjectType { 5 | Directory = 0, 6 | File = 1, 7 | Other = 2 8 | } 9 | 10 | @Sendable 11 | export class FileObject { 12 | path: string; 13 | size: number; 14 | type: number; 15 | id: string; 16 | childrens: collections.Array = new collections.Array(); 17 | 18 | constructor(path: string, size?: number) { 19 | this.path = path; 20 | this.id = path + '@' + Date.now().toString(); 21 | this.size = size || 0; 22 | 23 | let stat = fileIo.statSync(path); 24 | let isDirectory = stat.isDirectory(); 25 | let isFile = stat.isFile(); 26 | 27 | if (isDirectory) { 28 | this.type = 0; 29 | } else if (isFile) { 30 | this.type = 1; 31 | this.size = stat.size; 32 | } else { 33 | this.type = 2; 34 | } 35 | } 36 | 37 | update(workerPort?: ThreadWorkerGlobalScope) { 38 | if (this.type != 0) { 39 | if (workerPort) { 40 | // console.log('[FileObject] New file: ' + this.size.toString()); 41 | workerPort.postMessage(this.size); 42 | } 43 | return this.size; 44 | } 45 | 46 | this.size = 0; 47 | this.childrens = new collections.Array(); 48 | 49 | let dir_list = fileIo.listFileSync(this.path); 50 | for (let index = 0; index < dir_list.length; index++) { 51 | try { 52 | let fo = new FileObject(this.path + '/' + dir_list[index]); 53 | this.size += fo.update(workerPort); 54 | // console.log(this.path + ' - ' + this.size.toString()); 55 | // callback(this.size); 56 | this.childrens.push(fo); 57 | } catch (e) { 58 | console.error(e); 59 | } 60 | } 61 | 62 | return this.size; 63 | } 64 | 65 | getName() { 66 | let pathSplit = this.path.split('/'); 67 | return pathSplit[pathSplit.length-1]; 68 | } 69 | } -------------------------------------------------------------------------------- /entry/src/main/ets/entryability/EntryAbility.ets: -------------------------------------------------------------------------------- 1 | import { AbilityConstant, ConfigurationConstant, UIAbility, Want } from '@kit.AbilityKit'; 2 | import { hilog } from '@kit.PerformanceAnalysisKit'; 3 | import { display, window } from '@kit.ArkUI'; 4 | // import { BusinessError } from '@kit.BasicServicesKit'; 5 | 6 | const DOMAIN = 0x0000; 7 | 8 | export default class EntryAbility extends UIAbility { 9 | onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { 10 | this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); 11 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); 12 | } 13 | 14 | onDestroy(): void { 15 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); 16 | } 17 | 18 | onWindowStageCreate(windowStage: window.WindowStage): void { 19 | // Main window is created, set main page for this ability 20 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); 21 | 22 | windowStage.loadContent('pages/Index', (err) => { 23 | if (err.code) { 24 | hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); 25 | return; 26 | } 27 | hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); 28 | 29 | // let windowClass = windowStage.getMainWindowSync(); // Get app main window 30 | 31 | // try { 32 | // let decorHeight = windowClass.getWindowDecorHeight() || 0; 33 | // let promise = windowClass.resize(this.vp2px(600), this.vp2px(650 + decorHeight)); 34 | // promise.then(() => { 35 | // console.info('Succeeded in changing the window size.'); 36 | // }).catch((err: BusinessError) => { 37 | // console.error(`Failed to change the window size. Cause code: ${err.code}, path: ${err.message}`); 38 | // }); 39 | // } catch (exception) { 40 | // console.error(`Failed to change the window size. Cause code: ${exception.code}, path: ${exception.message}`); 41 | // } 42 | }); 43 | } 44 | 45 | onWindowStageDestroy(): void { 46 | // Main window is destroyed, release UI related resources 47 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); 48 | } 49 | 50 | onForeground(): void { 51 | // Ability has brought to foreground 52 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); 53 | } 54 | 55 | onBackground(): void { 56 | // Ability has back to background 57 | hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); 58 | } 59 | 60 | /** 61 | * Implements a same vp2px as this.getUIContext().vp2px in UI interfaces 62 | * @param vp the real pixel 63 | * @returns the px equivalent 64 | * */ 65 | vp2px(vp: number) { 66 | return vp * display.getDefaultDisplaySync().densityPixels; 67 | } 68 | } -------------------------------------------------------------------------------- /entry/src/main/ets/pages/Index.ets: -------------------------------------------------------------------------------- 1 | import { GridSpace } from '../components/GridSpace'; 2 | import { FileObject } from '../objects/FileObject'; 3 | import { copy } from '../utils/clipboardTools'; 4 | import { addSizeUnits, selectDirectory } from '../utils/storageTools'; 5 | import { common, OpenLinkOptions, Want } from '@kit.AbilityKit'; 6 | import { fileUri } from '@kit.CoreFileKit'; 7 | import { MessageEvents, worker } from '@kit.ArkTS'; 8 | import LinysButton from '../components/LinysButton'; 9 | import { BusinessError } from '@kit.BasicServicesKit'; 10 | 11 | @Entry 12 | @Component 13 | struct Index { 14 | // GLOBAL 15 | @StorageLink('newFileObject') newFileObject: FileObject | undefined = undefined; 16 | // Operations 17 | @LocalStorageLink('closeFolderRequest') closeFolderRequest: string = ''; 18 | @LocalStorageLink('scanFolderSize') scanFolderSize: string = 'Scanned'; 19 | // Environment (base) 20 | @LocalStorageLink('basePath') basePath: string = 'Meow'; 21 | @LocalStorageLink('baseFolderSize') baseFolderSize: string = 'Total'; 22 | @LocalStorageLink('baseFileObject') baseFileObject: FileObject | undefined = undefined; 23 | // Environment (this) 24 | @LocalStorageLink('thisPath') thisPath: string = 'Meow'; 25 | @LocalStorageLink('thisFolderSize') thisFolderSize: string = 'Folder'; 26 | @LocalStorageLink('thisFileObject') thisFileObject: FileObject | undefined = undefined; 27 | // Environment (history) 28 | @LocalStorageLink('paths') paths: string[] = []; 29 | @LocalStorageLink('folderSizes') folderSizes: number[] = []; 30 | @LocalStorageLink('fileObjects') fileObjects: FileObject[] = []; 31 | // UI 32 | @LocalStorageLink('windowId') windowId: number = Date.now(); 33 | @LocalStorageLink('windowHeight') @Watch('onWindowAreaChange') windowHeight: number = 100; 34 | @LocalStorageLink('windowWidth') @Watch('onWindowAreaChange') windowWidth: number = 100; 35 | // Load 36 | @State isLoading: boolean = false; 37 | // This 38 | @State maxSideLength: number = 100; 39 | 40 | aboutToAppear(): void { 41 | this.baseFileObject = this.newFileObject; 42 | this.newFileObject = undefined; 43 | this.rebaseAndDisplay(); 44 | } 45 | 46 | onBackPress(): boolean | void { 47 | if (this.basePath != this.thisPath) { 48 | // If can go back 49 | this.closeFolderRequest = ''; 50 | this.closeFolderRequest = this.thisPath; 51 | return true; 52 | } 53 | return false; 54 | } 55 | 56 | onWindowAreaChange() { 57 | this.maxSideLength = Math.min(this.windowHeight - 70, this.windowWidth); 58 | } 59 | 60 | build() { 61 | Column({ space: 10 }) { 62 | Scroll() { 63 | Row({ space: 10 }) { 64 | LinysButton({ text: $r('app.string.main_back') }) // Back 65 | .opacity(this.thisPath == this.basePath ? 0.5 : 1) 66 | .onClick(() => { 67 | if (this.basePath != this.thisPath) { 68 | this.closeFolderRequest = ''; 69 | this.closeFolderRequest = this.thisPath; 70 | } 71 | }) 72 | .keyboardShortcut(FunctionKey.ESC, []) 73 | LinysButton({ text: $r('app.string.main_select') }) // Select 74 | .enabled(!this.isLoading) 75 | .onClick(() => { 76 | this.isLoading = true; 77 | selectDirectory().then((path) => { 78 | if (path == '') { 79 | this.isLoading = false; 80 | return; 81 | } 82 | // this.baseFileObject.update(); 83 | let workerInstance = new worker.ThreadWorker("entry/ets/workers/Scanner.ets"); 84 | let scannedSize: number = 0; 85 | workerInstance.postMessage(path); 86 | workerInstance.onmessage = (e: MessageEvents): void => { 87 | if (typeof e.data == 'number') { 88 | // Returned a new file's size 89 | scannedSize += e.data; 90 | // console.log(scannedSize.toString()); 91 | this.scanFolderSize = addSizeUnits(scannedSize); 92 | } else { 93 | // Returned the FileObject 94 | this.baseFileObject = e.data as FileObject; 95 | workerInstance.terminate(); 96 | console.log('[Scanner] Result received. Worker terminated. ') 97 | this.rebaseAndDisplay(); 98 | this.isLoading = false; 99 | } 100 | } 101 | }) 102 | }) 103 | LinysButton({ text: $r('app.string.main_go_to_directory') }) // Go to dir 104 | .onClick(() => { 105 | // Open folder 106 | let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; 107 | let link: string = 'filemanager://openDirectory'; 108 | let openLinkOptions: OpenLinkOptions = { 109 | parameters: { 110 | 'fileUri': fileUri.getUriFromPath(this.thisPath) 111 | } 112 | }; 113 | try { 114 | context.openLink(link, openLinkOptions); 115 | } catch (e) { 116 | console.error(e); 117 | } 118 | }) 119 | LinysButton({ text: $r('app.string.main_open_as_new') }) // Open as new 120 | .opacity(!this.thisFileObject ? 0.5 : 1) 121 | .onClick(() => { 122 | // Open as new 123 | this.openAsNew(this.thisFileObject); 124 | }) 125 | LinysButton({ text: this.thisFolderSize + ' / ' + this.baseFolderSize + ' 󰄝' }) // Size Proportion 126 | LinysButton({ text: this.thisPath }) // thisPath 127 | .onClick(() => { 128 | copy(this.thisPath); 129 | }) 130 | LinysButton({ text: this.scanFolderSize + ' 󰀩' }) // Scan indicator 131 | } 132 | } // Title 133 | .align(Alignment.TopStart) 134 | .scrollBar(BarState.Off) 135 | .width('100%') 136 | .scrollable(ScrollDirection.Horizontal) 137 | .edgeEffect(EdgeEffect.Spring) 138 | 139 | GridSpace({ 140 | sideWidth: this.windowWidth - 20, 141 | sideHeight: this.windowHeight - 70, 142 | fileObject: this.baseFileObject, 143 | isOpened: true 144 | }) 145 | } 146 | .onAreaChange((_o, n) => { 147 | this.windowWidth = n.width as number; 148 | this.windowHeight = n.height as number; 149 | }) 150 | .padding(10) 151 | .height('100%') 152 | .width('100%') 153 | } 154 | 155 | rebaseAndDisplay() { 156 | if (!this.baseFileObject) { 157 | return; 158 | } 159 | this.basePath = this.baseFileObject.path; 160 | this.thisPath = this.basePath; 161 | this.thisFileObject = this.baseFileObject; 162 | this.paths = [this.basePath]; 163 | this.folderSizes = [this.baseFileObject.size]; 164 | this.fileObjects = [this.baseFileObject]; 165 | this.baseFolderSize = addSizeUnits(this.baseFileObject.size); 166 | this.thisFolderSize = this.baseFolderSize; 167 | } 168 | 169 | openAsNew(fileObj: FileObject | undefined) { 170 | if (!fileObj) { 171 | return; 172 | } 173 | // Open new window 174 | this.newFileObject = fileObj; 175 | let wantInfo: Want = { 176 | deviceId: '', // This device 177 | bundleName: 'com.next.liny.spaceow', 178 | moduleName: 'entry', 179 | abilityName: 'EntryAbility', 180 | }; 181 | let context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; 182 | context.startAbility(wantInfo).then(() => { 183 | console.log('startAbility success.'); 184 | }).catch((error: BusinessError) => { 185 | console.log('startAbility failed: ' + error); 186 | }); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /entry/src/main/ets/components/GridSpace.ets: -------------------------------------------------------------------------------- 1 | import { animationDefault } from '../hosts/defaults'; 2 | import { DisboardLayoutObject } from '../objects/DisboardLayoutObject'; 3 | import { FileObject } from '../objects/FileObject'; 4 | import { addSizeUnits } from '../utils/storageTools'; 5 | 6 | @Component 7 | export struct GridSpace { 8 | @LocalStorageLink('closeFolderRequest') @Watch('onRequestFolderClose') closeFolderRequest: string = ''; 9 | // environment (base) 10 | @LocalStorageLink('basePath') basePath: string = 'Meow'; 11 | @LocalStorageLink('baseFileObject') baseFileObject: FileObject | undefined = undefined; 12 | // environment (this) 13 | @LocalStorageLink('thisPath') thisPath: string = 'Meow'; 14 | @LocalStorageLink('thisFolderSize') thisFolderSize: string = ''; 15 | @LocalStorageLink('thisFileObject') thisFileObject: FileObject | undefined = undefined; 16 | thisFileObjectID: string = ''; 17 | // environment (history) 18 | @LocalStorageLink('paths') paths: string[] = []; 19 | @LocalStorageLink('folderSizes') folderSizes: number[] = []; 20 | @LocalStorageLink('fileObjects') fileObjects: FileObject[] = []; 21 | // meow 22 | @Prop @Watch('onLayoutSizeChange') sideWidth: number = 60; 23 | @Prop @Watch('onLayoutSizeChange') sideHeight: number = 50; 24 | @Prop @Watch('onPathChange') fileObject: FileObject | undefined = undefined; 25 | @Prop isOpened: boolean = false; 26 | // UI Settings / Params 27 | space: number = 4; 28 | grids: number = 14; 29 | // UI Statuses 30 | @Prop layer: number = 0; 31 | unitLengthVP: number = 10; 32 | maxSideLength: number = 14; 33 | rows: number = 14; 34 | cols: number = 14; 35 | refreshLayoutTimeout: number = 0; 36 | // Effect 37 | @State squareOffsetY: number = 100; 38 | @State squareOpacity: number = 0; 39 | @State useCompactText: boolean = false; 40 | // data 41 | @State layoutObjects: DisboardLayoutObject[] = []; 42 | 43 | aboutToAppear(): void { 44 | this.thisFileObjectID = this.fileObject?.id || ''; 45 | 46 | this.updateUseCompactText(); 47 | this.calculateUnitLength(); 48 | this.process(); 49 | 50 | // Respond to layout change 51 | // triggered by parent GridSpace size change 52 | setInterval(() => { 53 | if (0 < this.refreshLayoutTimeout && this.refreshLayoutTimeout <= 10) { 54 | // Execution of re-arrangement 55 | this.calculateUnitLength(); 56 | if (this.layer == 0) { 57 | this.process(); 58 | } else { 59 | // Cause ForEach contents to refresh 60 | let temp = this.layoutObjects; 61 | this.layoutObjects = []; 62 | this.layoutObjects = temp; 63 | } 64 | } 65 | this.refreshLayoutTimeout -= 10; 66 | if (this.refreshLayoutTimeout < 0) { 67 | this.refreshLayoutTimeout = 0; 68 | } 69 | }, 10); 70 | 71 | // Premiere animation ;P 72 | setTimeout(() => { 73 | this.squareOffsetY = 0; 74 | this.squareOpacity = 1; 75 | }, Math.floor(Math.random() * 200)); 76 | } 77 | 78 | onLayoutSizeChange() { 79 | if (this.layer == 0) { 80 | this.refreshLayoutTimeout = 200; 81 | } else { 82 | this.refreshLayoutTimeout = 50; 83 | } 84 | } 85 | 86 | onPathChange() { 87 | if (!this.fileObject) { 88 | return; 89 | } 90 | if (this.fileObject.id == this.thisFileObjectID) { 91 | // No actual changes in FileObject 92 | return; 93 | } 94 | this.thisFileObjectID = this.fileObject.id; 95 | this.refreshLayoutTimeout = 50; 96 | } 97 | 98 | onRequestFolderClose() { 99 | if (!this.fileObject) { 100 | return; 101 | } 102 | if (!this.closeFolderRequest) { 103 | // Empty request 104 | return; 105 | } 106 | if (this.closeFolderRequest == this.basePath) { 107 | // Impossible to close base file :O 108 | return; 109 | } 110 | if (this.closeFolderRequest != this.fileObject.path) { 111 | // If not closing me 112 | return; 113 | } 114 | // If is closing me (?! 115 | this.isOpened = false; 116 | // Operations (history) 117 | this.paths.splice(this.paths.length - 1, 1); 118 | this.folderSizes.splice(this.folderSizes.length - 1, 1); 119 | this.fileObjects.splice(this.fileObjects.length - 1, 1); 120 | // Operations (this) 121 | this.thisPath = this.paths[this.paths.length - 1]; 122 | this.thisFolderSize = addSizeUnits(this.folderSizes[this.folderSizes.length - 1]); 123 | this.thisFileObject = this.fileObjects[this.paths.length - 1]; 124 | // Set empty 125 | this.closeFolderRequest = ''; 126 | } 127 | 128 | build() { 129 | Column() { 130 | if (this.isOpened && this.fileObject?.type == 0) { 131 | // Display folder contents 132 | ForEach(this.layoutObjects, (item: DisboardLayoutObject, index: number) => { 133 | GridSpace({ 134 | fileObject: item.fileObject, 135 | sideWidth: this.unitLengthVP * this.layoutObjects[index].sideLength - this.space, 136 | sideHeight: this.unitLengthVP * this.layoutObjects[index].sideLength - this.space, 137 | layer: this.layer + 1, 138 | }) 139 | .position({ 140 | top: this.layoutObjects[index].row * this.unitLengthVP, 141 | left: this.layoutObjects[index].col * this.unitLengthVP 142 | }) 143 | .animation(animationDefault()) 144 | }, (item: DisboardLayoutObject) => { 145 | return item.fileObject.path; 146 | }) 147 | } else { 148 | // Display file info 149 | Column() { 150 | Text(this.fileObject?.getName()) 151 | .fontSize((this.useCompactText ? 0.12 : 0.2) * this.sideWidth) 152 | .fontColor($r('app.color.accent')) 153 | .fontWeight(FontWeight.Bold) 154 | .maxLines(this.useCompactText ? 5 : 3) 155 | // .animation(animationDefault()) 156 | Text(addSizeUnits(this.fileObject?.size || 0)) 157 | .fontSize((this.useCompactText ? 0.1 : 0.15) * this.sideWidth) 158 | .fontColor($r('app.color.accent')) 159 | .fontWeight(FontWeight.Medium) 160 | // .animation(animationDefault()) 161 | .opacity(0.75) 162 | } 163 | .width('100%') 164 | .height('100%') 165 | .padding(this.sideWidth * 0.05) 166 | .alignItems(HorizontalAlign.Start) 167 | .justifyContent(FlexAlign.End) 168 | } 169 | } 170 | .height(this.sideHeight) 171 | .width(this.sideWidth) 172 | .backgroundColor(this.isOpened ? $r('app.color.start_window_background') : $r('app.color.supplementary')) 173 | .opacity(this.squareOpacity * (this.isOpened ? 1 : ((this.fileObject?.path.indexOf(this.thisPath) || 0) == 0 ? 1 : 0.5))) 174 | .offset({ y: this.squareOffsetY }) 175 | .animation(animationDefault()) 176 | .padding(this.layer == 0 ? { left: this.space, top: this.space } : {}) 177 | .clip(true) 178 | .borderRadius(Math.max(0, 16 - this.layer * 1.25)) 179 | .onClick(() => { 180 | if (this.fileObject) { 181 | // Not a folder 182 | if (this.fileObject.type != 0) { 183 | return; 184 | } 185 | // Close 186 | if (this.isOpened) { 187 | this.closeFolderRequest = ''; 188 | this.closeFolderRequest = this.thisPath; 189 | return; 190 | } 191 | // Open 192 | if (this.fileObject.path.indexOf(this.thisPath) == 0) { 193 | this.isOpened = true; 194 | this.thisPath = this.fileObject.path; 195 | this.thisFileObject = this.fileObject; 196 | this.thisFolderSize = addSizeUnits(this.fileObject.size); 197 | this.paths.push(this.fileObject.path); 198 | this.folderSizes.push(this.fileObject.size); 199 | this.fileObjects.push(this.fileObject); 200 | return; 201 | } 202 | } 203 | }) 204 | .onMouse((me) => { 205 | if (me.button == MouseButton.Right && me.action == MouseAction.Press) { 206 | // TODO: Options 207 | } 208 | }) 209 | } 210 | 211 | /** 212 | * Calculates and refreshes the unitLength 213 | * */ 214 | private calculateUnitLength() { 215 | // leave more space if is the base layer 216 | let spaceOffset = (this.layer == 0 ? -1 : 1) * this.space; 217 | // Calculates default unitLength 218 | this.unitLengthVP = ((Math.min(this.sideWidth, this.sideHeight) + spaceOffset) / this.grids); 219 | // Update rows and cols of layout 220 | this.rows = Math.floor((this.sideHeight + spaceOffset) / this.unitLengthVP); 221 | this.cols = Math.floor((this.sideWidth + spaceOffset) / this.unitLengthVP); 222 | // Update maxLength of squares 223 | this.maxSideLength = Math.min(this.rows, this.cols); 224 | } 225 | 226 | private process() { 227 | if (!this.fileObject) { 228 | return; 229 | } 230 | if (this.fileObject.type != 0) { 231 | return; 232 | } 233 | 234 | // Timer head 235 | // let timerStart = Date.now(); 236 | 237 | // Init layoutObjects 238 | this.layoutObjects = []; 239 | let lo: DisboardLayoutObject[] = []; 240 | // Init disboard 241 | let disboard: boolean[][] = []; 242 | for (let i = 0; i < this.rows; i++) { 243 | disboard.push(new Array(this.cols).fill(false)); 244 | } 245 | 246 | // Traverse all children and fill processedArrays 247 | for (let index = 0; index < this.fileObject.childrens.length; index++) { 248 | const fo = this.fileObject.childrens[index]; 249 | // Calculate proper side length 250 | let squareSideLength = Math.round(Math.sqrt(fo.size / this.fileObject.size * this.rows * this.cols)); 251 | // Too small process 252 | squareSideLength = Math.max(1, squareSideLength); 253 | // Too big process 254 | if (squareSideLength >= this.maxSideLength) { 255 | squareSideLength = this.maxSideLength; 256 | if (this.rows == this.cols && this.fileObject.childrens.length > 1) { 257 | // Leave some space for other blocks 258 | squareSideLength -= 1; 259 | } 260 | } 261 | // Push to processed list 262 | lo.push(new DisboardLayoutObject(fo, squareSideLength)); 263 | } 264 | 265 | // Sort 266 | lo.sort((a, b) => b.sideLength - a.sideLength); 267 | 268 | // Fill and Determine Position for each square 269 | let objects = 0; 270 | for (let index = 0; index < lo.length; index++) { 271 | let fo = lo[index]; 272 | let placed = false; 273 | 274 | // Continuously try to put 275 | while (!placed && fo.sideLength > 0) { 276 | let foSideLen = fo.sideLength; 277 | let r = 0; 278 | let c = 0; 279 | // Traverse rows 280 | for (r = 0; r < this.rows - foSideLen + 1; r++) { 281 | // Traverse cols 282 | for (c = 0; c < this.cols - foSideLen + 1; c++) { 283 | if (this.canPlaceAtGrid(r, c, foSideLen, disboard)) { 284 | lo[index].row = r; 285 | lo[index].col = c; 286 | this.fillGrid(r, c, foSideLen, disboard); 287 | placed = true; 288 | break; 289 | } 290 | } 291 | // Check if placed on this row 292 | if (placed) { 293 | break; 294 | } 295 | } 296 | // try to fit in by shrink size ?! 297 | if (!placed) { 298 | fo.sideLength = foSideLen - 1; 299 | } 300 | } 301 | 302 | // Stop putting if full 303 | if (disboard[this.rows - 1][this.cols - 1]) { 304 | // Disboard full 305 | break; 306 | } 307 | objects++; 308 | } 309 | 310 | // Cut over length 311 | if (objects < lo.length) { 312 | // console.log('cut @ ' + (objects + 1).toString() + '/' + lo.length.toString()); 313 | this.layoutObjects = lo.slice(0, objects + 1); 314 | } else { 315 | this.layoutObjects = lo; 316 | } 317 | 318 | // Log time 319 | // let timerEnd = Date.now(); 320 | // console.log(' '.repeat(this.layer) + '[GridSpace] Rearranged [' + this.fileObject?.path + '] in ' + (timerEnd - timerStart).toString() + ' ms!'); 321 | } 322 | 323 | private updateUseCompactText() { 324 | if (!this.fileObject) { 325 | this.useCompactText = false; 326 | } else { 327 | this.useCompactText = this.fileObject.getName().length > 16; 328 | } 329 | } 330 | 331 | private canPlaceAtGrid(ro: number, co: number, sl: number, disboard: boolean[][]) { 332 | if (ro + sl > this.rows || co + sl > this.cols) { 333 | // Index out of bounds 334 | return false; 335 | } 336 | for (let i = 0; i < sl; i++) { 337 | try { 338 | if (disboard[ro][co+i]) { 339 | return false; 340 | } 341 | if (disboard[ro+i][co]) { 342 | return false; 343 | } 344 | } catch (e) { 345 | // Index out of bounds? or other issues? 346 | console.error(e); 347 | return false; 348 | } 349 | } 350 | return true; 351 | } 352 | 353 | private fillGrid(ro: number, co: number, sl: number, disboard: boolean[][]) { 354 | for (let r = 0; r < sl; r++) { 355 | for (let c = 0; c < sl; c++) { 356 | disboard[ro+r][co+c] = true; 357 | } 358 | } 359 | } 360 | } --------------------------------------------------------------------------------