更新 shenyan-app: 冰箱贴独立输入框 + push-server + APNs图标 + 完整.gitignore

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fyah
2026-05-16 20:15:17 +08:00
parent 2a053ab88e
commit 52443bffc2
24 changed files with 5167 additions and 10734 deletions

View File

@@ -43,11 +43,6 @@ yarn-error.log
# fastlane # 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/report.xml
**/fastlane/Preview.html **/fastlane/Preview.html
**/fastlane/screenshots **/fastlane/screenshots
@@ -73,3 +68,8 @@ yarn-error.log
!.yarn/releases !.yarn/releases
!.yarn/sdks !.yarn/sdks
!.yarn/versions !.yarn/versions
# bridge-local (contains APNs keys and local config)
bridge-local/node_modules/
bridge-local/*.p8
bridge-local/apns-key-page.png

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<network-security-config> <network-security-config>
<domain-config cleartextTrafficPermitted="true"> <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 includeSubdomains="false">101.36.73.102</domain>
</domain-config> </domain-config>
</network-security-config> </network-security-config>

154
shenyan-app/bridge-local/package-lock.json generated Normal file
View 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"
}
}
}
}

View 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

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,10 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0C80B921A6F3F58F76C31292 /* libPods-ShenYanApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ShenYanApp.a */; }; 0C80B921A6F3F58F76C31292 /* libPods-ShenYanApp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DCACB8F33CDC322A6C60F78 /* libPods-ShenYanApp.a */; };
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 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 */; }; 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 761780EC2CA45674006654EE /* AppDelegate.swift */; };
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; }; 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@@ -18,6 +22,9 @@
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ShenYanApp/Images.xcassets; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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 */, 13B07FB61A68108700A75B9A /* Info.plist */,
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */, 81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */, 13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
DD038106F9A04B6F9B117189 /* HealthKitBridge.m */,
EA8B565A2BF34A1AA5C637AD /* WeatherKitBridge.swift */,
119851A6272F407B9F73741B /* WeatherKitBridge.m */,
); );
name = ShenYanApp; name = ShenYanApp;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -159,6 +169,7 @@
files = ( files = (
81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */, 81AB9BB82411601600AC10FF /* LaunchScreen.storyboard in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
3231D459AC31E2728D987F8E /* PrivacyInfo.xcprivacy in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -245,6 +256,9 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
761780ED2CA45674006654EE /* AppDelegate.swift in Sources */, 761780ED2CA45674006654EE /* AppDelegate.swift in Sources */,
16AA953C8FA640C8B4ACA19D /* HealthKitBridge.m in Sources */,
5E73DDCAF25141D6A3511872 /* WeatherKitBridge.swift in Sources */,
011F82AA74254221BC703CE1 /* WeatherKitBridge.m in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -257,10 +271,12 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = ShenYanApp/ShenYanApp.entitlements;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZZMLW32PHH;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = ShenYanApp/Info.plist; INFOPLIST_FILE = ShenYanApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -271,9 +287,10 @@
"-ObjC", "-ObjC",
"-lc++", "-lc++",
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = com.shenyan.room;
PRODUCT_NAME = ShenYanApp; PRODUCT_NAME = ShenYanApp;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_OBJC_BRIDGING_HEADER = "ShenYanApp/ShenYanApp-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
@@ -287,9 +304,11 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = ShenYanApp/ShenYanApp.entitlements;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = ZZMLW32PHH;
INFOPLIST_FILE = ShenYanApp/Info.plist; INFOPLIST_FILE = ShenYanApp/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@@ -300,9 +319,10 @@
"-ObjC", "-ObjC",
"-lc++", "-lc++",
); );
PRODUCT_BUNDLE_IDENTIFIER = "org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)"; PRODUCT_BUNDLE_IDENTIFIER = com.shenyan.room;
PRODUCT_NAME = ShenYanApp; PRODUCT_NAME = ShenYanApp;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SWIFT_OBJC_BRIDGING_HEADER = "ShenYanApp/ShenYanApp-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
@@ -358,7 +378,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift, /usr/lib/swift,
"$(inherited)", "$(inherited)",
@@ -370,6 +390,10 @@
); );
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = ( OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)", "$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG", "-DFOLLY_NO_CONFIG",
@@ -377,8 +401,13 @@
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
); );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
}; };
name = Debug; name = Debug;
}; };
@@ -424,7 +453,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.1; IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
/usr/lib/swift, /usr/lib/swift,
"$(inherited)", "$(inherited)",
@@ -435,6 +464,10 @@
"\"$(inherited)\"", "\"$(inherited)\"",
); );
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = (
"$(inherited)",
"-DRCT_REMOVE_LEGACY_ARCH=1",
);
OTHER_CPLUSPLUSFLAGS = ( OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)", "$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG", "-DFOLLY_NO_CONFIG",
@@ -442,8 +475,12 @@
"-DFOLLY_USE_LIBCPP=1", "-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1", "-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1", "-DFOLLY_HAVE_CLOCK_GETTIME=1",
"-DRCT_REMOVE_LEGACY_ARCH=1",
); );
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos; SDKROOT = iphoneos;
SWIFT_ENABLE_EXPLICIT_MODULES = NO;
USE_HERMES = true;
VALIDATE_PRODUCT = YES; VALIDATE_PRODUCT = YES;
}; };
name = Release; name = Release;

View 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>

View File

@@ -1,53 +1,14 @@
{ {
"images" : [ "images" : [
{ { "filename" : "icon-20@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" },
"idiom" : "iphone", { "filename" : "icon-20@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" },
"scale" : "2x", { "filename" : "icon-29@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" },
"size" : "20x20" { "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" },
"idiom" : "iphone", { "filename" : "icon-60@2x.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" },
"scale" : "3x", { "filename" : "icon-60@3x.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" },
"size" : "20x20" { "filename" : "icon-1024.png", "idiom" : "ios-marketing", "scale" : "1x", "size" : "1024x1024" }
},
{
"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"
}
], ],
"info" : { "info" : { "author" : "xcode", "version" : 1 }
"author" : "xcode",
"version" : 1
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@@ -28,14 +28,29 @@
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<!-- Do not change NSAllowsArbitraryLoads to true, or you will risk app rejection! -->
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<false/> <false/>
<key>NSAllowsLocalNetworking</key> <key>NSAllowsLocalNetworking</key>
<true/> <true/>
<key>NSExceptionDomains</key>
<dict>
<key>101.36.73.102</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<false/>
</dict>
</dict>
</dict> </dict>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string></string> <string></string>
<key>NSHealthShareUsageDescription</key>
<string>沈晏需要读取健康数据来了解你的身体状态</string>
<key>NSHealthUpdateUsageDescription</key>
<string>沈晏需要写入健康数据</string>
<key>RCTNewArchEnabled</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>

View File

@@ -4,5 +4,9 @@
<dict> <dict>
<key>aps-environment</key> <key>aps-environment</key>
<string>development</string> <string>development</string>
<key>com.apple.developer.healthkit</key>
<true/>
<key>com.apple.developer.weatherkit</key>
<true/>
</dict> </dict>
</plist> </plist>

File diff suppressed because it is too large Load Diff

View File

@@ -1,46 +1,37 @@
{ {
"name": "ShenYanApp", "name": "anchor-desktop",
"version": "0.0.1",
"private": true, "private": true,
"version": "0.0.0",
"type": "module",
"scripts": { "scripts": {
"android": "react-native run-android", "dev": "electron-vite dev",
"ios": "react-native run-ios", "build": "electron-vite build",
"lint": "eslint .", "preview": "electron-vite preview"
"start": "react-native start",
"test": "jest"
}, },
"dependencies": { "dependencies": {
"@notifee/react-native": "^9.1.8", "react": "^19.2.5",
"@react-native-async-storage/async-storage": "^2.2.0", "react-dom": "^19.2.5"
"@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"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@eslint/js": "^10.0.1",
"@babel/preset-env": "^7.25.3", "@types/node": "^24.12.2",
"@babel/runtime": "^7.25.0", "@types/react": "^19.2.14",
"@react-native-community/cli": "20.1.0", "@types/react-dom": "^19.2.3",
"@react-native-community/cli-platform-android": "20.1.0", "@vitejs/plugin-react": "^4.7.0",
"@react-native-community/cli-platform-ios": "20.1.0", "autoprefixer": "^10.5.0",
"@react-native/babel-preset": "0.85.3", "clsx": "^2.1.1",
"@react-native/eslint-config": "0.85.3", "electron": "^41.3.0",
"@react-native/jest-preset": "0.85.3", "electron-vite": "^5.0.0",
"@react-native/metro-config": "0.85.3", "eslint": "^10.2.1",
"@react-native/typescript-config": "0.85.3", "eslint-plugin-react-hooks": "^7.1.1",
"@types/jest": "^29.5.13", "eslint-plugin-react-refresh": "^0.5.2",
"@types/react": "^19.2.0", "globals": "^17.5.0",
"@types/react-test-renderer": "^19.1.0", "lucide-react": "^1.11.0",
"eslint": "^8.19.0", "postcss": "^8.5.12",
"jest": "^29.6.3", "tailwind-merge": "^3.5.0",
"prettier": "2.8.8", "tailwindcss": "^4.2.4",
"react-test-renderer": "19.2.3", "typescript": "~6.0.2",
"typescript": "^5.8.3" "typescript-eslint": "^8.58.2",
}, "vite": "^7.3.2"
"engines": {
"node": ">= 22.11.0"
} }
} }

View 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');
});

View File

@@ -1,516 +1,122 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useState } from 'react'
import AsyncStorage from '@react-native-async-storage/async-storage'; import reactLogo from './assets/react.svg'
import PushNotificationIOS from '@react-native-community/push-notification-ios'; import viteLogo from './assets/vite.svg'
import { requestHealthAuth, getHealthSnapshot } from './health'; import heroImg from './assets/hero.png'
import { getCurrentWeather } from './weather'; import './App.css'
import {
Animated,
FlatList,
Image,
KeyboardAvoidingView,
Modal,
Platform,
StatusBar,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
Vibration,
View,
} from 'react-native';
type Status = 'away' | 'online' | 'busy'; function App() {
type Screen = 'home' | 'chat'; const [count, setCount] = useState(0)
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 {}
};
return ( 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" /> <section id="center">
<Text style={s.statusLabel}>{LABELS[status]}</Text> <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>
{/* 三区布局 */} <div className="ticks"></div>
<View style={s.homeRow}>
{/* 框1 — 敲门区域 */}
<TouchableOpacity style={s.zone1} onPress={knock} activeOpacity={1} />
{/* 框2 — 报告/memory 竖条 */} <section id="next-steps">
<TouchableOpacity style={s.zone2} activeOpacity={0.6} onPress={() => {}}> <div id="docs">
<Text style={s.zone2Label}></Text> <svg className="icon" role="presentation" aria-hidden="true">
<Text style={s.zone2Label}></Text> <use href="/icons.svg#documentation-icon"></use>
</TouchableOpacity> </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 — 冰箱贴 */} <div className="ticks"></div>
<TouchableOpacity style={s.zone3} onPress={openFridge} activeOpacity={0.6}> <section id="spacer"></section>
<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>
);
} }
const MSG_STORE_KEY = 'shenyan-chat-msgs'; export default App
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 },
});

View File

@@ -1,8 +1,7 @@
{ {
"extends": "@react-native/typescript-config", "files": [],
"compilerOptions": { "references": [
"types": ["jest"] { "path": "./tsconfig.app.json" },
}, { "path": "./tsconfig.node.json" }
"include": ["**/*.ts", "**/*.tsx"], ]
"exclude": ["**/node_modules", "**/Pods"]
} }