更新 shenyan-app: 冰箱贴独立输入框 + push-server + APNs图标 + 完整.gitignore
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
10
shenyan-app/.gitignore
vendored
@@ -43,11 +43,6 @@ yarn-error.log
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://docs.fastlane.tools/best-practices/source-control/
|
||||
|
||||
**/fastlane/report.xml
|
||||
**/fastlane/Preview.html
|
||||
**/fastlane/screenshots
|
||||
@@ -73,3 +68,8 @@ yarn-error.log
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# bridge-local (contains APNs keys and local config)
|
||||
bridge-local/node_modules/
|
||||
bridge-local/*.p8
|
||||
bridge-local/apns-key-page.png
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">101.36.73.102</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
154
shenyan-app/bridge-local/package-lock.json
generated
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"name": "bridge-local",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bridge-local",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
|
||||
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^4.0.1",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jwa/-/jwa-2.0.1.tgz",
|
||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/jws/-/jws-4.0.1.tgz",
|
||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.8.0",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz",
|
||||
"integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
shenyan-app/bridge-local/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "bridge-local",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "responder.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"jsonwebtoken": "^9.0.3"
|
||||
}
|
||||
}
|
||||
2238
shenyan-app/ios/Podfile.lock
Normal file
@@ -9,6 +9,10 @@
|
||||
/* Begin PBXBuildFile section */
|
||||
0C80B921A6F3F58F76C31292 /* libPods-ShenYanApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ShenYanApp.a */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
3231D459AC31E2728D987F8E /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */; };
|
||||
16AA953C8FA640C8B4ACA19D /* HealthKitBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = DD038106F9A04B6F9B117189 /* HealthKitBridge.m */; };
|
||||
5E73DDCAF25141D6A3511872 /* WeatherKitBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA8B565A2BF34A1AA5C637AD /* WeatherKitBridge.swift */; };
|
||||
011F82AA74254221BC703CE1 /* WeatherKitBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 119851A6272F407B9F73741B /* WeatherKitBridge.m */; };
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -18,6 +22,9 @@
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ShenYanApp/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ShenYanApp/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = PrivacyInfo.xcprivacy; path = ShenYanApp/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
DD038106F9A04B6F9B117189 /* HealthKitBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HealthKitBridge.m; path = ShenYanApp/HealthKitBridge.m; sourceTree = "<group>"; };
|
||||
EA8B565A2BF34A1AA5C637AD /* WeatherKitBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WeatherKitBridge.swift; path = ShenYanApp/WeatherKitBridge.swift; sourceTree = "<group>"; };
|
||||
119851A6272F407B9F73741B /* WeatherKitBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = WeatherKitBridge.m; path = ShenYanApp/WeatherKitBridge.m; sourceTree = "<group>"; };
|
||||
3B4392A12AC88292D35C810B /* Pods-ShenYanApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShenYanApp.debug.xcconfig"; path = "Target Support Files/Pods-ShenYanApp/Pods-ShenYanApp.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
5709B34CF0A7D63546082F79 /* Pods-ShenYanApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShenYanApp.release.xcconfig"; path = "Target Support Files/Pods-ShenYanApp/Pods-ShenYanApp.release.xcconfig"; sourceTree = "<group>"; };
|
||||
5DCACB8F33CDC322A6C60F78 /* libPods-ShenYanApp.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-ShenYanApp.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@@ -46,6 +53,9 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
|
||||
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
|
||||
DD038106F9A04B6F9B117189 /* HealthKitBridge.m */,
|
||||
EA8B565A2BF34A1AA5C637AD /* WeatherKitBridge.swift */,
|
||||
119851A6272F407B9F73741B /* WeatherKitBridge.m */,
|
||||
);
|
||||
name = ShenYanApp;
|
||||
sourceTree = "<group>";
|
||||
@@ -159,6 +169,7 @@
|
||||
files = (
|
||||
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3231D459AC31E2728D987F8E /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -245,6 +256,9 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
|
||||
16AA953C8FA640C8B4ACA19D /* HealthKitBridge.m in Sources */,
|
||||
5E73DDCAF25141D6A3511872 /* WeatherKitBridge.swift in Sources */,
|
||||
011F82AA74254221BC703CE1 /* WeatherKitBridge.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -257,10 +271,12 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = ShenYanApp/ShenYanApp.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = ZZMLW32PHH;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = ShenYanApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -271,9 +287,10 @@
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.shenyan.room;
|
||||
PRODUCT_NAME = ShenYanApp;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "ShenYanApp/ShenYanApp-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
@@ -287,9 +304,11 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = ShenYanApp/ShenYanApp.entitlements;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = ZZMLW32PHH;
|
||||
INFOPLIST_FILE = ShenYanApp/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@@ -300,9 +319,10 @@
|
||||
"-ObjC",
|
||||
"-lc++",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.shenyan.room;
|
||||
PRODUCT_NAME = ShenYanApp;
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "ShenYanApp/ShenYanApp-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
@@ -358,7 +378,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
@@ -370,6 +390,10 @@
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
OTHER_CFLAGS = (
|
||||
"$(inherited)",
|
||||
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||
);
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
@@ -377,8 +401,13 @@
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
@@ -424,7 +453,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
/usr/lib/swift,
|
||||
"$(inherited)",
|
||||
@@ -435,6 +464,10 @@
|
||||
"\"$(inherited)\"",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
OTHER_CFLAGS = (
|
||||
"$(inherited)",
|
||||
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||
);
|
||||
OTHER_CPLUSPLUSFLAGS = (
|
||||
"$(OTHER_CFLAGS)",
|
||||
"-DFOLLY_NO_CONFIG",
|
||||
@@ -442,8 +475,12 @@
|
||||
"-DFOLLY_USE_LIBCPP=1",
|
||||
"-DFOLLY_CFG_NO_COROUTINES=1",
|
||||
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
|
||||
"-DRCT_REMOVE_LEGACY_ARCH=1",
|
||||
);
|
||||
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
|
||||
USE_HERMES = true;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
|
||||
10
shenyan-app/ios/ShenYanApp.xcworkspace/contents.xcworkspacedata
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:ShenYanApp.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -1,53 +1,14 @@
|
||||
{
|
||||
"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" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
{ "filename" : "icon-20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" },
|
||||
{ "filename" : "icon-20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" },
|
||||
{ "filename" : "icon-29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" },
|
||||
{ "filename" : "icon-29@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" },
|
||||
{ "filename" : "icon-40@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" },
|
||||
{ "filename" : "icon-40@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" },
|
||||
{ "filename" : "icon-60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" },
|
||||
{ "filename" : "icon-60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" },
|
||||
{ "filename" : "icon-1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" }
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
"info" : { "author" : "xcode", "version" : 1 }
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 52 KiB |
@@ -28,14 +28,29 @@
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>101.36.73.102</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string></string>
|
||||
<key>NSHealthShareUsageDescription</key>
|
||||
<string>沈晏需要读取健康数据来了解你的身体状态</string>
|
||||
<key>NSHealthUpdateUsageDescription</key>
|
||||
<string>沈晏需要写入健康数据</string>
|
||||
<key>RCTNewArchEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
|
||||
@@ -4,5 +4,9 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.healthkit</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.weatherkit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
12586
shenyan-app/package-lock.json
generated
@@ -1,46 +1,37 @@
|
||||
{
|
||||
"name": "ShenYanApp",
|
||||
"version": "0.0.1",
|
||||
"name": "anchor-desktop",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"android": "react-native run-android",
|
||||
"ios": "react-native run-ios",
|
||||
"lint": "eslint .",
|
||||
"start": "react-native start",
|
||||
"test": "jest"
|
||||
"dev": "electron-vite dev",
|
||||
"build": "electron-vite build",
|
||||
"preview": "electron-vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@notifee/react-native": "^9.1.8",
|
||||
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||
"@react-native-community/blur": "^4.4.1",
|
||||
"@react-native/new-app-screen": "0.85.3",
|
||||
"axios": "^1.16.0",
|
||||
"react": "19.2.3",
|
||||
"react-native": "0.85.3",
|
||||
"react-native-safe-area-context": "^5.5.2"
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@babel/preset-env": "^7.25.3",
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"@react-native-community/cli": "20.1.0",
|
||||
"@react-native-community/cli-platform-android": "20.1.0",
|
||||
"@react-native-community/cli-platform-ios": "20.1.0",
|
||||
"@react-native/babel-preset": "0.85.3",
|
||||
"@react-native/eslint-config": "0.85.3",
|
||||
"@react-native/jest-preset": "0.85.3",
|
||||
"@react-native/metro-config": "0.85.3",
|
||||
"@react-native/typescript-config": "0.85.3",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
"eslint": "^8.19.0",
|
||||
"jest": "^29.6.3",
|
||||
"prettier": "2.8.8",
|
||||
"react-test-renderer": "19.2.3",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 22.11.0"
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^4.7.0",
|
||||
"autoprefixer": "^10.5.0",
|
||||
"clsx": "^2.1.1",
|
||||
"electron": "^41.3.0",
|
||||
"electron-vite": "^5.0.0",
|
||||
"eslint": "^10.2.1",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.5.0",
|
||||
"lucide-react": "^1.11.0",
|
||||
"postcss": "^8.5.12",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.2",
|
||||
"vite": "^7.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
57
shenyan-app/push-server.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const http = require('http');
|
||||
|
||||
const pending = [];
|
||||
|
||||
function parseBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', c => (body += c));
|
||||
req.on('end', () => {
|
||||
try {
|
||||
resolve(body ? JSON.parse(body) : {});
|
||||
} catch {
|
||||
reject(new Error('Invalid JSON'));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(204);
|
||||
return res.end();
|
||||
}
|
||||
|
||||
// POST /push/send — queue a notification
|
||||
if (req.method === 'POST' && req.url === '/push/send') {
|
||||
const { title, body } = await parseBody(req);
|
||||
const msg = {
|
||||
id: Date.now(),
|
||||
title: title || '沈晏',
|
||||
body: body || '',
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
pending.push(msg);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return res.end(JSON.stringify({ ok: true, queued: msg }));
|
||||
}
|
||||
|
||||
// GET /push/poll — fetch pending messages, then clear
|
||||
if (req.method === 'GET' && req.url === '/push/poll') {
|
||||
const snapshot = [...pending];
|
||||
pending.length = 0;
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
return res.end(JSON.stringify({ messages: snapshot }));
|
||||
}
|
||||
|
||||
res.writeHead(404);
|
||||
res.end('not found');
|
||||
});
|
||||
|
||||
server.listen(3002, () => {
|
||||
console.log('push-broker on http://localhost:3002');
|
||||
});
|
||||
@@ -1,516 +1,122 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import PushNotificationIOS from '@react-native-community/push-notification-ios';
|
||||
import { requestHealthAuth, getHealthSnapshot } from './health';
|
||||
import { getCurrentWeather } from './weather';
|
||||
import {
|
||||
Animated,
|
||||
FlatList,
|
||||
Image,
|
||||
KeyboardAvoidingView,
|
||||
Modal,
|
||||
Platform,
|
||||
StatusBar,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
Vibration,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useState } from 'react'
|
||||
import reactLogo from './assets/react.svg'
|
||||
import viteLogo from './assets/vite.svg'
|
||||
import heroImg from './assets/hero.png'
|
||||
import './App.css'
|
||||
|
||||
type Status = 'away' | 'online' | 'busy';
|
||||
type Screen = 'home' | 'chat';
|
||||
|
||||
interface Msg {
|
||||
id: string;
|
||||
role: 'user' | 'shenyan';
|
||||
text: string;
|
||||
}
|
||||
|
||||
const IMAGES: Record<Status, ReturnType<typeof require>> = {
|
||||
away: require('./assets/away.png'),
|
||||
online: require('./assets/online.png'),
|
||||
busy: require('./assets/busy.png'),
|
||||
};
|
||||
|
||||
const CHAT_BG = require('./assets/chat-bg.png');
|
||||
const PAPER_BG = require('./assets/paper.png');
|
||||
const HEALTH_URL = 'http://101.36.73.102/health';
|
||||
const BRIDGE_URL = 'http://192.168.31.51:3004';
|
||||
const POLL_INTERVAL = 8000;
|
||||
const PUSH_POLL_INTERVAL = 6000;
|
||||
|
||||
export default function App() {
|
||||
const [screen, setScreen] = useState<Screen>('home');
|
||||
const [status, setStatus] = useState<Status>('away');
|
||||
const [banner, setBanner] = useState<{ title: string; body: string } | null>(null);
|
||||
const bannerAnim = useRef(new Animated.Value(-120)).current;
|
||||
const bannerTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const lastTap = useRef(0);
|
||||
const [fridgeOpen, setFridgeOpen] = useState(false);
|
||||
const [notes, setNotes] = useState<any[]>([]);
|
||||
const [replyTexts, setReplyTexts] = useState<Record<number, string>>({});
|
||||
const lastSeenNoteRef = useRef(0);
|
||||
|
||||
const unreadCount = notes.filter(n => n.id > lastSeenNoteRef.current).length;
|
||||
|
||||
const openFridge = useCallback(() => {
|
||||
if (notes.length > 0) lastSeenNoteRef.current = Math.max(...notes.map(n => n.id));
|
||||
setFridgeOpen(true);
|
||||
}, [notes]);
|
||||
|
||||
const showBanner = useCallback((title: string, body: string) => {
|
||||
Vibration.vibrate(400);
|
||||
setBanner({ title, body });
|
||||
Animated.spring(bannerAnim, { toValue: 0, useNativeDriver: true }).start();
|
||||
clearTimeout(bannerTimer.current);
|
||||
bannerTimer.current = setTimeout(() => {
|
||||
Animated.timing(bannerAnim, { toValue: -120, duration: 300, useNativeDriver: true }).start(() => setBanner(null));
|
||||
}, 3500);
|
||||
}, [bannerAnim]);
|
||||
|
||||
const knock = useCallback(() => {
|
||||
const now = Date.now();
|
||||
if (now - lastTap.current < 350) {
|
||||
lastTap.current = 0;
|
||||
setScreen('chat');
|
||||
fetch(`${BRIDGE_URL}/knock`, { method: 'POST' }).catch(() => {});
|
||||
} else {
|
||||
lastTap.current = now;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkHealth = useCallback(async () => {
|
||||
try {
|
||||
const c = new AbortController();
|
||||
const t = setTimeout(() => c.abort(), 5000);
|
||||
const r = await fetch(HEALTH_URL, { signal: c.signal });
|
||||
clearTimeout(t);
|
||||
setStatus(r.ok ? 'online' : 'away');
|
||||
} catch {
|
||||
setStatus('away');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkHealth();
|
||||
const id = setInterval(checkHealth, POLL_INTERVAL);
|
||||
return () => clearInterval(id);
|
||||
}, [checkHealth]);
|
||||
|
||||
// APNs token registration — PushNotificationIOS native
|
||||
useEffect(() => {
|
||||
const registerToken = async (token: string) => {
|
||||
try {
|
||||
await fetch(`${BRIDGE_URL}/register-device`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ token }),
|
||||
});
|
||||
} catch {}
|
||||
};
|
||||
const regSub = PushNotificationIOS.addEventListener('register', (token: string) => {
|
||||
registerToken(token);
|
||||
});
|
||||
// tap notification → go straight to chat
|
||||
// check if launched from notification tap
|
||||
PushNotificationIOS.getInitialNotification().then((notif: any) => {
|
||||
if (notif) setScreen('chat');
|
||||
});
|
||||
const notifSub = PushNotificationIOS.addEventListener('notification', () => {
|
||||
setScreen('chat');
|
||||
});
|
||||
// clear badge when app becomes active
|
||||
const handleActive = () => { PushNotificationIOS.setApplicationIconBadgeNumber(0); };
|
||||
const { AppState } = require('react-native');
|
||||
const stateSub = AppState.addEventListener('change', (s: string) => {
|
||||
if (s === 'active') handleActive();
|
||||
});
|
||||
PushNotificationIOS.requestPermissions();
|
||||
return () => {
|
||||
try { regSub.remove(); notifSub.remove(); stateSub.remove(); } catch {}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// HealthKit authorization + periodic health snapshot
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await requestHealthAuth();
|
||||
// collect health snapshot every 10 minutes
|
||||
const collect = async () => {
|
||||
const [snap, weather] = await Promise.all([
|
||||
getHealthSnapshot(),
|
||||
getCurrentWeather(),
|
||||
]);
|
||||
if (snap.heartRate || snap.steps > 0 || weather) {
|
||||
const payload = { ...snap, weather };
|
||||
fetch(`${BRIDGE_URL}/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: `[health] ${JSON.stringify(payload)}`, type: 'health' }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
};
|
||||
collect(); // first run
|
||||
const id = setInterval(collect, 600000); // every 10 min
|
||||
return () => clearInterval(id);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// push polling with banner + vibration
|
||||
useEffect(() => {
|
||||
let lastPushId = 0;
|
||||
const pollPush = async () => {
|
||||
try {
|
||||
const r = await fetch(`${BRIDGE_URL}/poll?since=${lastPushId}&type=push`);
|
||||
const entries = await r.json();
|
||||
if (Array.isArray(entries) && entries.length > 0) {
|
||||
for (const e of entries) {
|
||||
if (e.id > lastPushId) lastPushId = e.id;
|
||||
showBanner(e.title || '沈晏', e.body || '');
|
||||
}
|
||||
fetch(`${BRIDGE_URL}/push/clear`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ids: entries.map((e: any) => e.id) }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
const id = setInterval(pollPush, PUSH_POLL_INTERVAL);
|
||||
return () => clearInterval(id);
|
||||
}, [showBanner]);
|
||||
|
||||
// fridge notes polling
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const r = await fetch(`${BRIDGE_URL}/notes`);
|
||||
const data = await r.json();
|
||||
if (Array.isArray(data)) setNotes(data);
|
||||
} catch {}
|
||||
};
|
||||
poll();
|
||||
const id = setInterval(poll, 10000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const replyToNote = async (noteId: number) => {
|
||||
const text = replyTexts[noteId] || '';
|
||||
if (!text.trim()) return;
|
||||
try {
|
||||
await fetch(`${BRIDGE_URL}/notes/reply`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ noteId, text: text.trim() }),
|
||||
});
|
||||
setReplyTexts(prev => { const n = {...prev}; delete n[noteId]; return n; });
|
||||
const r = await fetch(`${BRIDGE_URL}/notes`);
|
||||
const data = await r.json();
|
||||
if (Array.isArray(data)) setNotes(data);
|
||||
} catch {}
|
||||
};
|
||||
function App() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
return (
|
||||
<View style={s.root}>
|
||||
<StatusBar barStyle="light-content" backgroundColor="#000" />
|
||||
{screen === 'chat' ? (
|
||||
<ChatScreen onBack={() => setScreen('home')} />
|
||||
) : (
|
||||
<>
|
||||
<Image source={IMAGES[status]} style={s.statusImg} resizeMode="cover" />
|
||||
<Text style={s.statusLabel}>{LABELS[status]}</Text>
|
||||
<>
|
||||
<section id="center">
|
||||
<div className="hero">
|
||||
<img src={heroImg} className="base" width="170" height="179" alt="" />
|
||||
<img src={reactLogo} className="framework" alt="React logo" />
|
||||
<img src={viteLogo} className="vite" alt="Vite logo" />
|
||||
</div>
|
||||
<div>
|
||||
<h1>Get started</h1>
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to test <code>HMR</code>
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="counter"
|
||||
onClick={() => setCount((count) => count + 1)}
|
||||
>
|
||||
Count is {count}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* 三区布局 */}
|
||||
<View style={s.homeRow}>
|
||||
{/* 框1 — 敲门区域 */}
|
||||
<TouchableOpacity style={s.zone1} onPress={knock} activeOpacity={1} />
|
||||
<div className="ticks"></div>
|
||||
|
||||
{/* 框2 — 报告/memory 竖条 */}
|
||||
<TouchableOpacity style={s.zone2} activeOpacity={0.6} onPress={() => {}}>
|
||||
<Text style={s.zone2Label}>记</Text>
|
||||
<Text style={s.zone2Label}>录</Text>
|
||||
</TouchableOpacity>
|
||||
<section id="next-steps">
|
||||
<div id="docs">
|
||||
<svg className="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#documentation-icon"></use>
|
||||
</svg>
|
||||
<h2>Documentation</h2>
|
||||
<p>Your questions, answered</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://vite.dev/" target="_blank">
|
||||
<img className="logo" src={viteLogo} alt="" />
|
||||
Explore Vite
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://react.dev/" target="_blank">
|
||||
<img className="button-icon" src={reactLogo} alt="" />
|
||||
Learn more
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="social">
|
||||
<svg className="icon" role="presentation" aria-hidden="true">
|
||||
<use href="/icons.svg#social-icon"></use>
|
||||
</svg>
|
||||
<h2>Connect with us</h2>
|
||||
<p>Join the Vite community</p>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="https://github.com/vitejs/vite" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#github-icon"></use>
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://chat.vite.dev/" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#discord-icon"></use>
|
||||
</svg>
|
||||
Discord
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://x.com/vite_js" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#x-icon"></use>
|
||||
</svg>
|
||||
X.com
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://bsky.app/profile/vite.dev" target="_blank">
|
||||
<svg
|
||||
className="button-icon"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<use href="/icons.svg#bluesky-icon"></use>
|
||||
</svg>
|
||||
Bluesky
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 框3 — 冰箱贴 */}
|
||||
<TouchableOpacity style={s.zone3} onPress={openFridge} activeOpacity={0.6}>
|
||||
<Text style={s.fridgeLabel}>冰箱贴</Text>
|
||||
{unreadCount > 0 && <View style={s.fridgeBadge}><Text style={s.fridgeBadgeText}>{unreadCount}</Text></View>}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* fridge modal */}
|
||||
<Modal visible={fridgeOpen} animationType="slide" transparent>
|
||||
<View style={s.fridgeModal}>
|
||||
<Image source={IMAGES[status]} style={s.fridgeBg} resizeMode="cover" />
|
||||
<View style={s.fridgeHeader}>
|
||||
<Text style={s.fridgeTitle}>冰箱贴</Text>
|
||||
<TouchableOpacity onPress={() => setFridgeOpen(false)}>
|
||||
<Text style={s.fridgeClose}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<KeyboardAvoidingView style={{ flex: 1 }} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<FlatList
|
||||
data={notes}
|
||||
keyExtractor={n => String(n.id)}
|
||||
style={s.fridgeList}
|
||||
renderItem={({ item }) => (
|
||||
<View style={s.noteCard}>
|
||||
<Image source={PAPER_BG} style={s.paperBg} resizeMode="cover" />
|
||||
{item.title ? <Text style={s.noteTitle}>{item.title}</Text> : null}
|
||||
<Text style={s.noteBody}>{item.body}</Text>
|
||||
{item.replies && item.replies.length > 0 && (
|
||||
<View style={s.noteReplies}>
|
||||
{item.replies.map((r: any, i: number) => (
|
||||
<Text key={i} style={s.noteReplyText}>→ {r.text}</Text>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
<View style={s.noteInputRow}>
|
||||
<TextInput
|
||||
style={s.noteInput}
|
||||
placeholder="眠眠回复..."
|
||||
placeholderTextColor="rgba(0,0,0,0.25)"
|
||||
value={replyTexts[item.id] || ''}
|
||||
onChangeText={t => setReplyTexts(prev => ({...prev, [item.id]: t}))}
|
||||
onSubmitEditing={() => replyToNote(item.id)}
|
||||
/>
|
||||
<TouchableOpacity style={s.noteSend} onPress={() => replyToNote(item.id)}>
|
||||
<Text style={s.noteSendText}>回</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
</Modal>
|
||||
|
||||
{banner && (
|
||||
<Animated.View style={[s.banner, { transform: [{ translateY: bannerAnim }] }]}>
|
||||
<TouchableOpacity style={s.bannerContent} onPress={() => {
|
||||
Animated.timing(bannerAnim, { toValue: -120, duration: 300, useNativeDriver: true }).start(() => setBanner(null));
|
||||
if (screen === 'home') setScreen('chat');
|
||||
}}>
|
||||
<Text style={s.bannerTitle}>{banner.title}</Text>
|
||||
<Text style={s.bannerBody}>{banner.body}</Text>
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
<div className="ticks"></div>
|
||||
<section id="spacer"></section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MSG_STORE_KEY = 'shenyan-chat-msgs';
|
||||
|
||||
function ChatScreen({ onBack }: { onBack: () => void }) {
|
||||
const [text, setText] = useState('');
|
||||
const [msgs, setMsgs] = useState<Msg[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const listRef = useRef<FlatList<Msg>>(null);
|
||||
const lastResponseId = useRef(0);
|
||||
const msgsRef = useRef<Msg[]>([]);
|
||||
|
||||
// keep ref in sync for saving
|
||||
useEffect(() => { msgsRef.current = msgs; }, [msgs]);
|
||||
|
||||
// load saved messages + bridge history on mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(MSG_STORE_KEY);
|
||||
if (raw) {
|
||||
const saved: Msg[] = JSON.parse(raw);
|
||||
setMsgs(saved);
|
||||
for (const m of saved) {
|
||||
if (m.role === 'shenyan' && m.id.startsWith('s-')) {
|
||||
const bridgeId = parseInt(m.id.slice(2), 10);
|
||||
if (bridgeId > lastResponseId.current) lastResponseId.current = bridgeId;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// first load: pull full history from bridge
|
||||
const r = await fetch(`${BRIDGE_URL}/history`);
|
||||
const history: Msg[] = await r.json();
|
||||
if (Array.isArray(history) && history.length > 0) {
|
||||
setMsgs(history);
|
||||
for (const m of history) {
|
||||
if (m.role === 'shenyan' && m.id.startsWith('s-')) {
|
||||
const bridgeId = parseInt(m.id.slice(2), 10);
|
||||
if (bridgeId > lastResponseId.current) lastResponseId.current = bridgeId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {} finally { setLoaded(true); }
|
||||
})();
|
||||
}, []);
|
||||
|
||||
// persist messages on change (debounced)
|
||||
useEffect(() => {
|
||||
if (!loaded) return;
|
||||
const t = setTimeout(() => {
|
||||
AsyncStorage.setItem(MSG_STORE_KEY, JSON.stringify(msgsRef.current.slice(-100))).catch(() => {});
|
||||
}, 500);
|
||||
return () => clearTimeout(t);
|
||||
}, [msgs, loaded]);
|
||||
|
||||
// auto-scroll to bottom: use onContentSizeChange for reliable snap
|
||||
const scrollToBottom = useCallback(() => {
|
||||
listRef.current?.scrollToEnd({ animated: false });
|
||||
}, []);
|
||||
|
||||
const onContentSizeChange = useCallback(() => {
|
||||
scrollToBottom();
|
||||
}, [scrollToBottom]);
|
||||
|
||||
// poll for claude responses
|
||||
useEffect(() => {
|
||||
const poll = async () => {
|
||||
try {
|
||||
const r = await fetch(`${BRIDGE_URL}/poll?since=${lastResponseId.current}`);
|
||||
const entries = await r.json();
|
||||
if (Array.isArray(entries)) {
|
||||
for (const e of entries) {
|
||||
const isNew = e.id > lastResponseId.current;
|
||||
if (isNew) lastResponseId.current = e.id;
|
||||
setMsgs(prev => {
|
||||
const exists = prev.find(m => m.id === `s-${e.id}`);
|
||||
if (exists) return prev;
|
||||
return [...prev, { id: `s-${e.id}`, role: 'shenyan', text: e.text }];
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
};
|
||||
const id = setInterval(poll, 2000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
|
||||
const send = useCallback(async () => {
|
||||
const t = text.trim();
|
||||
if (!t) return;
|
||||
const msgId = `u-${Date.now()}`;
|
||||
setMsgs(prev => [...prev, { id: msgId, role: 'user', text: t }]);
|
||||
setText('');
|
||||
// send to Mac bridge (Claude Code session)
|
||||
fetch(`${BRIDGE_URL}/send`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: t }),
|
||||
}).catch(() => {});
|
||||
}, [text]);
|
||||
|
||||
const renderItem = useCallback(({ item }: { item: Msg }) => (
|
||||
<View style={[s.msgBubble, item.role === 'user' ? s.msgRight : s.msgLeft]}>
|
||||
<Text style={s.msgText}>{item.text}</Text>
|
||||
</View>
|
||||
), []);
|
||||
|
||||
return (
|
||||
<View style={s.root}>
|
||||
<Image source={CHAT_BG} style={s.chatBg} resizeMode="cover" />
|
||||
<StatusBar barStyle="light-content" backgroundColor="transparent" translucent />
|
||||
<TouchableOpacity style={s.backDot} onPress={onBack} activeOpacity={0.6} />
|
||||
<FlatList
|
||||
ref={listRef}
|
||||
data={msgs}
|
||||
keyExtractor={m => m.id}
|
||||
renderItem={renderItem}
|
||||
style={s.msgList}
|
||||
contentContainerStyle={s.msgContent}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
maintainVisibleContentPosition={{ minIndexForVisible: 0 }}
|
||||
/>
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
|
||||
<View style={s.inputRow}>
|
||||
<TextInput
|
||||
style={s.input}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
placeholder="输入"
|
||||
placeholderTextColor="rgba(255,255,255,0.3)"
|
||||
multiline
|
||||
onSubmitEditing={send}
|
||||
blurOnSubmit={false}
|
||||
/>
|
||||
<TouchableOpacity style={s.sendBtn} onPress={send} activeOpacity={0.5} />
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const LABELS: Record<Status, string> = { away: '不在', online: '在线', busy: '忙碌' };
|
||||
|
||||
const s = StyleSheet.create({
|
||||
root: { flex: 1, backgroundColor: '#000' },
|
||||
statusImg: { width: '100%', height: '100%', position: 'absolute' },
|
||||
statusLabel: { position: 'absolute', bottom: 60, alignSelf: 'center', fontFamily: 'Inter', fontSize: 14, color: 'rgba(255,255,255,0.3)' },
|
||||
|
||||
// 三区布局
|
||||
homeRow: { flex: 1, flexDirection: 'row', paddingTop: 80, paddingBottom: 80 },
|
||||
// 框1 — 敲门 (左 50%)
|
||||
zone1: { flex: 5, height: '100%' },
|
||||
// 框2 — 记录竖条 (窄,无视觉)
|
||||
zone2: { width: 64, height: '60%', alignSelf: 'center', justifyContent: 'center', alignItems: 'center', marginHorizontal: 8 },
|
||||
zone2Label: { fontFamily: 'Inter', fontSize: 10, color: 'rgba(255,255,255,0.12)', marginVertical: 2 },
|
||||
// 框3 — 冰箱贴 (右,无视觉)
|
||||
zone3: { flex: 2.7, height: '62%', alignSelf: 'center', justifyContent: 'center', alignItems: 'center', marginRight: 12 },
|
||||
fridgeLabel: { fontFamily: 'Inter', fontSize: 11, color: 'rgba(255,255,255,0.12)' },
|
||||
fridgeBadge: { position: 'absolute', top: -4, right: -4, width: 16, height: 16, borderRadius: 8, backgroundColor: 'rgba(255,80,80,0.5)', justifyContent: 'center', alignItems: 'center' },
|
||||
fridgeBadgeText: { fontFamily: 'Inter', fontSize: 9, color: '#FFF' },
|
||||
fridgeModal: { flex: 1, paddingTop: 60 },
|
||||
fridgeBg: { width: '100%', height: '100%', position: 'absolute' },
|
||||
fridgeHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 20, paddingBottom: 16, borderBottomWidth: 0.5, borderBottomColor: 'rgba(255,255,255,0.1)' },
|
||||
fridgeTitle: { fontFamily: 'Inter', fontSize: 20, fontWeight: '600', color: '#FFF' },
|
||||
fridgeClose: { fontFamily: 'Inter', fontSize: 22, color: 'rgba(255,255,255,0.4)', padding: 8 },
|
||||
fridgeList: { flex: 1, paddingHorizontal: 16, paddingTop: 16 },
|
||||
noteCard: { borderRadius: 12, marginBottom: 12, padding: 14, overflow: 'hidden' },
|
||||
paperBg: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, opacity: 0.6 },
|
||||
noteTitle: { fontFamily: "Inter", fontSize: 12, fontWeight: "600", color: "rgba(0,0,0,0.4)", marginBottom: 6, zIndex: 1 },
|
||||
noteBody: { fontFamily: "Inter", fontSize: 15, color: "rgba(0,0,0,0.8)", lineHeight: 22, zIndex: 1 },
|
||||
noteReplies: { marginTop: 10, zIndex: 1, paddingTop: 10, borderTopWidth: 0.5, borderTopColor: 'rgba(0,0,0,0.1)' },
|
||||
noteReplyText: { fontFamily: 'Inter', fontSize: 13, color: 'rgba(0,0,0,0.35)', marginBottom: 4 },
|
||||
noteInputRow: { flexDirection: 'row', alignItems: 'center', marginTop: 12, zIndex: 1 },
|
||||
noteInput: { flex: 1, fontFamily: 'Inter', fontSize: 14, color: '#000', backgroundColor: 'rgba(0,0,0,0.06)', borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8 },
|
||||
noteSend: { width: 32, height: 32, borderRadius: 16, backgroundColor: 'rgba(0,0,0,0.08)', justifyContent: 'center', alignItems: 'center', marginLeft: 8 },
|
||||
noteSendText: { fontFamily: 'Inter', fontSize: 13, color: 'rgba(0,0,0,0.4)' },
|
||||
|
||||
chatBg: { width: '100%', height: '100%', position: 'absolute' },
|
||||
backDot: { position: 'absolute', top: 60, left: 20, width: 20, height: 20, borderRadius: 10, backgroundColor: '#FFF', zIndex: 10 },
|
||||
msgList: { flex: 1 },
|
||||
msgContent: { paddingTop: 100, paddingHorizontal: 16, paddingBottom: 20 },
|
||||
msgBubble: { maxWidth: '82%', paddingHorizontal: 14, paddingVertical: 8, borderRadius: 16, marginBottom: 8 },
|
||||
msgRight: {
|
||||
alignSelf: 'flex-end',
|
||||
backgroundColor: 'rgba(255,255,255,0.25)',
|
||||
borderTopRightRadius: 4,
|
||||
},
|
||||
msgLeft: {
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'rgba(255,255,255,0.25)',
|
||||
borderTopLeftRadius: 4,
|
||||
},
|
||||
msgText: { fontFamily: 'Inter', fontSize: 16, color: '#FFF' },
|
||||
|
||||
inputRow: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12, paddingBottom: 12, paddingTop: 8 },
|
||||
input: { flex: 1, fontFamily: 'Inter', fontSize: 16, color: '#FFF', backgroundColor: 'rgba(255,255,255,0.18)', borderRadius: 20, paddingHorizontal: 16, paddingVertical: 10, maxHeight: 100 },
|
||||
sendBtn: { width: 36, height: 36, borderRadius: 18, backgroundColor: 'rgba(255,255,255,0.4)', marginLeft: 10 },
|
||||
|
||||
// banner styles
|
||||
banner: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100, paddingTop: 54, paddingHorizontal: 12 },
|
||||
bannerContent: {
|
||||
backgroundColor: 'rgba(30,30,30,0.95)',
|
||||
borderRadius: 14,
|
||||
padding: 16,
|
||||
borderWidth: 0.5,
|
||||
borderColor: 'rgba(255,255,255,0.25)',
|
||||
},
|
||||
bannerTitle: { fontFamily: 'Inter', fontSize: 13, fontWeight: '600', color: 'rgba(255,255,255,0.7)', marginBottom: 4 },
|
||||
bannerBody: { fontFamily: 'Inter', fontSize: 15, color: '#FFF', lineHeight: 20 },
|
||||
});
|
||||
export default App
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"extends": "@react-native/typescript-config",
|
||||
"compilerOptions": {
|
||||
"types": ["jest"]
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["**/node_modules", "**/Pods"]
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
|
||||