commit 976c14cd650bd3af912772bbafadf3da56272f5d Author: 杨堃 <3047900358@qq.com> Date: Mon Jul 13 11:33:59 2026 +0800 首次提交 diff --git a/.arkui-x/android/.gradle/8.4/checksums/checksums.lock b/.arkui-x/android/.gradle/8.4/checksums/checksums.lock new file mode 100644 index 0000000..9819b32 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/checksums/checksums.lock differ diff --git a/.arkui-x/android/.gradle/8.4/checksums/md5-checksums.bin b/.arkui-x/android/.gradle/8.4/checksums/md5-checksums.bin new file mode 100644 index 0000000..6069bca Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/checksums/md5-checksums.bin differ diff --git a/.arkui-x/android/.gradle/8.4/checksums/sha1-checksums.bin b/.arkui-x/android/.gradle/8.4/checksums/sha1-checksums.bin new file mode 100644 index 0000000..d4f7630 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/checksums/sha1-checksums.bin differ diff --git a/.arkui-x/android/.gradle/8.4/dependencies-accessors/dependencies-accessors.lock b/.arkui-x/android/.gradle/8.4/dependencies-accessors/dependencies-accessors.lock new file mode 100644 index 0000000..87ffbe8 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/dependencies-accessors/dependencies-accessors.lock differ diff --git a/.arkui-x/android/.gradle/8.4/dependencies-accessors/gc.properties b/.arkui-x/android/.gradle/8.4/dependencies-accessors/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.arkui-x/android/.gradle/8.4/executionHistory/executionHistory.bin b/.arkui-x/android/.gradle/8.4/executionHistory/executionHistory.bin new file mode 100644 index 0000000..df2476b Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/executionHistory/executionHistory.bin differ diff --git a/.arkui-x/android/.gradle/8.4/executionHistory/executionHistory.lock b/.arkui-x/android/.gradle/8.4/executionHistory/executionHistory.lock new file mode 100644 index 0000000..2d03648 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/executionHistory/executionHistory.lock differ diff --git a/.arkui-x/android/.gradle/8.4/fileChanges/last-build.bin b/.arkui-x/android/.gradle/8.4/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/fileChanges/last-build.bin differ diff --git a/.arkui-x/android/.gradle/8.4/fileHashes/fileHashes.bin b/.arkui-x/android/.gradle/8.4/fileHashes/fileHashes.bin new file mode 100644 index 0000000..d521339 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/fileHashes/fileHashes.bin differ diff --git a/.arkui-x/android/.gradle/8.4/fileHashes/fileHashes.lock b/.arkui-x/android/.gradle/8.4/fileHashes/fileHashes.lock new file mode 100644 index 0000000..e173bb2 Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/fileHashes/fileHashes.lock differ diff --git a/.arkui-x/android/.gradle/8.4/fileHashes/resourceHashesCache.bin b/.arkui-x/android/.gradle/8.4/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..85c556e Binary files /dev/null and b/.arkui-x/android/.gradle/8.4/fileHashes/resourceHashesCache.bin differ diff --git a/.arkui-x/android/.gradle/8.4/gc.properties b/.arkui-x/android/.gradle/8.4/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.arkui-x/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.arkui-x/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..f6ba6ca Binary files /dev/null and b/.arkui-x/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.arkui-x/android/.gradle/buildOutputCleanup/cache.properties b/.arkui-x/android/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..75fb6d4 --- /dev/null +++ b/.arkui-x/android/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Mon Jun 15 17:55:36 CST 2026 +gradle.version=8.4 diff --git a/.arkui-x/android/.gradle/buildOutputCleanup/outputFiles.bin b/.arkui-x/android/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..88619fa Binary files /dev/null and b/.arkui-x/android/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/.arkui-x/android/.gradle/file-system.probe b/.arkui-x/android/.gradle/file-system.probe new file mode 100644 index 0000000..afd43bd Binary files /dev/null and b/.arkui-x/android/.gradle/file-system.probe differ diff --git a/.arkui-x/android/.gradle/vcs-1/gc.properties b/.arkui-x/android/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/.arkui-x/android/app/build.gradle b/.arkui-x/android/app/build.gradle new file mode 100644 index 0000000..63c0150 --- /dev/null +++ b/.arkui-x/android/app/build.gradle @@ -0,0 +1,49 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 33 + buildToolsVersion "36.1.0" + + defaultConfig { + applicationId "com.example.keeping_accounts" + minSdkVersion 26 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + + ndk { + abiFilters "arm64-v8a", "armeabi-v7a" + } + } + + buildTypes { + release { + signingConfig signingConfigs.debug + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + dynamicFeatures = [] + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) + testImplementation 'junit:junit:4.+' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/.arkui-x/android/app/libs/arkui_android_adapter.jar b/.arkui-x/android/app/libs/arkui_android_adapter.jar new file mode 100644 index 0000000..367e95f Binary files /dev/null and b/.arkui-x/android/app/libs/arkui_android_adapter.jar differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libarkui_android.so b/.arkui-x/android/app/libs/arm64-v8a/libarkui_android.so new file mode 100644 index 0000000..ac0d14b Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libarkui_android.so differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libarkui_componentsnapshot.so b/.arkui-x/android/app/libs/arm64-v8a/libarkui_componentsnapshot.so new file mode 100644 index 0000000..f1bde7a Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libarkui_componentsnapshot.so differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libarkui_focuscontroller.so b/.arkui-x/android/app/libs/arm64-v8a/libarkui_focuscontroller.so new file mode 100644 index 0000000..23bdb0a Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libarkui_focuscontroller.so differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libdata_preferences.so b/.arkui-x/android/app/libs/arm64-v8a/libdata_preferences.so new file mode 100644 index 0000000..4a5bf77 Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libdata_preferences.so differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libhilog.so b/.arkui-x/android/app/libs/arm64-v8a/libhilog.so new file mode 100644 index 0000000..ab515eb Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libhilog.so differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libpromptaction.so b/.arkui-x/android/app/libs/arm64-v8a/libpromptaction.so new file mode 100644 index 0000000..4846df3 Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libpromptaction.so differ diff --git a/.arkui-x/android/app/libs/arm64-v8a/libxml2.so b/.arkui-x/android/app/libs/arm64-v8a/libxml2.so new file mode 100644 index 0000000..65a5f7d Binary files /dev/null and b/.arkui-x/android/app/libs/arm64-v8a/libxml2.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libarkui_android.so b/.arkui-x/android/app/libs/armeabi-v7a/libarkui_android.so new file mode 100644 index 0000000..5d6105f Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libarkui_android.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libarkui_componentsnapshot.so b/.arkui-x/android/app/libs/armeabi-v7a/libarkui_componentsnapshot.so new file mode 100644 index 0000000..2e2cdb1 Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libarkui_componentsnapshot.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libarkui_focuscontroller.so b/.arkui-x/android/app/libs/armeabi-v7a/libarkui_focuscontroller.so new file mode 100644 index 0000000..25bbff3 Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libarkui_focuscontroller.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libdata_preferences.so b/.arkui-x/android/app/libs/armeabi-v7a/libdata_preferences.so new file mode 100644 index 0000000..a6cf3cb Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libdata_preferences.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libhilog.so b/.arkui-x/android/app/libs/armeabi-v7a/libhilog.so new file mode 100644 index 0000000..a736a6b Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libhilog.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libpromptaction.so b/.arkui-x/android/app/libs/armeabi-v7a/libpromptaction.so new file mode 100644 index 0000000..5570d54 Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libpromptaction.so differ diff --git a/.arkui-x/android/app/libs/armeabi-v7a/libxml2.so b/.arkui-x/android/app/libs/armeabi-v7a/libxml2.so new file mode 100644 index 0000000..2ef9d13 Binary files /dev/null and b/.arkui-x/android/app/libs/armeabi-v7a/libxml2.so differ diff --git a/.arkui-x/android/app/proguard-rules.pro b/.arkui-x/android/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/.arkui-x/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/.arkui-x/android/app/src/androidTest/java/com/example/keeping_accounts/ExampleInstrumentedTest.java b/.arkui-x/android/app/src/androidTest/java/com/example/keeping_accounts/ExampleInstrumentedTest.java new file mode 100644 index 0000000..26a0a1e --- /dev/null +++ b/.arkui-x/android/app/src/androidTest/java/com/example/keeping_accounts/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.example.keeping_accounts; + +import android.content.Context; + +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.example.keeping_accounts", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/.arkui-x/android/app/src/main/AndroidManifest.xml b/.arkui-x/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..22ea3a3 --- /dev/null +++ b/.arkui-x/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.arkui-x/android/app/src/main/java/com/example/keeping_accounts/LodefusiLodefusiAbilityActivity.java b/.arkui-x/android/app/src/main/java/com/example/keeping_accounts/LodefusiLodefusiAbilityActivity.java new file mode 100644 index 0000000..214f123 --- /dev/null +++ b/.arkui-x/android/app/src/main/java/com/example/keeping_accounts/LodefusiLodefusiAbilityActivity.java @@ -0,0 +1,24 @@ +package com.example.keeping_accounts; + +import android.os.Bundle; +import android.util.Log; + +import ohos.stage.ability.adapter.StageActivity; + + +/** + * Example ace activity class, which will load ArkUI-X ability instance. + * StageActivity is provided by ArkUI-X + * @see + * to build android library + */ +public class LodefusiLodefusiAbilityActivity extends StageActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.e("HiHelloWorld", "LodefusiLodefusiAbilityActivity"); + + setInstanceName("com.example.keeping_accounts:lodefusi:LodefusiAbility:"); + super.onCreate(savedInstanceState); + } +} diff --git a/.arkui-x/android/app/src/main/java/com/example/keeping_accounts/MyApplication.java b/.arkui-x/android/app/src/main/java/com/example/keeping_accounts/MyApplication.java new file mode 100644 index 0000000..b85bbfe --- /dev/null +++ b/.arkui-x/android/app/src/main/java/com/example/keeping_accounts/MyApplication.java @@ -0,0 +1,25 @@ +package com.example.keeping_accounts; + +import android.util.Log; + +import ohos.stage.ability.adapter.StageApplication; + +/** + * Example ace application class, which will load ArkUI-X application instance. + * StageApplication is provided by ArkUI-X + * @see + * to build android library + */ +public class MyApplication extends StageApplication { + private static final String LOG_TAG = "HiHelloWorld"; + + private static final String RES_NAME = "res"; + + @Override + public void onCreate() { + Log.e(LOG_TAG, "MyApplication"); + super.onCreate(); + Log.e(LOG_TAG, "MyApplication onCreate"); + } +} diff --git a/.arkui-x/android/app/src/main/res/drawable/arkuix.png b/.arkui-x/android/app/src/main/res/drawable/arkuix.png new file mode 100644 index 0000000..ee0bdb1 Binary files /dev/null and b/.arkui-x/android/app/src/main/res/drawable/arkuix.png differ diff --git a/.arkui-x/android/app/src/main/res/values/strings.xml b/.arkui-x/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..65ae9fe --- /dev/null +++ b/.arkui-x/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + keeping_accounts + \ No newline at end of file diff --git a/.arkui-x/android/app/src/test/java/com/example/keeping_accounts/ExampleUnitTest.java b/.arkui-x/android/app/src/test/java/com/example/keeping_accounts/ExampleUnitTest.java new file mode 100644 index 0000000..b59539b --- /dev/null +++ b/.arkui-x/android/app/src/test/java/com/example/keeping_accounts/ExampleUnitTest.java @@ -0,0 +1,17 @@ +package com.example.keeping_accounts; + +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void addition_isCorrect() { + assertEquals(4, 2 + 2); + } +} \ No newline at end of file diff --git a/.arkui-x/android/build.gradle b/.arkui-x/android/build.gradle new file mode 100644 index 0000000..7108259 --- /dev/null +++ b/.arkui-x/android/build.gradle @@ -0,0 +1,26 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://repo.huaweicloud.com/repository/maven/' } + } + dependencies { + classpath 'com.android.tools.build:gradle:7.4.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://repo.huaweicloud.com/repository/maven/' } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/.arkui-x/android/gradle.properties b/.arkui-x/android/gradle.properties new file mode 100644 index 0000000..d52c760 --- /dev/null +++ b/.arkui-x/android/gradle.properties @@ -0,0 +1,19 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=false +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=false \ No newline at end of file diff --git a/.arkui-x/android/gradle/wrapper/gradle-wrapper.jar b/.arkui-x/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..490fda8 Binary files /dev/null and b/.arkui-x/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/.arkui-x/android/gradle/wrapper/gradle-wrapper.properties b/.arkui-x/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..483082e --- /dev/null +++ b/.arkui-x/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Jan 07 16:36:34 CST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://repo.huaweicloud.com/gradle/gradle-8.4-bin.zip diff --git a/.arkui-x/android/gradlew b/.arkui-x/android/gradlew new file mode 100644 index 0000000..6fe3981 --- /dev/null +++ b/.arkui-x/android/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="-Dfile.encoding=UTF-8" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/.arkui-x/android/gradlew.bat b/.arkui-x/android/gradlew.bat new file mode 100644 index 0000000..0dc644f --- /dev/null +++ b/.arkui-x/android/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Dfile.encoding=GBK" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/.arkui-x/android/local.properties b/.arkui-x/android/local.properties new file mode 100644 index 0000000..a5b46b0 --- /dev/null +++ b/.arkui-x/android/local.properties @@ -0,0 +1,7 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +sdk.dir=E:/Android \ No newline at end of file diff --git a/.arkui-x/android/settings.gradle b/.arkui-x/android/settings.gradle new file mode 100644 index 0000000..ff98fe2 --- /dev/null +++ b/.arkui-x/android/settings.gradle @@ -0,0 +1,2 @@ + include ':app' +rootProject.name = 'keeping_accounts' \ No newline at end of file diff --git a/.arkui-x/arkui-x-config.json5 b/.arkui-x/arkui-x-config.json5 new file mode 100644 index 0000000..8dfa002 --- /dev/null +++ b/.arkui-x/arkui-x-config.json5 @@ -0,0 +1,6 @@ +{ + "crossplatform": false, + "modules": [ + "lodefusi" + ] +} \ No newline at end of file diff --git a/.arkui-x/ios/app.xcodeproj/project.pbxproj b/.arkui-x/ios/app.xcodeproj/project.pbxproj new file mode 100644 index 0000000..7a77266 --- /dev/null +++ b/.arkui-x/ios/app.xcodeproj/project.pbxproj @@ -0,0 +1,391 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 89E41E4529F505070004F33E /* LodefusiLodefusiAbilityViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 89E41E4329F505070004F33E /* LodefusiLodefusiAbilityViewController.m */; }; + 89E41E4629F505CD0004F33E /* libarkui_ios.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */; }; + 89E41E4729F505CD0004F33E /* libarkui_ios.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DFC5555629D7F36400B63EB3 /* arkui-x in Resources */ = {isa = PBXBuildFile; fileRef = DFC5555529D7F36400B63EB3 /* arkui-x */; }; + F0AF1AD227D3684F008C55DD /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = F0AF1AD127D3684F008C55DD /* AppDelegate.m */; }; + F0AF1ADD27D36856008C55DD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F0AF1ADC27D36856008C55DD /* Assets.xcassets */; }; + F0AF1AE027D36856008C55DD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F0AF1ADE27D36856008C55DD /* LaunchScreen.storyboard */; }; + F0AF1AE327D36856008C55DD /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = F0AF1AE227D36856008C55DD /* main.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 89E41E4829F505CD0004F33E /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 89E41E4729F505CD0004F33E /* libarkui_ios.xcframework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 89E41E4329F505070004F33E /* LodefusiLodefusiAbilityViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LodefusiLodefusiAbilityViewController.m; sourceTree = ""; }; + 89E41E4429F505070004F33E /* LodefusiLodefusiAbilityViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LodefusiLodefusiAbilityViewController.h; sourceTree = ""; }; + DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = libarkui_ios.xcframework; path = frameworks/libarkui_ios.xcframework; sourceTree = ""; }; + DFC5555529D7F36400B63EB3 /* arkui-x */ = {isa = PBXFileReference; lastKnownFileType = folder; path = "arkui-x"; sourceTree = ""; }; + F0AF1ACD27D3684F008C55DD /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; + F0AF1AD027D3684F008C55DD /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + F0AF1AD127D3684F008C55DD /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = AppDelegate.m; sourceTree = ""; }; + F0AF1ADC27D36856008C55DD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + F0AF1ADF27D36856008C55DD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + F0AF1AE127D36856008C55DD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F0AF1AE227D36856008C55DD /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + F0AF1ACA27D3684F008C55DD /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 89E41E4629F505CD0004F33E /* libarkui_ios.xcframework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F0AF1AC427D3684F008C55DD = { + isa = PBXGroup; + children = ( + DFC5555529D7F36400B63EB3 /* arkui-x */, + F0AF1ACF27D3684F008C55DD /* app */, + F0AF1ACE27D3684F008C55DD /* Products */, + F0AF1AEB27D36948008C55DD /* Frameworks */, + ); + sourceTree = ""; + }; + F0AF1ACE27D3684F008C55DD /* Products */ = { + isa = PBXGroup; + children = ( + F0AF1ACD27D3684F008C55DD /* app.app */, + ); + name = Products; + sourceTree = ""; + }; + F0AF1ACF27D3684F008C55DD /* app */ = { + isa = PBXGroup; + children = ( + 89E41E4429F505070004F33E /* LodefusiLodefusiAbilityViewController.h */, + 89E41E4329F505070004F33E /* LodefusiLodefusiAbilityViewController.m */, + F0AF1AD027D3684F008C55DD /* AppDelegate.h */, + F0AF1AD127D3684F008C55DD /* AppDelegate.m */, + F0AF1ADC27D36856008C55DD /* Assets.xcassets */, + F0AF1ADE27D36856008C55DD /* LaunchScreen.storyboard */, + F0AF1AE127D36856008C55DD /* Info.plist */, + F0AF1AE227D36856008C55DD /* main.m */, + ); + path = app; + sourceTree = ""; + }; + F0AF1AEB27D36948008C55DD /* Frameworks */ = { + isa = PBXGroup; + children = ( + DF9C1B5D28B9FC3A005DCF58 /* libarkui_ios.xcframework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F0AF1ACC27D3684F008C55DD /* app */ = { + isa = PBXNativeTarget; + buildConfigurationList = F0AF1AE627D36856008C55DD /* Build configuration list for PBXNativeTarget "app" */; + buildPhases = ( + F0AF1AC927D3684F008C55DD /* Sources */, + F0AF1ACA27D3684F008C55DD /* Frameworks */, + F0AF1ACB27D3684F008C55DD /* Resources */, + 89E41E4829F505CD0004F33E /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = app; + productName = app; + productReference = F0AF1ACD27D3684F008C55DD /* app.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + F0AF1AC527D3684F008C55DD /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + TargetAttributes = { + F0AF1ACC27D3684F008C55DD = { + CreatedOnToolsVersion = 13.0; + }; + }; + }; + buildConfigurationList = F0AF1AC827D3684F008C55DD /* Build configuration list for PBXProject "app" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F0AF1AC427D3684F008C55DD; + productRefGroup = F0AF1ACE27D3684F008C55DD /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F0AF1ACC27D3684F008C55DD /* app */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + F0AF1ACB27D3684F008C55DD /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F0AF1AE027D36856008C55DD /* LaunchScreen.storyboard in Resources */, + DFC5555629D7F36400B63EB3 /* arkui-x in Resources */, + F0AF1ADD27D36856008C55DD /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + F0AF1AC927D3684F008C55DD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F0AF1AD227D3684F008C55DD /* AppDelegate.m in Sources */, + 89E41E4529F505070004F33E /* LodefusiLodefusiAbilityViewController.m in Sources */, + F0AF1AE327D36856008C55DD /* main.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + F0AF1ADE27D36856008C55DD /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + F0AF1ADF27D36856008C55DD /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + F0AF1AE427D36856008C55DD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + F0AF1AE527D36856008C55DD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + F0AF1AE727D36856008C55DD /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JCS8W75JJS; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = app/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.keeping_accounts"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "no sign"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/frameworks/libarkui_ios.xcframework/ios-arm64/libarkui_ios.framework/Headers/include"; + }; + name = Debug; + }; + F0AF1AE827D36856008C55DD /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = JCS8W75JJS; + ENABLE_BITCODE = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = app/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.keeping_accounts"; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = "no sign"; + SWIFT_EMIT_LOC_STRINGS = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + USER_HEADER_SEARCH_PATHS = "$(SRCROOT)/frameworks/libarkui_ios.xcframework/ios-arm64/libarkui_ios.framework/Headers/include"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + F0AF1AC827D3684F008C55DD /* Build configuration list for PBXProject "app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F0AF1AE427D36856008C55DD /* Debug */, + F0AF1AE527D36856008C55DD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F0AF1AE627D36856008C55DD /* Build configuration list for PBXNativeTarget "app" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F0AF1AE727D36856008C55DD /* Debug */, + F0AF1AE827D36856008C55DD /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = F0AF1AC527D3684F008C55DD /* Project object */; +} diff --git a/.arkui-x/ios/app/AppDelegate.h b/.arkui-x/ios/app/AppDelegate.h new file mode 100644 index 0000000..95f9bdd --- /dev/null +++ b/.arkui-x/ios/app/AppDelegate.h @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end + diff --git a/.arkui-x/ios/app/AppDelegate.m b/.arkui-x/ios/app/AppDelegate.m new file mode 100644 index 0000000..eeed5fc --- /dev/null +++ b/.arkui-x/ios/app/AppDelegate.m @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 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 "AppDelegate.h" +#import "LodefusiLodefusiAbilityViewController.h" +#import + +#define BUNDLE_DIRECTORY @"arkui-x" +#define BUNDLE_NAME @"com.example.keeping_accounts" + +@interface AppDelegate () + +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [StageApplication configModuleWithBundleDirectory:BUNDLE_DIRECTORY]; + [StageApplication launchApplication]; + + NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",BUNDLE_NAME, @"lodefusi", @"LodefusiAbility"]; + LodefusiLodefusiAbilityViewController *mainView = [[LodefusiLodefusiAbilityViewController alloc] initWithInstanceName:instanceName]; + [self setNavRootVC:mainView]; + return YES; +} + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + NSLog(@"appdelegate openUrl callback, url : %@", url.absoluteString); // eg: (com.entry.arkui://entry?OtherAbility) + + NSString *bundleName = url.scheme; + NSString *moduleName = url.host; + NSString *abilityName, *params; + + NSURLComponents * urlComponents = [NSURLComponents componentsWithString:url.absoluteString]; + NSArray *array = urlComponents.queryItems; + for (NSURLQueryItem * item in array) { + if ([item.name isEqualToString:@"abilityName"]) { + abilityName = item.value; + } else if ([item.name isEqualToString:@"params"]) { + params = item.value; + } + } + + [self handleOpenUrlWithBundleName:bundleName + moduleName:moduleName + abilityName:abilityName + params:params, nil]; + + return YES; + } + +- (BOOL)handleOpenUrlWithBundleName:(NSString *)bundleName + moduleName:(NSString *)moduleName + abilityName:(NSString *)abilityName + params:(NSString *)params, ...NS_REQUIRES_NIL_TERMINATION { + + id rootVC = [[UIApplication sharedApplication].delegate window].rootViewController; + BOOL hasRoot = NO; + if ([rootVC isKindOfClass:[UINavigationController class]]) { + hasRoot = YES; + } + + id subStageVC = nil; + + if ([moduleName isEqualToString:@"lodefusi"] && [abilityName isEqualToString:@"LodefusiAbility"]) { + NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName]; + LodefusiLodefusiAbilityViewController *otherVC = [[LodefusiLodefusiAbilityViewController alloc] initWithInstanceName:instanceName]; + subStageVC = (LodefusiLodefusiAbilityViewController *)otherVC; + } else if ([moduleName isEqualToString:@"lodefusi"] && [abilityName isEqualToString:@"LodefusiAbility"]) { + NSString *instanceName = [NSString stringWithFormat:@"%@:%@:%@",bundleName, moduleName, abilityName]; + LodefusiLodefusiAbilityViewController *otherVC = [[LodefusiLodefusiAbilityViewController alloc] initWithInstanceName:instanceName]; + subStageVC = (LodefusiLodefusiAbilityViewController *)otherVC; + } // other ViewController + + if (!subStageVC) { + return NO; + } + + if (!hasRoot) { + [self setNavRootVC:subStageVC]; + } else { + UINavigationController *rootNav = (UINavigationController *)self.window.rootViewController; + [rootNav pushViewController:subStageVC animated:YES]; + } + return YES; +} + +- (void)setNavRootVC:(id)viewController { + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + self.window.backgroundColor = [UIColor whiteColor]; + [self.window makeKeyAndVisible]; + UINavigationController *navi = [[UINavigationController alloc]initWithRootViewController:viewController]; + [self setNaviAppearance:navi]; + self.window.rootViewController = navi; +} + +- (void)setNaviAppearance:(UINavigationController *)navi { + UINavigationBarAppearance *appearance = [UINavigationBarAppearance new]; + [appearance configureWithOpaqueBackground]; + appearance.backgroundColor = UIColor.whiteColor; + navi.navigationBar.standardAppearance = appearance; + navi.navigationBar.scrollEdgeAppearance = navi.navigationBar.standardAppearance; +} + +@end diff --git a/.arkui-x/ios/app/Assets.xcassets/AccentColor.colorset/Contents.json b/.arkui-x/ios/app/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/.arkui-x/ios/app/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/.arkui-x/ios/app/Assets.xcassets/AppIcon.appiconset/Contents.json b/.arkui-x/ios/app/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/.arkui-x/ios/app/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/.arkui-x/ios/app/Assets.xcassets/Contents.json b/.arkui-x/ios/app/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/.arkui-x/ios/app/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/.arkui-x/ios/app/Base.lproj/LaunchScreen.storyboard b/.arkui-x/ios/app/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/.arkui-x/ios/app/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.arkui-x/ios/app/Info.plist b/.arkui-x/ios/app/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/.arkui-x/ios/app/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/.arkui-x/ios/app/LodefusiLodefusiAbilityViewController.h b/.arkui-x/ios/app/LodefusiLodefusiAbilityViewController.h new file mode 100644 index 0000000..4f525c8 --- /dev/null +++ b/.arkui-x/ios/app/LodefusiLodefusiAbilityViewController.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 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. + */ + +#ifndef LodefusiLodefusiAbilityViewController_h +#define LodefusiLodefusiAbilityViewController_h +#import +#import +@interface LodefusiLodefusiAbilityViewController : StageViewController + + +@end + +#endif /* LodefusiLodefusiAbilityViewController_h */ diff --git a/.arkui-x/ios/app/LodefusiLodefusiAbilityViewController.m b/.arkui-x/ios/app/LodefusiLodefusiAbilityViewController.m new file mode 100644 index 0000000..2ed6421 --- /dev/null +++ b/.arkui-x/ios/app/LodefusiLodefusiAbilityViewController.m @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 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 "LodefusiLodefusiAbilityViewController.h" + +@interface LodefusiLodefusiAbilityViewController () + +@end + +@implementation LodefusiLodefusiAbilityViewController +- (instancetype)initWithInstanceName:(NSString *)instanceName { + self = [super initWithInstanceName:instanceName]; + if (self) { + + } + return self; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + self.edgesForExtendedLayout = UIRectEdgeNone; + self.extendedLayoutIncludesOpaqueBars = YES; +} +@end diff --git a/.arkui-x/ios/app/main.m b/.arkui-x/ios/app/main.m new file mode 100644 index 0000000..c83b080 --- /dev/null +++ b/.arkui-x/ios/app/main.m @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2022 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 +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + NSString * appDelegateClassName; + @autoreleasepool { + // Setup code that might create autoreleased objects goes here. + appDelegateClassName = NSStringFromClass([AppDelegate class]); + } + return UIApplicationMain(argc, argv, nil, appDelegateClassName); +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d2ff201 --- /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 0000000..3576b74 --- /dev/null +++ b/AppScope/app.json5 @@ -0,0 +1,11 @@ +{ + "app": { + "bundleName": "com.example.keeping_accounts", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "buildVersion": "1", + "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 0000000..b7c2fc8 --- /dev/null +++ b/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "keeping_accounts" + } + ] +} diff --git a/AppScope/resources/base/media/background.png b/AppScope/resources/base/media/background.png new file mode 100644 index 0000000..923f2b3 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 0000000..eb94275 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 0000000..fb49920 --- /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/build-profile.json5 b/build-profile.json5 new file mode 100644 index 0000000..b9e64f7 --- /dev/null +++ b/build-profile.json5 @@ -0,0 +1,57 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + // WARNING: Do not commit this file to public repositories. Signing passwords are embedded below. + "material": { + "certpath": "C:\\Users\\lodefusi\\.ohos\\config\\default_keeping_accounts_QSOCMOSU2_ar8w445YEOohCkq630KmARx3eMOTkvLHQ=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001B5E9F1B4CF8894085D0FF0524E2BC4FF51C05BAC5B446C9B7D7EE12251FAB0531F3141D3BC18123", + "profile": "C:\\Users\\lodefusi\\.ohos\\config\\default_keeping_accounts_QSOCMOSU2_ar8w445YEOohCkq630KmARx3eMOTkvLHQ=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\lodefusi\\.ohos\\config\\default_keeping_accounts_QSOCMOSU2_ar8w445YEOohCkq630KmARx3eMOTkvLHQ=.p12", + "storePassword": "0000001BC298573CFB2C728FF0696210E5AA28B51A3F6BE19F229D05315D537EBC1E4B423AAD86836ADD75" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "6.1.0(23)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": false + } + }, + "targetSdkVersion": "6.1.0(23)" + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "lodefusi", + "srcPath": "./lodefusi", + "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 0000000..073990f --- /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/hvigor/hvigor-config.json5 b/hvigor/hvigor-config.json5 new file mode 100644 index 0000000..2934cd6 --- /dev/null +++ b/hvigor/hvigor-config.json5 @@ -0,0 +1,23 @@ +{ + "modelVersion": "6.1.0", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | "ultrafine" | 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 */ + // "optimizationStrategy": "memory" /* Define the optimization strategy. Value: [ "memory" | "performance" ]. Default: "memory" */ + }, + "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*/ + } +} \ No newline at end of file diff --git a/hvigorfile.ts b/hvigorfile.ts new file mode 100644 index 0000000..47113e2 --- /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. */ +} \ No newline at end of file diff --git a/lodefusi/.gitignore b/lodefusi/.gitignore new file mode 100644 index 0000000..e2713a2 --- /dev/null +++ b/lodefusi/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/lodefusi/build-profile.json5 b/lodefusi/build-profile.json5 new file mode 100644 index 0000000..6bd6457 --- /dev/null +++ b/lodefusi/build-profile.json5 @@ -0,0 +1,33 @@ +{ + "apiType": "stageMode", + "buildOption": { + "resOptions": { + "copyCodeResource": { + "enable": false + } + } + }, + "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/lodefusi/hvigorfile.ts b/lodefusi/hvigorfile.ts new file mode 100644 index 0000000..b0e3a1a --- /dev/null +++ b/lodefusi/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. */ +} \ No newline at end of file diff --git a/lodefusi/obfuscation-rules.txt b/lodefusi/obfuscation-rules.txt new file mode 100644 index 0000000..1e7e54e --- /dev/null +++ b/lodefusi/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/source-obfuscation + +# 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/lodefusi/oh-package.json5 b/lodefusi/oh-package.json5 new file mode 100644 index 0000000..32ec0e1 --- /dev/null +++ b/lodefusi/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "name": "lodefusi", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {} +} + diff --git a/lodefusi/src/main/ets/components/AddExpense.ets b/lodefusi/src/main/ets/components/AddExpense.ets new file mode 100644 index 0000000..7bf6ef7 --- /dev/null +++ b/lodefusi/src/main/ets/components/AddExpense.ets @@ -0,0 +1,343 @@ +import { ExpenseItem, addExpense as addToStore, generateId, EXPENSE_CATEGORIES, INCOME_CATEGORIES, CATEGORY_COLORS } from '../model/ExpenseModel'; +import { aiClassify, submitBill } from '../model/ApiService'; +import { promptAction } from '@kit.ArkUI'; + +@Component +export struct AddExpense { + @State amount: string = ''; + @State selectedCategory: string = EXPENSE_CATEGORIES[0]; + @State remark: string = ''; + @State showSuccess: boolean = false; + @State selectedType: '消费' | '收入' = '消费'; + @State showAmountError: boolean = false; + @State useAI: boolean = false; + @State aiLoading: boolean = false; + @State aiResult: string = ''; + @State submitting: boolean = false; + onSaved?: () => void; + + aboutToAppear(): void { + this.selectedCategory = this.selectedType === '消费' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0]; + } + + onTypeChange(newType: '消费' | '收入'): void { + this.selectedType = newType; + this.selectedCategory = newType === '消费' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0]; + this.useAI = false; + this.aiResult = ''; + } + + async doAddExpense(): Promise { + const amt = parseFloat(this.amount); + if (isNaN(amt) || amt <= 0) { + this.showAmountError = true; + return; + } + this.showAmountError = false; + this.submitting = true; + + let category = this.selectedCategory; + let message = ''; + + try { + if (this.useAI && this.selectedType === '消费') { + this.aiLoading = true; + this.aiResult = ''; + const aiResp = await aiClassify(this.selectedType, amt, this.remark.trim()); + this.aiLoading = false; + + if (aiResp.code === 200 && aiResp.category) { + category = aiResp.category; + message = aiResp.message || ''; + this.aiResult = `AI识别: ${category} — ${message || ''}`; + this.selectedCategory = category; + const billResult = await submitBill(this.selectedType, category, amt, this.remark.trim()); + if (billResult.code !== 200) { + promptAction.showToast({ message: '服务器同步失败,已保存到本地' }); + } + } else { + this.aiResult = aiResp.msg || 'AI分类失败,请手动选择'; + this.submitting = false; + return; + } + } else { + submitBill(this.selectedType, category, amt, this.remark.trim()) + .then(result => { + if (result.code !== 200) { + promptAction.showToast({ message: '服务器同步失败,已保存到本地' }); + } + }) + .catch(() => promptAction.showToast({ message: '网络不可用,已保存到本地' })); + } + + const item: ExpenseItem = { + id: generateId(), + type: this.selectedType, + category: category, + amount: amt, + remark: this.remark.trim(), + message: message, + createTime: new Date().toISOString() + }; + + await addToStore(item); + this.onSaved?.(); + + this.amount = ''; + this.remark = ''; + this.aiResult = ''; + this.useAI = false; + this.selectedCategory = this.selectedType === '消费' ? EXPENSE_CATEGORIES[0] : INCOME_CATEGORIES[0]; + this.showSuccess = true; + setTimeout(() => { this.showSuccess = false; }, 1800); + } finally { + this.submitting = false; + } + } + + build() { + Stack() { + Scroll() { + Column() { + Row() { + Button('消费') + .fontSize(14) + .fontWeight(FontWeight.Medium) + .layoutWeight(1) + .height(42) + .borderRadius(21) + .backgroundColor(this.selectedType === '消费' ? '#E85545' : 'rgba(255,255,255,0.7)') + .fontColor(this.selectedType === '消费' ? '#FFFFFF' : '#86909C') + .onClick(() => { this.onTypeChange('消费'); }) + + Button('收入') + .fontSize(14) + .fontWeight(FontWeight.Medium) + .layoutWeight(1) + .height(42) + .borderRadius(21) + .backgroundColor(this.selectedType === '收入' ? '#2BA471' : 'rgba(255,255,255,0.7)') + .fontColor(this.selectedType === '收入' ? '#FFFFFF' : '#86909C') + .onClick(() => { this.onTypeChange('收入'); }) + .margin({ left: 10 }) + } + .width('100%') + .padding({ left: 16, right: 16 }) + .margin({ top: 20, bottom: 20 }) + + Row() { + Text('¥') + .fontSize(32) + .fontWeight(FontWeight.Bold) + .fontColor(this.selectedType === '消费' ? '#E85545' : '#2BA471') + + TextInput({ text: this.amount, placeholder: '0.00' }) + .fontSize(36) + .fontWeight(FontWeight.Bold) + .type(InputType.Number) + .maxLength(10) + .backgroundColor('transparent') + .layoutWeight(1) + .onChange((value: string) => { + const filtered = value.replace(/[^\d.]/g, ''); + const parts = filtered.split('.'); + if (parts.length > 2) { + this.amount = parts[0] + '.' + parts.slice(1).join(''); + } else { + this.amount = filtered; + } + this.showAmountError = false; + }) + } + .width('100%') + .padding({ left: 16, right: 16 }) + + if (this.showAmountError) { + Text('请输入有效的金额') + .fontSize(12) + .fontColor('#E85545') + .width('100%') + .padding({ left: 16, top: 4 }) + } + + Divider().margin({ top: 18, bottom: 18 }).color('#E5E6EB') + + Column() { + if (this.selectedType === '消费') { + Row() { + Text('选择类别') + .fontSize(13) + .fontColor('#86909C') + + Blank() + + Row() { + Text('AI 智能分类') + .fontSize(12) + .fontColor(this.useAI ? '#4F6EF7' : '#86909C') + .margin({ right: 6 }) + + Toggle({ type: ToggleType.Switch, isOn: this.useAI }) + .selectedColor('#4F6EF7') + .onChange((on: boolean) => { + this.useAI = on; + this.aiResult = ''; + }) + } + } + .width('100%') + .margin({ bottom: 10 }) + + if (this.useAI) { + TextInput({ text: this.remark, placeholder: '输入消费内容,AI 自动识别分类...' }) + .height(48) + .width('100%') + .backgroundColor('rgba(79,110,247,0.06)') + .borderRadius(12) + .padding({ left: 14, right: 14 }) + .border({ width: 1, color: 'rgba(79,110,247,0.2)' }) + .onChange((value: string) => { this.remark = value; }) + + if (this.aiLoading) { + Row() { + LoadingProgress().width(16).height(16).color('#4F6EF7') + Text('AI 正在识别...') + .fontSize(12) + .fontColor('#4F6EF7') + .margin({ left: 8 }) + } + .margin({ top: 12 }) + } + + if (this.aiResult) { + Row() { + Text(this.aiResult) + .fontSize(13) + .fontColor('#1D2129') + .backgroundColor('rgba(79,110,247,0.08)') + .borderRadius(8) + .padding({ left: 12, right: 12, top: 8, bottom: 8 }) + } + .width('100%') + .margin({ top: 10 }) + } + } else { + Grid() { + ForEach(EXPENSE_CATEGORIES, (cat: string) => { + GridItem() { + Column() { + Circle({ width: 42, height: 42 }) + .fill(CATEGORY_COLORS[cat] || '#8899A6') + .opacity(this.selectedCategory === cat ? 1.0 : 0.4) + .shadow(this.selectedCategory === cat ? + { radius: 8, color: (CATEGORY_COLORS[cat] || '#8899A6') + '50', offsetY: 3 } : + { radius: 0, color: 'transparent', offsetY: 0 }) + + Text(cat) + .fontSize(11) + .fontWeight(this.selectedCategory === cat ? FontWeight.Medium : FontWeight.Normal) + .fontColor(this.selectedCategory === cat ? '#1D2129' : '#86909C') + .margin({ top: 6 }) + } + .padding({ top: 5, bottom: 5, left: 2, right: 2 }) + } + .onClick(() => { this.selectedCategory = cat; }) + }) + } + .columnsTemplate('1fr 1fr 1fr 1fr') + .rowsGap(8) + .columnsGap(8) + .width('100%') + } + } + + if (this.selectedType === '收入') { + Text('收入类别') + .fontSize(13) + .fontColor('#86909C') + .width('100%') + .margin({ bottom: 10 }) + + Grid() { + ForEach(INCOME_CATEGORIES, (cat: string) => { + GridItem() { + Column() { + Circle({ width: 42, height: 42 }) + .fill(CATEGORY_COLORS[cat] || '#546E7A') + .opacity(this.selectedCategory === cat ? 1.0 : 0.4) + .shadow(this.selectedCategory === cat ? + { radius: 8, color: (CATEGORY_COLORS[cat] || '#546E7A') + '50', offsetY: 3 } : + { radius: 0, color: 'transparent', offsetY: 0 }) + + Text(cat) + .fontSize(11) + .fontWeight(this.selectedCategory === cat ? FontWeight.Medium : FontWeight.Normal) + .fontColor(this.selectedCategory === cat ? '#1D2129' : '#86909C') + .margin({ top: 6 }) + } + .padding({ top: 5, bottom: 5, left: 2, right: 2 }) + } + .onClick(() => { this.selectedCategory = cat; }) + }) + } + .columnsTemplate('1fr 1fr 1fr 1fr') + .rowsGap(8) + .columnsGap(8) + .width('100%') + } + + if (!this.useAI || this.selectedType === '收入') { + Divider().margin({ top: 18, bottom: 14 }).color('#E5E6EB') + + TextInput({ text: this.remark, placeholder: '添加备注(选填)' }) + .height(48) + .width('100%') + .backgroundColor('rgba(0,0,0,0.03)') + .borderRadius(12) + .padding({ left: 14, right: 14 }) + .onChange((value: string) => { this.remark = value; }) + } + + Blank() + } + .height(380) + .width('100%') + + Button(this.submitting ? '添加中...' : '确认添加') + .width('100%') + .height(50) + .fontSize(16) + .fontWeight(FontWeight.Bold) + .backgroundColor(this.selectedType === '消费' ? '#E85545' : '#2BA471') + .fontColor('#FFFFFF') + .borderRadius(25) + .margin({ top: 24 }) + .shadow({ radius: 10, color: this.selectedType === '消费' ? 'rgba(232,85,69,0.3)' : 'rgba(43,164,113,0.3)', offsetY: 4 }) + .enabled(!this.submitting) + .onClick(() => { this.doAddExpense(); }) + + Blank().height(20) + } + .width('100%') + .padding({ left: 16, right: 16 }) + } + .scrollBar(BarState.Off) + + if (this.showSuccess) { + Column() { + Text('✓ 添加成功') + .fontSize(15) + .fontColor('#FFFFFF') + .padding({ left: 28, right: 28, top: 12, bottom: 12 }) + .backgroundColor(this.selectedType === '消费' ? '#E85545' : '#2BA471') + .borderRadius(22) + .shadow({ radius: 12, color: 'rgba(0,0,0,0.15)', offsetY: 4 }) + } + .position({ bottom: 50 }) + .transition(TransitionEffect.OPACITY.animation({ duration: 300, curve: Curve.EaseOut })) + } + } + .width('100%') + .height('100%') + } +} diff --git a/lodefusi/src/main/ets/components/BluetoothTransfer.ets b/lodefusi/src/main/ets/components/BluetoothTransfer.ets new file mode 100644 index 0000000..a842cb3 --- /dev/null +++ b/lodefusi/src/main/ets/components/BluetoothTransfer.ets @@ -0,0 +1,577 @@ +import { bluetoothService, BluetoothDevice, TransferProgressCallback, TransferResultCallback } from '../model/BluetoothService'; +import { ExpenseItem, loadExpenses, saveExpenses } from '../model/ExpenseModel'; + +enum TransferState { + IDLE, + SELECTING, + SCANNING, + CONNECTING, + TRANSFERRING, + WAITING, + COMPLETE, + ERROR +} + +@Component +export struct BluetoothTransfer { + @State state: TransferState = TransferState.IDLE; + @State stateMessage: string = ''; + @State errorMessage: string = ''; + @State discoveredDevices: BluetoothDevice[] = []; + @State selectedDevice: BluetoothDevice | null = null; + @State transferProgress: number = 0; + @State transferTotal: number = 0; + @State receivedRecords: ExpenseItem[] = []; + @State receivedCount: number = 0; + @Consume expenses: ExpenseItem[] = []; + @State selectedIds: Set = new Set(); + @State selectedCount: number = 0; + @State bluetoothReady: boolean = false; + onDataChanged?: () => void; + + aboutToAppear(): void { + if (!this.expenses) { + this.expenses = []; + } + this.bluetoothReady = bluetoothService.isBluetoothEnabled(); + } + + onDismiss(): void { + bluetoothService.destroy(); + this.state = TransferState.IDLE; + } + + async ensureBluetooth(): Promise { + if (this.bluetoothReady) return true; + this.stateMessage = '正在打开蓝牙...'; + bluetoothService.enableBluetooth(); + const ok = await bluetoothService.waitForBluetoothOn(10000); + if (ok) { + this.bluetoothReady = true; + this.stateMessage = ''; + return true; + } + this.errorMessage = '无法打开蓝牙,请确认蓝牙权限已开启'; + this.state = TransferState.ERROR; + return false; + } + + // ===== 发送流程 ===== + + async startSend(): Promise { + if (this.expenses.length === 0) { + this.errorMessage = '没有可发送的记录'; + this.state = TransferState.ERROR; + return; + } + if (!(await this.ensureBluetooth())) return; + this.state = TransferState.SELECTING; + this.selectedIds.clear(); + this.selectedCount = 0; + this.stateMessage = '选择要发送的记录'; + } + + toggleSelect(id: string): void { + if (this.selectedIds.has(id)) { + this.selectedIds.delete(id); + } else { + this.selectedIds.add(id); + } + this.selectedCount = this.selectedIds.size; + } + + selectAll(): void { + if (this.selectedIds.size === this.expenses.length) { + this.selectedIds.clear(); + this.selectedCount = 0; + } else { + for (const item of this.expenses) { + this.selectedIds.add(item.id); + } + this.selectedCount = this.selectedIds.size; + } + } + + confirmSelection(): void { + if (this.selectedCount === 0) { + this.errorMessage = '请至少选择一条记录'; + this.state = TransferState.ERROR; + return; + } + this.discoveredDevices = []; + this.state = TransferState.SCANNING; + this.stateMessage = '正在搜索附近的蓝牙设备...'; + + bluetoothService.startDiscovery((device: BluetoothDevice) => { + const exists = this.discoveredDevices.some(d => d.deviceId === device.deviceId); + if (!exists) { + this.discoveredDevices = [...this.discoveredDevices, device]; + } + }); + + setTimeout(() => { + if (this.state === TransferState.SCANNING) { + bluetoothService.stopDiscovery(); + if (this.discoveredDevices.length === 0) { + this.errorMessage = '未发现蓝牙设备,请确保对方开启了"接收"模式且蓝牙已打开'; + this.state = TransferState.ERROR; + } + } + }, 12000); + } + + async sendToDevice(device: BluetoothDevice): Promise { + this.selectedDevice = device; + this.state = TransferState.CONNECTING; + this.stateMessage = `正在连接 ${device.deviceName}...`; + + const selectedData = this.expenses.filter(item => this.selectedIds.has(item.id)); + + bluetoothService.connectAndSend( + device.deviceId, + selectedData, + (success: boolean, _data?: ExpenseItem[], error?: string) => { + if (success) { + this.state = TransferState.COMPLETE; + this.stateMessage = `成功发送 ${this.selectedCount} 条记录到 ${device.deviceName}`; + } else { + this.errorMessage = error || '发送失败'; + this.state = TransferState.ERROR; + } + }, + (current: number, total: number) => { + this.transferProgress = current; + this.transferTotal = total; + this.state = TransferState.TRANSFERRING; + } + ); + } + + // ===== 接收流程 ===== + + async startReceive(): Promise { + if (!(await this.ensureBluetooth())) return; + this.state = TransferState.WAITING; + this.stateMessage = `等待发送方连接...\n本机名称: ${bluetoothService.getLocalDeviceName()}`; + this.receivedRecords = []; + this.receivedCount = 0; + + bluetoothService.startServer( + (success: boolean, data?: ExpenseItem[], error?: string) => { + if (success && data) { + this.receivedRecords = data; + this.receivedCount = data.length; + this.state = TransferState.COMPLETE; + this.stateMessage = `接收完成,共 ${data.length} 条记录`; + } else { + this.errorMessage = error || '接收数据失败'; + this.state = TransferState.ERROR; + } + }, + (current: number, _total: number) => { + this.transferProgress = current; + this.state = TransferState.TRANSFERRING; + } + ); + } + + async importReceivedData(): Promise { + if (this.receivedRecords.length === 0) return; + const current = await loadExpenses(); + const all = [...this.receivedRecords, ...current]; + await saveExpenses(all); + this.onDataChanged?.(); + this.onDismiss(); + } + + // ===== 格式化辅助 ===== + + formatProgress(): string { + if (this.transferTotal > 0) { + const pct = Math.round((this.transferProgress / this.transferTotal) * 100); + return `${pct}%`; + } + if (this.transferProgress > 0) { + return `${(this.transferProgress / 1024).toFixed(1)} KB`; + } + return ''; + } + + getDeviceIcon(index: number): string { + const icons = ['📱', '💻', '⌚', '🎧']; + return icons[index % icons.length]; + } + + formatDateShort(iso: string): string { + const d = new Date(iso); + if (isNaN(d.getTime())) return '--'; + return `${d.getMonth() + 1}/${d.getDate()}`; + } + + // ===== 构建 ===== + + build() { + if (this.state === TransferState.IDLE) { + Column() { + this.buildBackdrop() + this.buildIdleView() + } + .width('100%').height('100%') + } else { + Column() { + this.buildBackdrop() + this.buildContentCard() + } + .width('100%').height('100%') + } + } + + @Builder + buildBackdrop() { + Column() + .width('100%').height('100%') + .backgroundColor('rgba(0,0,0,0.45)') + .onClick(() => { + if (this.state === TransferState.IDLE || this.state === TransferState.COMPLETE || this.state === TransferState.ERROR || + this.state === TransferState.WAITING || this.state === TransferState.TRANSFERRING) { + this.onDismiss(); + } + }) + } + + @Builder + buildIdleView() { + Column() { + Text('蓝牙传输') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + .margin({ bottom: 8 }) + + Text('将记账数据发送到附近的设备,或从其他设备接收数据') + .fontSize(13) + .fontColor('#86909C') + .textAlign(TextAlign.Center) + .margin({ bottom: 28 }) + .padding({ left: 20, right: 20 }) + + Row() { + this.buildActionButton('📤', '发送', '#4F6EF7', () => { this.startSend(); }) + this.buildActionButton('📥', '接收', '#2BA471', () => { this.startReceive(); }) + } + .width('100%') + .justifyContent(FlexAlign.SpaceEvenly) + } + .width('84%') + .backgroundColor('#FFFFFF') + .borderRadius(20) + .padding({ top: 28, bottom: 28, left: 20, right: 20 }) + } + + @Builder + buildActionButton(icon: string, label: string, color: string, action: () => void) { + Column() { + Column() { + Text(icon).fontSize(36) + Text(label) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor('#FFFFFF') + .margin({ top: 6 }) + } + .width(110).height(110) + .borderRadius(18) + .justifyContent(FlexAlign.Center) + .backgroundColor(color) + .shadow({ radius: 12, color: color + '40', offsetY: 4 }) + .onClick(action) + } + } + + @Builder + buildContentCard() { + Column() { + this.buildHeader() + + if (this.state === TransferState.SELECTING) { + this.buildSelectView() + } else if (this.state === TransferState.SCANNING || this.state === TransferState.CONNECTING || this.state === TransferState.TRANSFERRING) { + this.buildScanOrTransferView() + } else if (this.state === TransferState.WAITING) { + this.buildWaitingView() + } else if (this.state === TransferState.COMPLETE) { + this.buildCompleteView() + } else if (this.state === TransferState.ERROR) { + this.buildErrorView() + } + } + .width('88%') + .maxHeight('82%') + .backgroundColor('#FFFFFF') + .borderRadius(20) + .padding({ top: 0, bottom: 20, left: 16, right: 16 }) + } + + @Builder + buildHeader() { + Row() { + if (this.state === TransferState.SELECTING || this.state === TransferState.SCANNING || this.state === TransferState.CONNECTING || this.state === TransferState.WAITING) { + Button('✕').fontSize(16).backgroundColor('transparent').fontColor('#86909C') + .onClick(() => { + bluetoothService.destroy(); + this.onDismiss(); + }) + } else { + Blank().width(36) + } + + Text(this.getTitle()) + .fontSize(17) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + .layoutWeight(1) + .textAlign(TextAlign.Center) + + if (this.state === TransferState.SELECTING || this.state === TransferState.SCANNING || this.state === TransferState.CONNECTING || this.state === TransferState.WAITING) { + Blank().width(36) + } else { + Blank().width(36) + } + } + .width('100%') + .height(52) + .padding({ left: 4, right: 4 }) + } + + getTitle(): string { + switch (this.state) { + case TransferState.SELECTING: return '选择记录'; + case TransferState.SCANNING: return '搜索设备'; + case TransferState.CONNECTING: return '正在连接'; + case TransferState.TRANSFERRING: return '传输中'; + case TransferState.WAITING: return '等待连接'; + case TransferState.COMPLETE: return '传输完成'; + case TransferState.ERROR: return '传输失败'; + default: return ''; + } + } + + @Builder + buildSelectView() { + Column() { + Row() { + Button(this.selectedIds.size === this.expenses.length ? '取消全选' : '全选') + .fontSize(12) + .height(30) + .backgroundColor('transparent') + .fontColor('#4F6EF7') + .onClick(() => { this.selectAll(); }) + + Blank() + + Text(`已选 ${this.selectedCount} 条`) + .fontSize(12).fontColor('#86909C') + + Button('发送') + .fontSize(13) + .height(32) + .backgroundColor(this.selectedCount > 0 ? '#4F6EF7' : '#C9CDD4') + .fontColor('#FFFFFF') + .borderRadius(16) + .padding({ left: 16, right: 16 }) + .enabled(this.selectedCount > 0) + .onClick(() => { this.confirmSelection(); }) + .margin({ left: 10 }) + } + .width('100%') + .padding({ left: 4, right: 4, bottom: 10 }) + + List() { + ForEach(this.expenses, (item: ExpenseItem) => { + ListItem() { + Row() { + Checkbox() + .select(this.selectedIds.has(item.id)) + .selectedColor('#4F6EF7') + .onChange((checked: boolean) => { this.toggleSelect(item.id); }) + + Column() { + Row() { + Text(item.category) + .fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1D2129') + + if (item.remark) { + Text(item.remark) + .fontSize(11).fontColor('#A9AEB8') + .maxLines(1).textOverflow({ overflow: TextOverflow.Ellipsis }) + .margin({ left: 8 }) + } + } + + Text(this.formatDateShort(item.createTime)) + .fontSize(10).fontColor('#C9CDD4').margin({ top: 2 }) + } + .margin({ left: 10 }).layoutWeight(1).alignItems(HorizontalAlign.Start) + + Text(`${item.type === '消费' ? '-' : '+'}¥${(item.amount || 0).toFixed(2)}`) + .fontSize(15).fontWeight(FontWeight.Bold) + .fontColor(item.type === '消费' ? '#E85545' : '#2BA471') + } + .width('100%') + .padding({ top: 10, bottom: 10, left: 8, right: 8 }) + .borderRadius(10) + .backgroundColor('#F8F9FC') + .margin({ top: 3, bottom: 3 }) + } + }) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Off) + .divider({ strokeWidth: 0 }) + } + .width('100%') + .layoutWeight(1) + } + + @Builder + buildScanOrTransferView() { + Column() { + if (this.state === TransferState.SCANNING) { + LoadingProgress().width(32).height(32).color('#4F6EF7').margin({ bottom: 16 }) + Text(this.stateMessage) + .fontSize(13).fontColor('#86909C').margin({ bottom: 16 }) + + if (this.discoveredDevices.length > 0) { + Text(`发现 ${this.discoveredDevices.length} 台设备`) + .fontSize(12).fontColor('#A9AEB8').margin({ bottom: 10 }) + + List() { + ForEach(this.discoveredDevices, (device: BluetoothDevice, index: number) => { + ListItem() { + Row() { + Text(this.getDeviceIcon(index)).fontSize(24) + Column() { + Text(device.deviceName).fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1D2129') + Text(device.deviceId).fontSize(10).fontColor('#A9AEB8').maxLines(1) + } + .margin({ left: 12 }).layoutWeight(1).alignItems(HorizontalAlign.Start) + + Button('连接') + .fontSize(12).height(30) + .backgroundColor('#4F6EF7').fontColor('#FFFFFF') + .borderRadius(15).padding({ left: 14, right: 14 }) + .onClick(() => { this.sendToDevice(device); }) + } + .width('100%') + .padding({ top: 12, bottom: 12, left: 12, right: 12 }) + .borderRadius(12) + .backgroundColor('#F8F9FC') + .margin({ top: 3, bottom: 3 }) + } + }) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Off) + } + } else if (this.state === TransferState.CONNECTING) { + LoadingProgress().width(32).height(32).color('#4F6EF7').margin({ bottom: 16 }) + Text(this.stateMessage).fontSize(13).fontColor('#86909C') + } else if (this.state === TransferState.TRANSFERRING) { + Text('📡').fontSize(40).margin({ bottom: 12 }) + Text('正在传输数据...').fontSize(14).fontWeight(FontWeight.Medium).fontColor('#1D2129').margin({ bottom: 8 }) + Text(this.formatProgress()).fontSize(28).fontWeight(FontWeight.Bold).fontColor('#4F6EF7') + } + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + .padding({ bottom: 40 }) + } + + @Builder + buildWaitingView() { + Column() { + Column() { + Text('📡').fontSize(48).margin({ bottom: 12 }) + Text('等待发送方连接') + .fontSize(15).fontWeight(FontWeight.Medium).fontColor('#1D2129').margin({ bottom: 8 }) + Text(this.stateMessage) + .fontSize(12).fontColor('#86909C') + .textAlign(TextAlign.Center).margin({ bottom: 20 }) + + LoadingProgress().width(28).height(28).color('#2BA471') + } + .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) + + Button('取消等待') + .width('100%').height(44) + .fontSize(14).backgroundColor('rgba(0,0,0,0.05)').fontColor('#86909C') + .borderRadius(22) + .onClick(() => { + bluetoothService.stopServer(); + this.onDismiss(); + }) + } + .width('100%') + .layoutWeight(1) + } + + @Builder + buildCompleteView() { + Column() { + Text('✓').fontSize(48).fontColor('#2BA471').margin({ bottom: 12 }) + Text('传输完成') + .fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1D2129').margin({ bottom: 8 }) + Text(this.stateMessage) + .fontSize(13).fontColor('#86909C').textAlign(TextAlign.Center).margin({ bottom: 20 }) + + if (this.receivedRecords.length > 0) { + Text(`收到 ${this.receivedRecords.length} 条记账记录`) + .fontSize(13).fontColor('#4F6EF7').margin({ bottom: 16 }) + + Row() { + this.buildCompleteButton('导入数据', '#2BA471', () => { this.importReceivedData(); }) + this.buildCompleteButton('取消', '#86909C', () => { this.onDismiss(); }) + } + .width('100%') + .justifyContent(FlexAlign.SpaceEvenly) + } else { + this.buildCompleteButton('完成', '#4F6EF7', () => { this.onDismiss(); }) + } + } + .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) + } + + @Builder + buildCompleteButton(label: string, color: string, action: () => void) { + Button(label) + .fontSize(14).height(42) + .backgroundColor(color).fontColor('#FFFFFF') + .borderRadius(21).padding({ left: 32, right: 32 }) + .onClick(action) + } + + @Builder + buildErrorView() { + Column() { + Text('✕').fontSize(40).fontColor('#E85545').margin({ bottom: 12 }) + Text('传输失败').fontSize(18).fontWeight(FontWeight.Bold).fontColor('#1D2129').margin({ bottom: 8 }) + Text(this.errorMessage) + .fontSize(13).fontColor('#86909C').textAlign(TextAlign.Center).margin({ bottom: 24 }) + .padding({ left: 20, right: 20 }) + + Row() { + this.buildCompleteButton('重试', '#4F6EF7', () => { + this.state = TransferState.IDLE; + this.onDismiss(); + }) + this.buildCompleteButton('关闭', 'rgba(0,0,0,0.05)', () => { this.onDismiss(); }) + } + .width('100%') + .justifyContent(FlexAlign.SpaceEvenly) + } + .width('100%').layoutWeight(1).justifyContent(FlexAlign.Center) + } +} diff --git a/lodefusi/src/main/ets/components/Device.ets b/lodefusi/src/main/ets/components/Device.ets new file mode 100644 index 0000000..1b7d7c2 --- /dev/null +++ b/lodefusi/src/main/ets/components/Device.ets @@ -0,0 +1,339 @@ +import { bluetoothService, BluetoothDevice } from '../model/BluetoothService'; +import { ExpenseItem } from '../model/ExpenseModel'; +import { promptAction } from '@kit.ArkUI'; + +interface PairedDevice { + device: BluetoothDevice; + connected: boolean; + lastSync: string; + syncedCount: number; +} + +@Component +export struct DevicePage { + @Consume expenses: ExpenseItem[] = []; + @State devices: PairedDevice[] = []; + @State bluetoothOn: boolean = false; + @State loading: boolean = false; + @State autoSync: boolean = false; + @State syncExpense: boolean = true; + @State syncIncome: boolean = true; + @State showSyncResult: boolean = false; + @State syncMessage: string = ''; + onDataChanged?: () => void; + + aboutToAppear(): void { + this.loadDeviceList(); + } + + loadDeviceList(): void { + this.bluetoothOn = bluetoothService.isBluetoothEnabled(); + if (!this.bluetoothOn) return; + + const paired = bluetoothService.getPairedDevices(); + const result: PairedDevice[] = []; + for (const d of paired) { + result.push({ + device: d, + connected: false, + lastSync: '从未同步', + syncedCount: 0 + }); + } + this.devices = result; + } + + async refreshDevices(): Promise { + this.loading = true; + this.loadDeviceList(); + this.loading = false; + if (!this.bluetoothOn) { + promptAction.showToast({ message: '蓝牙未开启' }); + } else if (this.devices.length === 0) { + promptAction.showToast({ message: '未发现配对设备' }); + } + } + + async connectDevice(device: PairedDevice): Promise { + if (device.connected) return; + promptAction.showToast({ message: `正在连接 ${device.device.deviceName}...` }); + device.connected = true; + } + + async syncToDevice(device: PairedDevice): Promise { + if (this.expenses.length === 0) { + promptAction.showToast({ message: '没有可同步的数据' }); + return; + } + if (!device.connected) { + await this.connectDevice(device); + } + + let data = this.expenses; + if (!this.syncExpense) { + data = data.filter(item => item.type !== '消费'); + } + if (!this.syncIncome) { + data = data.filter(item => item.type !== '收入'); + } + + this.loading = true; + const now = new Date(); + bluetoothService.connectAndSend( + device.device.deviceId, + data, + (success: boolean, _data?: ExpenseItem[], error?: string) => { + this.loading = false; + if (success) { + device.lastSync = `${now.getMonth() + 1}/${now.getDate()} ${now.getHours()}:${now.getMinutes().toString().padStart(2, '0')}`; + device.syncedCount = data.length; + this.syncMessage = `已同步 ${data.length} 条记录`; + this.showSyncResult = true; + setTimeout(() => { this.showSyncResult = false; }, 2000); + } else { + promptAction.showToast({ message: error || '同步失败' }); + } + }, + (current: number, total: number) => { + this.syncMessage = `传输中 ${Math.round((current / total) * 100)}%`; + this.showSyncResult = true; + } + ); + } + + getSyncStatus(device: PairedDevice): string { + if (device.syncedCount === 0) return '未同步'; + return `已同步 ${device.syncedCount} 条`; + } + + build() { + Column() { + if (this.showSyncResult) { + Row() { + Text(this.syncMessage) + .fontSize(13) + .fontColor('#FFFFFF') + .padding({ left: 16, right: 16, top: 10, bottom: 10 }) + .backgroundColor('#4F6EF7') + .borderRadius(20) + } + .width('100%') + .justifyContent(FlexAlign.Center) + .margin({ top: 12, bottom: 4 }) + .transition(TransitionEffect.OPACITY.animation({ duration: 300 })) + } + + Scroll() { + Column() { + // Bluetooth status bar + Row() { + Circle({ width: 8, height: 8 }) + .fill(this.bluetoothOn ? '#2BA471' : '#C9CDD4') + .margin({ right: 8 }) + + Text(this.bluetoothOn ? '蓝牙已开启' : '蓝牙未开启') + .fontSize(13) + .fontColor(this.bluetoothOn ? '#2BA471' : '#86909C') + + Blank() + + Button('刷新') + .fontSize(12) + .height(30) + .backgroundColor('transparent') + .fontColor('#4F6EF7') + .padding({ left: 12, right: 12 }) + .onClick(() => { this.refreshDevices(); }) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius(12) + .padding({ left: 16, right: 8, top: 10, bottom: 10 }) + .margin({ top: 16, bottom: 12, left: 16, right: 16 }) + + // Device list + if (!this.bluetoothOn) { + Column() { + Text('📡') + .fontSize(36) + .margin({ bottom: 10 }) + Text('请先开启蓝牙') + .fontSize(14) + .fontColor('#86909C') + Button('开启蓝牙') + .fontSize(13) + .height(36) + .backgroundColor('#4F6EF7') + .fontColor('#FFFFFF') + .borderRadius(18) + .padding({ left: 24, right: 24 }) + .margin({ top: 14 }) + .onClick(() => { + bluetoothService.enableBluetooth(); + bluetoothService.waitForBluetoothOn(10000).then((ok) => { + if (ok) { + this.bluetoothOn = true; + this.loadDeviceList(); + } else { + promptAction.showToast({ message: '无法开启蓝牙' }); + } + }); + }) + } + .width('100%') + .padding({ top: 40, bottom: 40 }) + } else if (this.devices.length === 0) { + Column() { + Text('⌚') + .fontSize(40) + .margin({ bottom: 10 }) + Text('暂无配对设备') + .fontSize(14) + .fontColor('#86909C') + Text('请在系统蓝牙设置中配对您的手表') + .fontSize(12) + .fontColor('#C9CDD4') + .margin({ top: 6 }) + } + .width('100%') + .padding({ top: 30, bottom: 30 }) + } else { + Text('已配对设备') + .fontSize(13) + .fontColor('#86909C') + .width('100%') + .padding({ left: 16, bottom: 8 }) + + List() { + ForEach(this.devices, (device: PairedDevice) => { + ListItem() { + Column() { + Row() { + Circle({ width: 46, height: 46 }) + .fill('rgba(79,110,247,0.1)') + .border({ width: 2, color: device.connected ? '#2BA471' : '#E5E6EB' }) + + Column() { + Text(device.device.deviceName) + .fontSize(15) + .fontWeight(FontWeight.Medium) + .fontColor('#1D2129') + + Row() { + Circle({ width: 6, height: 6 }) + .fill(device.connected ? '#2BA471' : '#C9CDD4') + .margin({ right: 4 }) + Text(device.connected ? '已连接' : '未连接') + .fontSize(11) + .fontColor(device.connected ? '#2BA471' : '#C9CDD4') + } + .margin({ top: 3 }) + } + .margin({ left: 12 }) + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Button(this.loading ? '同步中...' : '同步') + .fontSize(12) + .height(32) + .backgroundColor('#4F6EF7') + .fontColor('#FFFFFF') + .borderRadius(16) + .padding({ left: 14, right: 14 }) + .enabled(!this.loading) + .onClick(() => { this.syncToDevice(device); }) + } + .width('100%') + + Row() { + Text(`上次同步: ${device.lastSync}`) + .fontSize(11) + .fontColor('#A9AEB8') + + Blank() + + Text(this.getSyncStatus(device)) + .fontSize(11) + .fontColor(device.syncedCount > 0 ? '#2BA471' : '#A9AEB8') + } + .width('100%') + .margin({ top: 10, left: 58 }) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius(14) + .padding(14) + .margin({ bottom: 10 }) + } + }) + } + .width('100%') + .padding({ left: 16, right: 16 }) + .scrollBar(BarState.Off) + } + + // Sync settings + Text('同步设置') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + .width('100%') + .padding({ left: 16, top: 24, bottom: 10 }) + + Column() { + this.SettingItem('发送消费记录', this.syncExpense, (on: boolean) => { this.syncExpense = on; }) + Divider().color('#F2F3F5').margin({ left: 16 }) + this.SettingItem('发送收入记录', this.syncIncome, (on: boolean) => { this.syncIncome = on; }) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius(14) + .padding({ top: 4, bottom: 4 }) + .margin({ left: 16, right: 16 }) + + // Watch app settings + Text('手表应用') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + .width('100%') + .padding({ left: 16, top: 20, bottom: 10 }) + + Column() { + this.SettingItem('手表端显示消费明细', true, (_on: boolean) => {}) + Divider().color('#F2F3F5').margin({ left: 16 }) + this.SettingItem('手表端显示收入明细', true, (_on: boolean) => {}) + Divider().color('#F2F3F5').margin({ left: 16 }) + this.SettingItem('手表端震动提醒', false, (_on: boolean) => {}) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius(14) + .padding({ top: 4, bottom: 4 }) + .margin({ left: 16, right: 16, bottom: 24 }) + } + .width('100%') + } + .scrollBar(BarState.Off) + .layoutWeight(1) + } + .width('100%') + .height('100%') + } + + @Builder + SettingItem(label: string, checked: boolean, onChange: (on: boolean) => void) { + Row() { + Text(label) + .fontSize(14) + .fontColor('#1D2129') + .layoutWeight(1) + + Toggle({ type: ToggleType.Switch, isOn: checked }) + .selectedColor('#4F6EF7') + .onChange(onChange) + } + .width('100%') + .padding({ left: 16, right: 16, top: 12, bottom: 12 }) + } +} diff --git a/lodefusi/src/main/ets/components/ExpenseRecords.ets b/lodefusi/src/main/ets/components/ExpenseRecords.ets new file mode 100644 index 0000000..0696d89 --- /dev/null +++ b/lodefusi/src/main/ets/components/ExpenseRecords.ets @@ -0,0 +1,241 @@ +import { promptAction } from '@kit.ArkUI'; +import { ExpenseItem, INCOME_CATEGORIES, CATEGORY_COLORS, saveExpenses } from '../model/ExpenseModel'; + +@Component +export struct ExpenseRecords { + @Consume expenses: ExpenseItem[] = []; + @Consume filterCategory: string = ''; + deleteTargetId: string = ''; + onDataChanged?: () => void; + onClearFilter?: () => void; + + confirmDelete(id: string): void { + this.deleteTargetId = id; + promptAction.showDialog({ + title: '确认删除', + message: '删除后无法恢复,确定要删除这条记录吗?', + buttons: [ + { text: '取消', color: '#86909C' }, + { text: '删除', color: '#E85545' } + ] + }).then(async (result: promptAction.ShowDialogSuccessResponse) => { + if (result.index === 1) { + await this.performDelete(); + } + this.deleteTargetId = ''; + }); + } + + async performDelete(): Promise { + const id = this.deleteTargetId; + const updated = this.expenses.filter((r: ExpenseItem) => r.id !== id); + await saveExpenses(updated); + this.onDataChanged?.(); + } + + formatDate(iso: string): string { + const d = new Date(iso); + if (isNaN(d.getTime())) return '--'; + const month = (d.getMonth() + 1).toString().padStart(2, '0'); + const day = d.getDate().toString().padStart(2, '0'); + const hour = d.getHours().toString().padStart(2, '0'); + const min = d.getMinutes().toString().padStart(2, '0'); + return `${month}-${day} ${hour}:${min}`; + } + + getFilteredRecords(): ExpenseItem[] { + if (this.filterCategory === '') return this.expenses; + if (this.filterCategory === '收入') return this.expenses.filter(item => item.type === '收入'); + if (INCOME_CATEGORIES.indexOf(this.filterCategory) >= 0) { + return this.expenses.filter(item => item.type === '收入' && item.category === this.filterCategory); + } + return this.expenses.filter(item => item.type === '消费' && item.category === this.filterCategory); + } + + groupByDate(): Map { + const map = new Map(); + for (const item of this.getFilteredRecords()) { + const d = new Date(item.createTime); + if (isNaN(d.getTime())) continue; + const key = `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(item); + } + return map; + } + + getDayTotal(items: ExpenseItem[]): string { + let expense = 0; + let income = 0; + for (const item of items) { + if (item.type === '消费') expense += item.amount || 0; + else income += item.amount || 0; + } + if (income > 0 && expense > 0) return `收 ¥${income.toFixed(2)} 支 ¥${expense.toFixed(2)}`; + if (income > 0) return `收入 ¥${income.toFixed(2)}`; + return `支出 ¥${expense.toFixed(2)}`; + } + + getCategoryColor(item: ExpenseItem): string { + return CATEGORY_COLORS[item.category] || (item.type === '收入' ? '#2BA471' : '#8899A6'); + } + + getFilterLabel(): string { + if (this.filterCategory === '收入') return '全部收入'; + return this.filterCategory; + } + + getDisplayCategory(item: ExpenseItem): string { + if (item.category && item.category.length > 0) return item.category; + return item.type === '收入' ? '收入' : '其他'; + } + + build() { + Column() { + if (this.filterCategory !== '') { + Row() { + Button('← 返回') + .fontSize(12) + .height(32) + .backgroundColor('transparent') + .fontColor('#4F6EF7') + .padding({ left: 8, right: 12 }) + .onClick(() => { this.onClearFilter?.(); }) + + Text(this.getFilterLabel()) + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + + Blank() + + Text(`${this.getFilteredRecords().length} 条`) + .fontSize(12) + .fontColor('#86909C') + .padding({ right: 16 }) + } + .width('100%') + .height(48) + .backgroundColor('rgba(255,255,255,0.85)') + .padding({ left: 4 }) + } + + if (this.getFilteredRecords().length === 0) { + Column() { + Text('📋') + .fontSize(48) + .margin({ bottom: 12 }) + Text(this.filterCategory !== '' ? '暂无该类记录' : '暂无消费记录') + .fontSize(15) + .fontColor('#86909C') + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } else { + List() { + ForEach(Array.from(this.groupByDate().entries()), + (entry: [string, ExpenseItem[]]) => { + ListItem() { + Column() { + Row() { + Text(entry[0]) + .fontSize(13) + .fontWeight(FontWeight.Bold) + .fontColor('#86909C') + + Blank() + + Text(this.getDayTotal(entry[1])) + .fontSize(11) + .fontColor('#A9AEB8') + } + .width('100%') + .padding({ left: 16, right: 16, top: 12, bottom: 4 }) + + ForEach(entry[1], (item: ExpenseItem) => { + ListItem() { + this.RecordRow(item) + } + }) + } + .width('100%') + .padding({ bottom: 8 }) + } + }, + (entry: [string, ExpenseItem[]]) => entry[0] + ) + } + .width('100%') + .layoutWeight(1) + .scrollBar(BarState.Off) + .divider({ strokeWidth: 0.5, color: '#E5E6EB' }) + } + } + .width('100%') + .height('100%') + } + + @Builder + RecordRow(item: ExpenseItem) { + Row() { + Circle({ width: 40, height: 40 }) + .fill(this.getCategoryColor(item)) + + Column() { + Row() { + Text(this.getDisplayCategory(item)) + .fontSize(14) + .fontWeight(FontWeight.Medium) + .fontColor('#1D2129') + + if (item.message) { + Text(item.message) + .fontSize(10) + .fontColor('#4F6EF7') + .backgroundColor('rgba(79,110,247,0.08)') + .borderRadius(6) + .padding({ left: 6, right: 6, top: 2, bottom: 2 }) + .margin({ left: 6 }) + .maxLines(1) + } + } + + if (item.remark) { + Text(item.remark) + .fontSize(12) + .fontColor('#86909C') + .maxLines(1) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + } + } + .margin({ left: 12 }) + .layoutWeight(1) + .alignItems(HorizontalAlign.Start) + + Column() { + Text(`${item.type === '消费' ? '-' : '+'}¥${(item.amount || 0).toFixed(2)}`) + .fontSize(16) + .fontWeight(FontWeight.Bold) + .fontColor(item.type === '消费' ? '#E85545' : '#2BA471') + + Text(this.formatDate(item.createTime)) + .fontSize(10) + .fontColor('#A9AEB8') + } + .alignItems(HorizontalAlign.End) + } + .width('100%') + .padding({ left: 16, right: 16, top: 10, bottom: 10 }) + .backgroundColor('#FFFFFF') + .borderRadius(12) + .shadow({ radius: 6, color: 'rgba(0,0,0,0.03)', offsetY: 1 }) + .margin({ left: 16, right: 16, top: 3, bottom: 3 }) + .gesture( + LongPressGesture() + .onAction(() => { + this.confirmDelete(item.id); + }) + ) + } +} diff --git a/lodefusi/src/main/ets/components/Overview.ets b/lodefusi/src/main/ets/components/Overview.ets new file mode 100644 index 0000000..4b1f433 --- /dev/null +++ b/lodefusi/src/main/ets/components/Overview.ets @@ -0,0 +1,264 @@ +import { ExpenseItem, EXPENSE_CATEGORIES, INCOME_CATEGORIES, CATEGORY_COLORS, loadExpenses } from '../model/ExpenseModel'; + +@Component +export struct Overview { + @Consume @Watch('onExpensesChange') expenses: ExpenseItem[] = []; + @State totalIncome: number = 0; + @State totalExpense: number = 0; + @State categoryTotals: Record = {}; + @State incomeTotals: Record = {}; + @State loaded: boolean = false; + + aboutToAppear(): void { + this.calcTotals(); + } + + onExpensesChange(): void { + this.calcTotals(); + this.loaded = true; + } + + async refreshFromStore(): Promise { + this.expenses = await loadExpenses(); + this.calcTotals(); + } + + calcTotals(): void { + let income = 0; + let expense = 0; + const catMap: Record = {}; + for (const cat of EXPENSE_CATEGORIES) { + catMap[cat] = 0; + } + const incMap: Record = {}; + for (const cat of INCOME_CATEGORIES) { + incMap[cat] = 0; + } + + const now = new Date(); + const thisMonth = now.getMonth(); + const thisYear = now.getFullYear(); + + for (const item of this.expenses) { + const d = new Date(item.createTime); + if (isNaN(d.getTime())) continue; + if (d.getFullYear() === thisYear && d.getMonth() === thisMonth) { + if (item.type === '收入') { + income += item.amount || 0; + if (incMap[item.category] !== undefined) { + incMap[item.category] += item.amount || 0; + } else { + incMap['其他收入'] += item.amount || 0; + } + } else { + expense += item.amount || 0; + if (catMap[item.category] !== undefined) { + catMap[item.category] += item.amount || 0; + } else { + catMap['其他'] += item.amount || 0; + } + } + } + } + + this.totalIncome = income; + this.totalExpense = expense; + this.categoryTotals = catMap; + this.incomeTotals = incMap; + } + + build() { + Column() { + Row() { + this.SummaryCard('总收入', this.totalIncome, '#2BA471') + + this.SummaryCard('总支出', this.totalExpense, '#E85545') + + this.SummaryCard('结余', this.totalIncome - this.totalExpense, '#4F6EF7') + + Column() { + Button('刷新') + .fontSize(12) + .height(32) + .backgroundColor('#4F6EF7') + .fontColor('#FFFFFF') + .borderRadius(16) + .padding({ left: 12, right: 12 }) + .onClick(() => { this.refreshFromStore(); }) + } + .justifyContent(FlexAlign.Center) + .padding({ left: 4, right: 8 }) + } + .width('100%') + .justifyContent(FlexAlign.SpaceEvenly) + .padding({ top: 16, bottom: 6 }) + + if (!this.loaded) { + Column() { + LoadingProgress().width(40).height(40) + } + .width('100%') + .layoutWeight(1) + .justifyContent(FlexAlign.Center) + } else { + Scroll() { + Column() { + if (this.totalExpense > 0) { + Row() { + Text('消费分类') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + } + .width('100%') + .padding({ left: 16, top: 12, bottom: 8 }) + + Grid() { + ForEach(EXPENSE_CATEGORIES.filter(c => (this.categoryTotals[c] || 0) > 0).sort((a, b) => (this.categoryTotals[b] || 0) - (this.categoryTotals[a] || 0)), + (cat: string) => { + GridItem() { + this.CategoryCard(cat, this.categoryTotals[cat] || 0, false) + } + }) + } + .columnsTemplate('1fr 1fr') + .columnsGap(8) + .rowsGap(8) + .width('100%') + .padding({ left: 16, right: 16 }) + } + + if (this.totalIncome > 0) { + Row() { + Text('收入来源') + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + } + .width('100%') + .padding({ left: 16, top: 16, bottom: 8 }) + + Grid() { + ForEach(INCOME_CATEGORIES.filter(c => (this.incomeTotals[c] || 0) > 0).sort((a, b) => (this.incomeTotals[b] || 0) - (this.incomeTotals[a] || 0)), + (cat: string) => { + GridItem() { + this.CategoryCard(cat, this.incomeTotals[cat] || 0, true) + } + }) + } + .columnsTemplate('1fr 1fr') + .columnsGap(8) + .rowsGap(8) + .width('100%') + .padding({ left: 16, right: 16 }) + } + + if (this.totalExpense === 0 && this.totalIncome === 0) { + Column() { + Text('💰') + .fontSize(40) + .margin({ bottom: 10 }) + Text('本月暂无记录') + .fontSize(14) + .fontColor('#86909C') + } + .width('100%') + .padding({ top: 60 }) + } + + Blank().height(16) + } + } + .layoutWeight(1) + .scrollBar(BarState.Off) + } + } + .width('100%') + .height('100%') + } + + @Builder + SummaryCard(label: string, amount: number, color: string) { + Column() { + Stack() { + Progress({ value: 100, total: 100, type: ProgressType.Ring }) + .width(72) + .height(72) + .color('#E5E6EB') + .style({ strokeWidth: 4, scaleCount: 1, scaleWidth: 0 }) + + Progress({ value: this.getRingProgress(amount, label), total: 100, type: ProgressType.Ring }) + .width(72) + .height(72) + .color(color) + .style({ strokeWidth: 4, scaleCount: 1, scaleWidth: 0 }) + + Column() { + Text((amount || 0).toFixed(0)) + .fontSize(15) + .fontWeight(FontWeight.Bold) + .fontColor('#1D2129') + Text('¥') + .fontSize(9) + .fontColor('#86909C') + } + } + + Text(label) + .fontSize(11) + .fontColor('#86909C') + .margin({ top: 5 }) + } + } + + getRingProgress(amount: number, label: string): number { + const amt = amount || 0; + if (label === '总收入') { + return this.totalIncome > 0 ? Math.min(100, (amt / this.totalIncome) * 100) : 0; + } else if (label === '总支出') { + return this.totalExpense > 0 ? Math.min(100, (amt / this.totalExpense) * 100) : 0; + } + const total = (this.totalIncome || 0) + (this.totalExpense || 0); + if (total === 0) return 0; + return Math.min(100, (amt / total) * 100); + } + + @Builder + CategoryCard(cat: string, amount: number, isIncome: boolean) { + Column() { + Row() { + Circle({ width: 10, height: 10 }) + .fill(CATEGORY_COLORS[cat] || '#8899A6') + Text(cat) + .fontSize(13) + .fontWeight(FontWeight.Medium) + .fontColor('#4E5969') + .margin({ left: 8 }) + Blank() + } + .width('100%') + + Text(`${isIncome ? '+' : '-'}¥${(amount || 0).toFixed(2)}`) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor(isIncome ? '#2BA471' : '#E85545') + .margin({ top: 10 }) + + Progress({ + value: amount, + total: isIncome ? (this.totalIncome || 1) : (this.totalExpense || 1), + type: ProgressType.Linear + }) + .width('100%') + .height(4) + .color(CATEGORY_COLORS[cat] || '#8899A6') + .style({ strokeRadius: 2 }) + .margin({ top: 8 }) + } + .width('100%') + .backgroundColor('#FFFFFF') + .borderRadius(14) + .padding(14) + .shadow({ radius: 8, color: 'rgba(0,0,0,0.04)', offsetY: 2 }) + } +} diff --git a/lodefusi/src/main/ets/lodefusiability/LodefusiAbility.ets b/lodefusi/src/main/ets/lodefusiability/LodefusiAbility.ets new file mode 100644 index 0000000..ff9aac9 --- /dev/null +++ b/lodefusi/src/main/ets/lodefusiability/LodefusiAbility.ets @@ -0,0 +1,43 @@ +import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; + +const DOMAIN = 0xD000; + +export default class LodefusiAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + 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'); + } +} diff --git a/lodefusi/src/main/ets/model/ApiConfig.ets b/lodefusi/src/main/ets/model/ApiConfig.ets new file mode 100644 index 0000000..f8f6536 --- /dev/null +++ b/lodefusi/src/main/ets/model/ApiConfig.ets @@ -0,0 +1,34 @@ +import { preferences } from '@kit.ArkData'; + +const CONFIG_STORE = 'app_config'; +const KEY_SERVER_URL = 'server_url'; +const DEFAULT_SERVER = 'http://192.168.1.1:9255'; + +let configStore: preferences.Preferences | null = null; + +async function getStore(): Promise { + if (configStore === null) { + configStore = await preferences.getPreferences(getContext(), CONFIG_STORE); + } + return configStore; +} + +export async function getServerUrl(): Promise { + try { + const store = await getStore(); + const url = await store.get(KEY_SERVER_URL, DEFAULT_SERVER); + return (url as string) || DEFAULT_SERVER; + } catch (_e) { + return DEFAULT_SERVER; + } +} + +export async function setServerUrl(url: string): Promise { + try { + const store = await getStore(); + await store.put(KEY_SERVER_URL, url); + await store.flush(); + } catch (e) { + console.error('setServerUrl failed: ' + e); + } +} diff --git a/lodefusi/src/main/ets/model/ApiService.ets b/lodefusi/src/main/ets/model/ApiService.ets new file mode 100644 index 0000000..971143e --- /dev/null +++ b/lodefusi/src/main/ets/model/ApiService.ets @@ -0,0 +1,107 @@ +import { http } from '@kit.NetworkKit'; +import { getServerUrl } from './ApiConfig'; + +export interface AiResultItem { + category: string; + message: string; +} + +export interface ApiResult { + code: number; + msg: string; + category?: string; + message?: string; + aiResult?: AiResultItem; +} + +export interface BillItem { + id: number; + type: string; + category: string; + amount: number; + message: string; + remark: string; + createTime: string; +} + +export async function submitBill( + type: string, + category: string, + amount: number, + remark: string +): Promise { + const baseUrl = await getServerUrl(); + const req = http.createHttp(); + try { + const body = JSON.stringify({ type, category, amount, remark }); + const resp = await req.request(`${baseUrl}/api/bills`, { + method: http.RequestMethod.POST, + header: { 'Content-Type': 'application/json' }, + extraData: body, + connectTimeout: 5000, + readTimeout: 10000 + }); + if (resp.responseCode === 200) { + return JSON.parse(resp.result as string) as ApiResult; + } + return { code: resp.responseCode, msg: `请求失败: ${resp.responseCode}` }; + } catch (e) { + return { code: -1, msg: '网络不可用,已保存到本地' }; + } finally { + req.destroy(); + } +} + +export async function aiClassify( + type: string, + amount: number, + remark: string +): Promise { + const baseUrl = await getServerUrl(); + const req = http.createHttp(); + try { + const body = JSON.stringify({ type, amount, remark }); + const resp = await req.request(`${baseUrl}/api/transfer`, { + method: http.RequestMethod.POST, + header: { 'Content-Type': 'application/json' }, + extraData: body, + connectTimeout: 5000, + readTimeout: 10000 + }); + if (resp.responseCode === 200) { + return JSON.parse(resp.result as string) as ApiResult; + } + return { code: resp.responseCode, msg: `请求失败: ${resp.responseCode}` }; + } catch (e) { + return { code: -1, msg: '网络不可用,请手动选择分类' }; + } finally { + req.destroy(); + } +} + +export async function fetchBills(type?: string, category?: string): Promise { + const baseUrl = await getServerUrl(); + const req = http.createHttp(); + try { + let queryUrl = `${baseUrl}/api/bills`; + const params: string[] = []; + if (type) params.push(`type=${encodeURIComponent(type)}`); + if (category) params.push(`category=${encodeURIComponent(category)}`); + if (params.length > 0) queryUrl += '?' + params.join('&'); + + const resp = await req.request(queryUrl, { + method: http.RequestMethod.GET, + header: { 'Content-Type': 'application/json' }, + connectTimeout: 5000, + readTimeout: 10000 + }); + if (resp.responseCode === 200) { + return JSON.parse(resp.result as string) as BillItem[]; + } + return []; + } catch (e) { + return []; + } finally { + req.destroy(); + } +} diff --git a/lodefusi/src/main/ets/model/BluetoothService.ets b/lodefusi/src/main/ets/model/BluetoothService.ets new file mode 100644 index 0000000..0573e44 --- /dev/null +++ b/lodefusi/src/main/ets/model/BluetoothService.ets @@ -0,0 +1,357 @@ +import access from '@ohos.bluetooth.access'; +import connection from '@ohos.bluetooth.connection'; +import socket from '@ohos.bluetooth.socket'; +import { ExpenseItem } from './ExpenseModel'; + +export interface BluetoothDevice { + deviceId: string; + deviceName: string; +} + +export type DeviceDiscoveredCallback = (device: BluetoothDevice) => void; +export type TransferProgressCallback = (current: number, total: number) => void; +export type TransferResultCallback = (success: boolean, data?: ExpenseItem[], error?: string) => void; + +const SPP_UUID = '00001101-0000-1000-8000-00805F9B34FB'; +const DATA_END_MARKER = '__BT_EOF__'; + +export class BluetoothService { + private serverSocket: number = -1; + private clientSocket: number = -1; + private onDeviceFound: DeviceDiscoveredCallback | null = null; + private destroyFlag: boolean = false; + + isBluetoothEnabled(): boolean { + try { + return access.getState() === access.BluetoothState.STATE_ON; + } catch (_e) { + return false; + } + } + + enableBluetooth(): void { + if (!this.isBluetoothEnabled()) { + try { + access.enableBluetooth(); + } catch (_e) { + // best effort + } + } + } + + waitForBluetoothOn(timeoutMs: number): Promise { + if (access.getState() === access.BluetoothState.STATE_ON) { + return Promise.resolve(true); + } + return new Promise((resolve) => { + let resolved = false; + const timer = setTimeout(() => { + if (!resolved) { + resolved = true; + try { access.off('stateChange', handler); } catch (_e) {} + resolve(false); + } + }, timeoutMs); + + const handler = (data: access.BluetoothState) => { + if (resolved) return; + if (data === access.BluetoothState.STATE_ON) { + resolved = true; + clearTimeout(timer); + try { access.off('stateChange', handler); } catch (_e) {} + resolve(true); + } + }; + try { + access.on('stateChange', handler); + } catch (_e) { + if (!resolved) { + resolved = true; + clearTimeout(timer); + resolve(false); + } + } + }); + } + + getLocalDeviceName(): string { + try { + return connection.getLocalName(); + } catch (_e) { + return '我的设备'; + } + } + + getPairedDevices(): BluetoothDevice[] { + try { + const list = connection.getPairedDevices(); + const result: BluetoothDevice[] = []; + for (const addr of list) { + if (!addr) continue; + let name = '未知设备'; + try { name = connection.getRemoteDeviceName(addr); } catch (_e) {} + result.push({ deviceId: addr, deviceName: name }); + } + return result; + } catch (_e) { + return []; + } + } + + startDiscovery(onDevice: DeviceDiscoveredCallback): void { + this.onDeviceFound = onDevice; + this.destroyFlag = false; + try { + connection.on('bluetoothDeviceFind', (devices: string[]) => { + if (this.destroyFlag) return; + for (const addr of devices) { + if (!addr) continue; + let name = '未知设备'; + try { name = connection.getRemoteDeviceName(addr); } catch (_e) {} + this.onDeviceFound?.({ deviceId: addr, deviceName: name }); + } + }); + connection.startBluetoothDiscovery(); + } catch (e) { + console.error('startDiscovery failed: ' + JSON.stringify(e)); + } + } + + stopDiscovery(): void { + this.destroyFlag = true; + try { + connection.stopBluetoothDiscovery(); + connection.off('bluetoothDeviceFind'); + } catch (_e) {} + this.onDeviceFound = null; + } + + startServer( + onResult: TransferResultCallback, + onProgress: TransferProgressCallback + ): void { + this.destroyFlag = false; + try { + const option: socket.SppOptions = { + uuid: SPP_UUID, + secure: true, + type: socket.SppType.SPP_RFCOMM + }; + socket.sppListen('记账传输', option, (err, serverId) => { + if (this.destroyFlag) { + this.closeAll(); + return; + } + if (err) { + onResult(false, undefined, '启动监听失败: ' + JSON.stringify(err)); + return; + } + this.serverSocket = serverId; + socket.sppAccept(serverId, (acceptErr, clientId) => { + if (this.destroyFlag) { + this.closeAll(); + return; + } + if (acceptErr) { + onResult(false, undefined, '接受连接失败: ' + JSON.stringify(acceptErr)); + this.closeAll(); + return; + } + this.clientSocket = clientId; + this.receiveDataLoop(onResult, onProgress); + }); + }); + } catch (e) { + if (!this.destroyFlag) { + onResult(false, undefined, '启动监听失败: ' + JSON.stringify(e)); + } + } + } + + private receiveDataLoop( + onResult: TransferResultCallback, + onProgress: TransferProgressCallback + ): void { + const chunks: Uint8Array[] = []; + let totalSize = 0; + + const readHandler = (data: ArrayBuffer) => { + if (this.destroyFlag || this.clientSocket < 0) return; + + const chunk = new Uint8Array(data); + chunks.push(chunk); + totalSize += chunk.byteLength; + + const allData = this.concatChunks(chunks, totalSize); + const text = this.bytesToStr(allData); + const eofIdx = text.indexOf(DATA_END_MARKER); + if (eofIdx >= 0) { + const jsonStr = text.substring(0, eofIdx); + try { + const records = JSON.parse(jsonStr) as ExpenseItem[]; + onProgress(totalSize, totalSize); + onResult(true, records); + } catch (_e) { + onResult(false, undefined, '数据解析失败'); + } + this.closeAll(); + return; + } + onProgress(totalSize, totalSize + 1); + }; + + try { + socket.on('sppRead', this.clientSocket, readHandler); + } catch (e) { + if (!this.destroyFlag) { + onResult(false, undefined, '注册读取监听失败: ' + JSON.stringify(e)); + } + this.closeAll(); + } + } + + connectAndSend( + deviceId: string, + data: ExpenseItem[], + onResult: TransferResultCallback, + onProgress: TransferProgressCallback + ): void { + if (data.length === 0) { + onResult(false, undefined, '没有可发送的数据'); + return; + } + this.destroyFlag = false; + try { + const option: socket.SppOptions = { + uuid: SPP_UUID, + secure: true, + type: socket.SppType.SPP_RFCOMM + }; + socket.sppConnect(deviceId, option, (err, clientId) => { + if (this.destroyFlag) { + this.closeAll(); + return; + } + if (err) { + onResult(false, undefined, '连接失败: ' + JSON.stringify(err)); + this.closeAll(); + return; + } + this.clientSocket = clientId; + this.sendData(data, onResult, onProgress); + }); + } catch (e) { + if (!this.destroyFlag) { + onResult(false, undefined, '连接失败: ' + JSON.stringify(e)); + } + this.closeAll(); + } + } + + private sendData( + data: ExpenseItem[], + onResult: TransferResultCallback, + onProgress: TransferProgressCallback + ): void { + try { + const jsonStr = JSON.stringify(data); + const payload = jsonStr + DATA_END_MARKER; + const buffer = this.strToBytes(payload); + const total = buffer.byteLength; + const chunkSize = 512; + let sent = 0; + + while (sent < total && !this.destroyFlag && this.clientSocket >= 0) { + const end = Math.min(sent + chunkSize, total); + const chunk = buffer.slice(sent, end); + const writeBuf = chunk.buffer.slice(chunk.byteOffset, chunk.byteOffset + chunk.byteLength); + socket.sppWrite(this.clientSocket, writeBuf); + sent = end; + onProgress(sent, total); + } + if (!this.destroyFlag) { + onResult(true); + } + } catch (e) { + if (!this.destroyFlag) { + onResult(false, undefined, '发送失败: ' + JSON.stringify(e)); + } + } finally { + setTimeout(() => this.closeAll(), 300); + } + } + + stopServer(): void { + this.destroyFlag = true; + this.closeAll(); + } + + cancelTransfer(): void { + this.destroyFlag = true; + this.closeAll(); + } + + private closeAll(): void { + try { socket.sppCloseClientSocket(this.clientSocket); } catch (_e) {} + try { socket.sppCloseServerSocket(this.serverSocket); } catch (_e) {} + this.clientSocket = -1; + this.serverSocket = -1; + } + + destroy(): void { + this.destroyFlag = true; + this.stopDiscovery(); + this.closeAll(); + } + + strToBytes(str: string): Uint8Array { + const buf = new ArrayBuffer(str.length * 3); + const view = new Uint8Array(buf); + let pos = 0; + for (let i = 0; i < str.length; i++) { + const code = str.charCodeAt(i); + if (code < 0x80) { + view[pos++] = code; + } else if (code < 0x800) { + view[pos++] = 0xC0 | (code >> 6); + view[pos++] = 0x80 | (code & 0x3F); + } else { + view[pos++] = 0xE0 | (code >> 12); + view[pos++] = 0x80 | ((code >> 6) & 0x3F); + view[pos++] = 0x80 | (code & 0x3F); + } + } + return new Uint8Array(buf, 0, pos); + } + + bytesToStr(arr: Uint8Array): string { + let result = ''; + let i = 0; + while (i < arr.length) { + const b1 = arr[i++]; + if (b1 < 0x80) { + result += String.fromCharCode(b1); + } else if ((b1 & 0xE0) === 0xC0) { + const b2 = arr[i++]; + result += String.fromCharCode(((b1 & 0x1F) << 6) | (b2 & 0x3F)); + } else { + const b2 = arr[i++]; + const b3 = arr[i++]; + result += String.fromCharCode(((b1 & 0x0F) << 12) | ((b2 & 0x3F) << 6) | (b3 & 0x3F)); + } + } + return result; + } + + private concatChunks(chunks: Uint8Array[], totalSize: number): Uint8Array { + const result = new Uint8Array(totalSize); + let offset = 0; + for (const c of chunks) { + result.set(c, offset); + offset += c.byteLength; + } + return result; + } +} + +export const bluetoothService = new BluetoothService(); diff --git a/lodefusi/src/main/ets/model/ExpenseModel.ets b/lodefusi/src/main/ets/model/ExpenseModel.ets new file mode 100644 index 0000000..a58b464 --- /dev/null +++ b/lodefusi/src/main/ets/model/ExpenseModel.ets @@ -0,0 +1,134 @@ +import { preferences } from '@kit.ArkData'; + +export interface ExpenseItem { + id: string; + type: '消费' | '收入'; + category: string; + amount: number; + remark: string; + message: string; + createTime: string; +} + +export const EXPENSE_CATEGORIES: string[] = [ + '餐饮', '医药', '数码', '还款', '购物', '交通', + '娱乐', '住房', '日用', '服饰', '教育', '人情', '其他' +]; + +export const INCOME_CATEGORIES: string[] = [ + '工资', '奖金', '理财', '兼职', '报销', '退款', '红包', '其他收入' +]; + +export const CATEGORY_COLORS: Record = { + '餐饮': '#F4A125', + '医药': '#4CAF50', + '数码': '#4F6EF7', + '还款': '#795548', + '购物': '#9C27B0', + '交通': '#009688', + '娱乐': '#FF5722', + '住房': '#607D8B', + '日用': '#00BCD4', + '服饰': '#E91E63', + '教育': '#3F51B5', + '人情': '#FF6F00', + '其他': '#8899A6', + '工资': '#2E7D32', + '奖金': '#FF8F00', + '理财': '#1565C0', + '兼职': '#6A1B9A', + '报销': '#00838F', + '退款': '#C62828', + '红包': '#D84315', + '其他收入': '#546E7A', +}; + +const STORE_NAME = 'expense_store'; +const KEY_EXPENSES = 'expenses'; + +let storeInstance: preferences.Preferences | null = null; + +async function getOrCreateStore(): Promise { + if (storeInstance === null) { + storeInstance = await preferences.getPreferences(getContext(), STORE_NAME); + } + return storeInstance; +} + +function getNumber(val: Object | undefined): number { + if (typeof val === 'number') return val as number; + return 0; +} + +function getString(val: Object | undefined): string { + if (typeof val === 'string' && (val as string).length > 0) return val as string; + return ''; +} + +function migrateItem(raw: Record): ExpenseItem { + let amount = getNumber(raw.amount); + if (amount === 0) amount = getNumber(raw.mash); + + let category = getString(raw.category); + if (category === '') category = getString(raw.name); + if (category === 'good' || category === 'income') category = '其他收入'; + + let remark = getString(raw.remark); + if (remark === '') remark = getString(raw.text); + if (remark === '') remark = getString(raw.note); + + let type: '消费' | '收入' = '消费'; + if (raw.type === '收入' || raw.type === 'income') { + type = '收入'; + } + + return { + id: getString(raw.id), + type: type, + category: category, + amount: amount, + remark: remark, + message: getString(raw.message), + createTime: getString(raw.createTime) || getString(raw.date) || new Date().toISOString() + }; +} + +export async function loadExpenses(): Promise { + try { + const store = await getOrCreateStore(); + const json = await store.get(KEY_EXPENSES, '[]'); + const raw = JSON.parse(json as string) as Record[]; + const result: ExpenseItem[] = []; + for (let i = 0; i < raw.length; i++) { + result.push(migrateItem(raw[i])); + } + return result; + } catch (e) { + console.error('loadExpenses failed: ' + e); + return []; + } +} + +export async function saveExpenses(expenses: ExpenseItem[]): Promise { + try { + const store = await getOrCreateStore(); + await store.put(KEY_EXPENSES, JSON.stringify(expenses)); + await store.flush(); + } catch (e) { + console.error('saveExpenses failed: ' + e); + } +} + +export async function addExpense(item: ExpenseItem): Promise { + const expenses = await loadExpenses(); + expenses.unshift(item); + await saveExpenses(expenses); + return expenses; +} + +let idCounter = 0; + +export function generateId(): string { + idCounter++; + return Date.now().toString(36) + idCounter.toString(36) + Math.random().toString(36).substring(2, 10); +} diff --git a/lodefusi/src/main/ets/pages/Index.ets b/lodefusi/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..4dea9cd --- /dev/null +++ b/lodefusi/src/main/ets/pages/Index.ets @@ -0,0 +1,164 @@ +import { Overview } from '../components/Overview'; +import { AddExpense } from '../components/AddExpense'; +import { DevicePage } from '../components/Device'; +import { ExpenseItem, loadExpenses } from '../model/ExpenseModel'; + +@Entry +@Component +struct Index { + @State currentIndex: number = 0; + @State showSplash: boolean = true; + @State splashOpacity: number = 0; + @State splashScale: number = 0.9; + @Provide expenses: ExpenseItem[] = []; + private splashTimer: number = -1; + private innerSplashTimer: number = -1; + + async aboutToAppear(): Promise { + this.expenses = await loadExpenses(); + + animateTo({ duration: 1000, curve: Curve.EaseOut }, () => { + this.splashOpacity = 1; + this.splashScale = 1; + }); + + this.splashTimer = setTimeout(() => { + animateTo({ duration: 350, curve: Curve.EaseIn }, () => { + this.splashOpacity = 0; + }); + this.innerSplashTimer = setTimeout(() => { + this.showSplash = false; + }, 350); + }, 2200); + } + + aboutToDisappear(): void { + clearTimeout(this.splashTimer); + clearTimeout(this.innerSplashTimer); + } + + async refreshData(): Promise { + this.expenses = await loadExpenses(); + } + + build() { + if (this.showSplash) { + Column() { + Stack() { + Column() + .width('100%') + .height('100%') + .linearGradient({ + direction: GradientDirection.Bottom, + colors: [['#1D2129', 0], ['#2B3A5C', 0.5], ['#1D2129', 1]] + }) + + Column() { + Text('¥') + .fontSize(56) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .opacity(this.splashOpacity) + .scale({ x: this.splashScale, y: this.splashScale }) + .margin({ bottom: 20 }) + + Text('这个月你省钱了吗?') + .fontSize(26) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + .opacity(this.splashOpacity) + .textShadow({ radius: 4, color: 'rgba(0,0,0,0.4)', offsetX: 0, offsetY: 2 }) + + Text('记录每一笔开销,让消费更有意义') + .fontSize(13) + .fontColor('rgba(255,255,255,0.55)') + .opacity(this.splashOpacity) + .margin({ top: 10 }) + } + .alignItems(HorizontalAlign.Center) + .width('100%') + } + } + .width('100%') + .height('100%') + } else { + Column() + .width('100%') + .height('100%') + .linearGradient({ + direction: GradientDirection.Bottom, + colors: [['#F2F3F8', 0], ['#E8EBF2', 1]] + }) + + Column() { + Tabs({ barPosition: BarPosition.End, index: this.currentIndex }) { + TabContent() { + Overview() + } + .tabBar(this.TabBar('总览', 0)) + + TabContent() { + AddExpense({ onSaved: () => { this.refreshData(); } }) + } + .tabBar(this.TabBar('记账', 1)) + + TabContent() { + DevicePage({ onDataChanged: () => { this.refreshData(); } }) + } + .tabBar(this.TabBar('设备', 2)) + } + .barHeight(60) + .barMode(BarMode.Fixed) + .width('100%') + .layoutWeight(1) + .onChange((index: number) => { + this.currentIndex = index; + }) + } + .width('100%') + .height('100%') + } + } + + @Builder + TabBar(label: string, index: number) { + Column() { + if (index === 0) { + Stack({ alignContent: Alignment.Center }) { + Circle({ width: 24, height: 24 }) + .fill(index === this.currentIndex ? '#4F6EF7' : '#C9CDD4') + Path() + .width(14) + .height(14) + .commands('M7 0 A7 7 0 0 1 14 7 L7 7 Z') + .fill('#FFFFFF') + } + } else if (index === 1) { + Stack({ alignContent: Alignment.Center }) { + Circle({ width: 28, height: 28 }) + .fill(index === this.currentIndex ? '#4F6EF7' : '#C9CDD4') + Text('+') + .fontSize(20) + .fontWeight(FontWeight.Bold) + .fontColor('#FFFFFF') + } + } else { + Stack({ alignContent: Alignment.Center }) { + Circle({ width: 22, height: 22 }) + .fill(index === this.currentIndex ? '#4F6EF7' : '#C9CDD4') + Text('⌚') + .fontSize(13) + } + } + + Text(label) + .fontSize(10) + .fontColor(index === this.currentIndex ? '#4F6EF7' : '#A9AEB8') + .margin({ top: 4 }) + } + .width('100%') + .height('100%') + .justifyContent(FlexAlign.Center) + .padding({ top: 8 }) + } +} diff --git a/lodefusi/src/main/module.json5 b/lodefusi/src/main/module.json5 new file mode 100644 index 0000000..40eb84c --- /dev/null +++ b/lodefusi/src/main/module.json5 @@ -0,0 +1,47 @@ +{ + "module": { + "name": "lodefusi", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "LodefusiAbility", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false, + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + }, + { + "name": "ohos.permission.USE_BLUETOOTH" + }, + { + "name": "ohos.permission.DISCOVER_BLUETOOTH" + } + ], + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "LodefusiAbility", + "srcEntry": "./ets/lodefusiability/LodefusiAbility.ets", + "description": "$string:LodefusiAbility_desc", + "icon": "$media:layered_image", + "label": "$string:LodefusiAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "ohos.want.action.home" + ] + } + ] + } + ] + } +} \ No newline at end of file diff --git a/lodefusi/src/main/resources/base/element/color.json b/lodefusi/src/main/resources/base/element/color.json new file mode 100644 index 0000000..5e7cb11 --- /dev/null +++ b/lodefusi/src/main/resources/base/element/color.json @@ -0,0 +1,40 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#1D2129" + }, + { + "name": "primary", + "value": "#4F6EF7" + }, + { + "name": "income_color", + "value": "#2BA471" + }, + { + "name": "expense_color", + "value": "#E85545" + }, + { + "name": "bg_color", + "value": "#F2F3F8" + }, + { + "name": "card_bg", + "value": "#FFFFFF" + }, + { + "name": "text_primary", + "value": "#1D2129" + }, + { + "name": "text_secondary", + "value": "#86909C" + }, + { + "name": "divider_color", + "value": "#E5E6EB" + } + ] +} diff --git a/lodefusi/src/main/resources/base/element/float.json b/lodefusi/src/main/resources/base/element/float.json new file mode 100644 index 0000000..baa1bee --- /dev/null +++ b/lodefusi/src/main/resources/base/element/float.json @@ -0,0 +1,32 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + }, + { + "name": "title_font_size", + "value": "22fp" + }, + { + "name": "body_font_size", + "value": "16fp" + }, + { + "name": "small_font_size", + "value": "12fp" + }, + { + "name": "loading_text_size", + "value": "28fp" + }, + { + "name": "card_radius", + "value": "16fp" + }, + { + "name": "button_radius", + "value": "24fp" + } + ] +} diff --git a/lodefusi/src/main/resources/base/element/string.json b/lodefusi/src/main/resources/base/element/string.json new file mode 100644 index 0000000..50d8b2e --- /dev/null +++ b/lodefusi/src/main/resources/base/element/string.json @@ -0,0 +1,100 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "记账小助手" + }, + { + "name": "LodefusiAbility_desc", + "value": "智能记账应用,支持AI分类" + }, + { + "name": "LodefusiAbility_label", + "value": "记账" + }, + { + "name": "loading_text", + "value": "这个月你省钱了吗?" + }, + { + "name": "tab_overview", + "value": "总览" + }, + { + "name": "tab_add", + "value": "记账" + }, + { + "name": "tab_records", + "value": "记录" + }, + { + "name": "total_income", + "value": "总收入" + }, + { + "name": "total_expense", + "value": "总支出" + }, + { + "name": "total_balance", + "value": "本月结余" + }, + { + "name": "add_expense_title", + "value": "记账" + }, + { + "name": "amount_hint", + "value": "请输入金额" + }, + { + "name": "category_label", + "value": "选择类别" + }, + { + "name": "ai_classify_label", + "value": "AI 智能分类" + }, + { + "name": "note_hint", + "value": "添加备注(选填)" + }, + { + "name": "submit_btn", + "value": "确认添加" + }, + { + "name": "records_title", + "value": "消费记录" + }, + { + "name": "no_records", + "value": "暂无消费记录" + }, + { + "name": "server_settings", + "value": "服务器地址" + }, + { + "name": "server_url_hint", + "value": "http://192.168.1.1:9255" + }, + { + "name": "type_expense", + "value": "消费" + }, + { + "name": "type_income", + "value": "收入" + }, + { + "name": "refresh_btn", + "value": "刷新" + }, + { + "name": "back_btn", + "value": "← 返回" + } + ] +} diff --git a/lodefusi/src/main/resources/base/media/background.png b/lodefusi/src/main/resources/base/media/background.png new file mode 100644 index 0000000..923f2b3 Binary files /dev/null and b/lodefusi/src/main/resources/base/media/background.png differ diff --git a/lodefusi/src/main/resources/base/media/foreground.png b/lodefusi/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000..97014d3 Binary files /dev/null and b/lodefusi/src/main/resources/base/media/foreground.png differ diff --git a/lodefusi/src/main/resources/base/media/layered_image.json b/lodefusi/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000..fb49920 --- /dev/null +++ b/lodefusi/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/lodefusi/src/main/resources/base/media/startIcon.png b/lodefusi/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000..205ad8b Binary files /dev/null and b/lodefusi/src/main/resources/base/media/startIcon.png differ diff --git a/lodefusi/src/main/resources/base/profile/main_pages.json b/lodefusi/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000..1898d94 --- /dev/null +++ b/lodefusi/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/lodefusi/src/mock/mock-config.json5 b/lodefusi/src/mock/mock-config.json5 new file mode 100644 index 0000000..7a73a41 --- /dev/null +++ b/lodefusi/src/mock/mock-config.json5 @@ -0,0 +1,2 @@ +{ +} \ No newline at end of file diff --git a/lodefusi/src/ohosTest/ets/test/Ability.test.ets b/lodefusi/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000..85c78f6 --- /dev/null +++ b/lodefusi/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/lodefusi/src/ohosTest/ets/test/List.test.ets b/lodefusi/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000..794c7dc --- /dev/null +++ b/lodefusi/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/lodefusi/src/ohosTest/module.json5 b/lodefusi/src/ohosTest/module.json5 new file mode 100644 index 0000000..e20dacb --- /dev/null +++ b/lodefusi/src/ohosTest/module.json5 @@ -0,0 +1,11 @@ +{ + "module": { + "name": "lodefusi_test", + "type": "feature", + "deviceTypes": [ + "phone" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/lodefusi/src/test/List.test.ets b/lodefusi/src/test/List.test.ets new file mode 100644 index 0000000..bb5b5c3 --- /dev/null +++ b/lodefusi/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/lodefusi/src/test/LocalUnit.test.ets b/lodefusi/src/test/LocalUnit.test.ets new file mode 100644 index 0000000..165fc16 --- /dev/null +++ b/lodefusi/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/oh-package-lock.json5 b/oh-package-lock.json5 new file mode 100644 index 0000000..c5a91ec --- /dev/null +++ b/oh-package-lock.json5 @@ -0,0 +1,28 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "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.25": "@ohos/hypium@1.0.25" + }, + "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.25": { + "name": "@ohos/hypium", + "version": "1.0.25", + "integrity": "sha512-l6uO2pjl8HyEKdekLqQt7tUpWbDqX/42zoAzkagtUVZAW9jT6lMvbe54MVjoLxq/RwQGygRvi6j4GpypSMFSHw==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.25.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/oh-package.json5 b/oh-package.json5 new file mode 100644 index 0000000..41eb3e0 --- /dev/null +++ b/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "modelVersion": "6.1.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.25", + "@ohos/hamock": "1.0.0" + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4f96d50 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "keeping_accounts", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}