Compare commits
1 Commits
dev
...
cf466a3959
| Author | SHA1 | Date | |
|---|---|---|---|
| cf466a3959 |
@@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); deps={**d.get\\('dependencies',{}\\), **d.get\\('devDependencies',{}\\)}; [print\\(f'{k}: {v}'\\) for k,v in deps.items\\(\\) if 'navigation' in k or 'tab' in k.lower\\(\\)]\")",
|
|
||||||
"Bash(npx tsc *)",
|
|
||||||
"Bash(echo \"EXIT: $?\")",
|
|
||||||
"Bash(npm view *)",
|
|
||||||
"Bash(npx patch-package *)",
|
|
||||||
"Bash(rm -rf node_modules/expo-router)",
|
|
||||||
"Bash(npm install *)",
|
|
||||||
"Bash(python -c \"import struct, os; paths=['D:/codes/carrot_bbs/frontend/assets/splash-icon.png','D:/codes/carrot_bbs/frontend/assets/icon.png','D:/codes/carrot_bbs/frontend/assets/android-icon-foreground.png','D:/codes/carrot_bbs/frontend/assets/android-icon-background.png','D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png','D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png','D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png','D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png','D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png'];\\\\nfor p in paths:\\\\n try:\\\\n with open\\(p,'rb'\\) as f: data=f.read\\(33\\)\\\\n sig=data[:8]; w=h=None\\\\n if sig==b'\\\\\\\\x89PNG\\\\\\\\r\\\\\\\\n\\\\\\\\x1a\\\\\\\\n': w,h=struct.unpack\\('>II', data[16:24]\\)\\\\n print\\(f'{p} | exists={os.path.exists\\(p\\)} | bytes={os.path.getsize\\(p\\)} | png={sig==b\"\\\\\\\\x89PNG\\\\\\\\r\\\\\\\\n\\\\\\\\x1a\\\\\\\\n\"} | size={w}x{h}'\\)\\\\n except Exception as e:\\\\n print\\(f'{p} | ERROR {e}'\\)\")",
|
|
||||||
"Bash(python -c 'import struct, os; paths=[\"D:/codes/carrot_bbs/frontend/assets/splash-icon.png\",\"D:/codes/carrot_bbs/frontend/assets/icon.png\",\"D:/codes/carrot_bbs/frontend/assets/android-icon-foreground.png\",\"D:/codes/carrot_bbs/frontend/assets/android-icon-background.png\",\"D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-mdpi/splashscreen_logo.png\",\"D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-hdpi/splashscreen_logo.png\",\"D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-xhdpi/splashscreen_logo.png\",\"D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-xxhdpi/splashscreen_logo.png\",\"D:/codes/carrot_bbs/frontend/android/app/src/main/res/drawable-xxxhdpi/splashscreen_logo.png\"]; sig_png=b\"\\\\x89PNG\\\\r\\\\n\\\\x1a\\\\n\"; [print\\(f\"{p} | bytes={os.path.getsize\\(p\\)} | png={open\\(p,\"rb\"\\).read\\(8\\)==sig_png} | size={struct.unpack\\(\">II\", open\\(p,\"rb\"\\).read\\(24\\)[16:24]\\) if open\\(p,\"rb\"\\).read\\(8\\)==sig_png else None}\"\\) for p in paths]')"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +1,10 @@
|
|||||||
# Dependencies
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
# Source control
|
|
||||||
.git
|
.git
|
||||||
.gitignore
|
|
||||||
|
|
||||||
# Expo caches
|
|
||||||
.expo
|
.expo
|
||||||
.expo-shared
|
|
||||||
|
|
||||||
# Build artifacts
|
|
||||||
dist
|
dist
|
||||||
dist-web
|
dist-web
|
||||||
dist-android
|
dist-android
|
||||||
dist-ios
|
|
||||||
dist-android-update.zip
|
dist-android-update.zip
|
||||||
dist-ios-update.zip
|
|
||||||
android
|
android
|
||||||
ios
|
|
||||||
web-build
|
|
||||||
|
|
||||||
# Native credentials (never ship into image)
|
|
||||||
*.jks
|
|
||||||
*.p8
|
|
||||||
*.p12
|
|
||||||
*.key
|
|
||||||
*.mobileprovision
|
|
||||||
*.orig.*
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
screenshots
|
screenshots
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
|
||||||
.idea
|
|
||||||
.DS_Store
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
coverage
|
|
||||||
__tests__
|
|
||||||
|
|||||||
@@ -21,28 +21,25 @@ on:
|
|||||||
env:
|
env:
|
||||||
REGISTRY: code.littlelan.cn
|
REGISTRY: code.littlelan.cn
|
||||||
IMAGE_NAME: carrot_bbs/frontend-web
|
IMAGE_NAME: carrot_bbs/frontend-web
|
||||||
|
OTA_PLATFORM: android
|
||||||
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
|
OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish
|
||||||
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
|
OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ota:
|
ota-android:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true')
|
if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true')
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
platform: [android, ios]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Node
|
- name: Set up Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '25.6.1'
|
node-version: '25.6.1'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -50,68 +47,68 @@ jobs:
|
|||||||
- name: Resolve runtime version
|
- name: Resolve runtime version
|
||||||
id: runtime
|
id: runtime
|
||||||
run: |
|
run: |
|
||||||
RUNTIME_VERSION="$(node -p "require('./app.config.js').runtimeVersion")"
|
RUNTIME_VERSION="$(node -p "require('./app.json').expo.version")"
|
||||||
echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Resolved runtimeVersion: ${RUNTIME_VERSION}"
|
echo "Resolved runtimeVersion: ${RUNTIME_VERSION}"
|
||||||
|
|
||||||
- name: Export update bundle
|
- name: Export Android update bundle
|
||||||
run: |
|
run: |
|
||||||
rm -rf dist-${{ matrix.platform }} dist-${{ matrix.platform }}-update.zip
|
rm -rf dist-android dist-android-update.zip
|
||||||
npx expo export --platform ${{ matrix.platform }} --output-dir dist-${{ matrix.platform }}
|
npx expo export --platform android --output-dir dist-android
|
||||||
npx expo config --type public --json > dist-${{ matrix.platform }}/expoConfig.json
|
npx expo config --type public --json > dist-android/expoConfig.json
|
||||||
|
|
||||||
- name: Archive update bundle
|
- name: Archive Android update bundle
|
||||||
run: |
|
run: |
|
||||||
python -c "
|
python - <<'PY'
|
||||||
import os, zipfile
|
import os
|
||||||
p = '${{ matrix.platform }}'
|
import zipfile
|
||||||
dist = f'dist-{p}'
|
|
||||||
with zipfile.ZipFile(f'dist-{p}-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf:
|
with zipfile.ZipFile('dist-android-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||||
for root, _, files in os.walk(dist):
|
for root, _, files in os.walk('dist-android'):
|
||||||
for name in files:
|
for name in files:
|
||||||
src = os.path.join(root, name)
|
src = os.path.join(root, name)
|
||||||
arc = os.path.relpath(src, dist)
|
arc = os.path.relpath(src, 'dist-android')
|
||||||
zf.write(src, arc)
|
zf.write(src, arc)
|
||||||
"
|
PY
|
||||||
|
|
||||||
- name: Publish OTA
|
- name: Publish OTA
|
||||||
env:
|
env:
|
||||||
OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }}
|
OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1)
|
test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1)
|
||||||
curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${{ matrix.platform }}" \
|
curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${OTA_PLATFORM}" \
|
||||||
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
||||||
-H "Content-Type: application/zip" \
|
-H "Content-Type: application/zip" \
|
||||||
--data-binary @"dist-${{ matrix.platform }}-update.zip"
|
--data-binary @"dist-android-update.zip"
|
||||||
|
|
||||||
- name: Verify OTA manifest
|
- name: Verify OTA manifest
|
||||||
run: |
|
run: |
|
||||||
REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \
|
REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \
|
||||||
-H "expo-platform: ${{ matrix.platform }}" \
|
-H "expo-platform: ${OTA_PLATFORM}" \
|
||||||
-H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \
|
-H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \
|
||||||
-H "expo-protocol-version: 1" \
|
-H "expo-protocol-version: 1" \
|
||||||
| python -c "import sys, json, re; s=sys.stdin.read(); m=re.search(r'\\{\"id\":.*\"extra\":\{.*\}\}', s); j=json.loads(m.group(0)); print(j['launchAsset']['url'].split('/')[-1].split('&')[0])")"
|
| python -c "import sys, json, re; s=sys.stdin.read(); m=re.search(r'\\{\\\"id\\\":.*\\\"extra\\\":\\{.*\\}\\}', s); j=json.loads(m.group(0)); print(j['launchAsset']['url'].split('/')[-1].split('&')[0])")"
|
||||||
LOCAL="$(python -c "import json; m=json.load(open('dist-${{ matrix.platform }}/metadata.json')); print(m['fileMetadata']['${{ matrix.platform }}']['bundle'].split('/')[-1])")"
|
LOCAL="$(python -c "import json; m=json.load(open('dist-android/metadata.json')); print(m['fileMetadata']['android']['bundle'].split('/')[-1])")"
|
||||||
echo "Remote bundle: ${REMOTE}"
|
echo "Remote bundle: ${REMOTE}"
|
||||||
echo "Local bundle: ${LOCAL}"
|
echo "Local bundle: ${LOCAL}"
|
||||||
test "${REMOTE}" = "${LOCAL}"
|
test "${REMOTE}" = "${LOCAL}"
|
||||||
|
|
||||||
build-android-apk:
|
build-android-apk:
|
||||||
runs-on: android-builder
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: reactnativecommunity/react-native-android:latest
|
image: reactnativecommunity/react-native-android:latest
|
||||||
env:
|
env:
|
||||||
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=false -Dorg.gradle.workers.max=1"
|
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.workers.max=4 -Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=512m"
|
||||||
NODE_OPTIONS: "--max-old-space-size=2048"
|
_JAVA_OPTIONS: "-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=512m"
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
NODE_ENV: "production"
|
NODE_ENV: "production"
|
||||||
GRADLE_USER_HOME: /root/.gradle
|
NDK_NUM_JOBS: "4"
|
||||||
|
CMAKE_BUILD_PARALLEL_LEVEL: "4"
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Java
|
- name: Set up Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v4
|
||||||
@@ -123,6 +120,7 @@ jobs:
|
|||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '25.6.1'
|
node-version: '25.6.1'
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
@@ -130,120 +128,128 @@ jobs:
|
|||||||
- name: Generate Android native project
|
- name: Generate Android native project
|
||||||
run: npx expo prebuild --platform android
|
run: npx expo prebuild --platform android
|
||||||
|
|
||||||
- name: Configure Gradle with signing
|
- name: Configure Gradle with Aliyun Maven mirror
|
||||||
run: |
|
run: |
|
||||||
cd android
|
cd android
|
||||||
|
|
||||||
# Decode Android signing keystore
|
# Update settings.gradle with Aliyun Maven mirror
|
||||||
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > app/withyou-release-key.keystore
|
cat > settings.gradle << 'SETTINGS_EOF'
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
gradlePluginPortal()
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
||||||
|
}
|
||||||
|
def reactNativeGradlePlugin = new File(
|
||||||
|
providers.exec {
|
||||||
|
workingDir(rootDir)
|
||||||
|
commandLine("node", "--print", "require.resolve('@react-native/gradle-plugin/package.json', { paths: [require.resolve('react-native/package.json')] })")
|
||||||
|
}.standardOutput.asText.get().trim()
|
||||||
|
).getParentFile().absolutePath
|
||||||
|
includeBuild(reactNativeGradlePlugin)
|
||||||
|
|
||||||
# Append signing properties to gradle.properties (secrets appended after prebuild for better cache hit rate).
|
def expoPluginsPath = new File(
|
||||||
# IMPORTANT 1: each echo line writes a column-0 string — gradle.properties keys cannot have
|
providers.exec {
|
||||||
# leading whitespace, otherwise hasProperty() returns false and release signing breaks
|
workingDir(rootDir)
|
||||||
# (see app/build.gradle signingConfigs.release).
|
commandLine("node", "--print", "require.resolve('expo-modules-autolinking/package.json', { paths: [require.resolve('expo/package.json')] })")
|
||||||
# IMPORTANT 2: prebuild emits gradle.properties WITHOUT a trailing newline (last line is
|
}.standardOutput.asText.get().trim(),
|
||||||
# `expo.inlineModules.watchedDirectories=[]`). A naive `>>` append concatenates the first
|
"../android/expo-gradle-plugin"
|
||||||
# new key onto that last line, corrupting MYAPP_UPLOAD_STORE_FILE. We unconditionally
|
).absolutePath
|
||||||
# prepend a newline before the appended block to guarantee a clean line break.
|
includeBuild(expoPluginsPath)
|
||||||
# NB: this block runs with `set -e`; if any echo fails the step aborts.
|
}
|
||||||
{
|
|
||||||
printf '\n'
|
|
||||||
echo "MYAPP_UPLOAD_STORE_FILE=withyou-release-key.keystore"
|
|
||||||
echo "MYAPP_UPLOAD_STORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}"
|
|
||||||
echo "MYAPP_UPLOAD_KEY_ALIAS=withyou-key"
|
|
||||||
echo "MYAPP_UPLOAD_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}"
|
|
||||||
} >> gradle.properties
|
|
||||||
|
|
||||||
# CI build tuning (low-memory container): force single-worker, reduce JVM heap,
|
plugins {
|
||||||
# plus restore properties that prebuild does NOT generate but the build needs.
|
id("com.facebook.react.settings")
|
||||||
# printf '\n' below is defensive — even though the signing block above already
|
id("expo-autolinking-settings")
|
||||||
# ensured a trailing newline, both blocks should be independently safe.
|
}
|
||||||
{
|
|
||||||
printf '\n'
|
|
||||||
echo "org.gradle.daemon=false"
|
|
||||||
echo "org.gradle.parallel=false"
|
|
||||||
echo "org.gradle.configureondemand=false"
|
|
||||||
echo "org.gradle.workers.max=1"
|
|
||||||
echo "org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError"
|
|
||||||
echo "kotlin.daemon.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:ReservedCodeCacheSize=256m"
|
|
||||||
echo "ndkVersion=27.1.12297006"
|
|
||||||
echo "systemProp.org.gradle.internal.http.connectionTimeout=30000"
|
|
||||||
echo "systemProp.org.gradle.internal.http.socketTimeout=30000"
|
|
||||||
# ExpoAutolinkingPlugin requires this; without it Gradle calls
|
|
||||||
# `node ... mirror-kotlin-inline-modules --watched-directories-serialized ''`
|
|
||||||
# and node JSON.parse('') throws SyntaxError, exiting 1.
|
|
||||||
# See ExpoAutolinkingPlugin.kt:51 (findProperty fallback to emptyList)
|
|
||||||
# and ExpoAutolinkingPlugin.kt:67 (passes value to node as-is).
|
|
||||||
echo "expo.inlineModules.watchedDirectories=[]"
|
|
||||||
} >> gradle.properties
|
|
||||||
|
|
||||||
# Verify signing config in app/build.gradle
|
extensions.configure(com.facebook.react.ReactSettingsExtension) { ex ->
|
||||||
echo "=== app/build.gradle signing section ==="
|
if (System.getenv('EXPO_USE_COMMUNITY_AUTOLINKING') == '1') {
|
||||||
grep -A 20 'signingConfigs' app/build.gradle || echo "signingConfigs not found"
|
ex.autolinkLibrariesFromCommand()
|
||||||
|
} else {
|
||||||
|
ex.autolinkLibrariesFromCommand(expoAutolinking.rnConfigCommand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expoAutolinking.useExpoModules()
|
||||||
|
|
||||||
# Diagnostic: dump full gradle.properties + verify all 4 signing keys are at column 0.
|
rootProject.name = '萝卜社区'
|
||||||
# Each grep -c MUST print 1; 0 means the key was concatenated onto a previous line
|
|
||||||
# (typically because prebuild's gradle.properties had no trailing newline — see fix above).
|
expoAutolinking.useExpoVersionCatalog()
|
||||||
echo "=== gradle.properties (final) ==="
|
|
||||||
cat gradle.properties
|
include ':app'
|
||||||
echo "=== signing keys column-0 check (each must be 1) ==="
|
includeBuild(expoAutolinking.reactNativeGradlePlugin)
|
||||||
for k in MYAPP_UPLOAD_STORE_FILE MYAPP_UPLOAD_STORE_PASSWORD MYAPP_UPLOAD_KEY_ALIAS MYAPP_UPLOAD_KEY_PASSWORD; do
|
SETTINGS_EOF
|
||||||
count=$(grep -c "^${k}=" gradle.properties || true)
|
|
||||||
echo "${k}=${count}"
|
# Update build.gradle with Aliyun Maven mirror
|
||||||
if [ "${count}" != "1" ]; then
|
cat > build.gradle << 'BUILD_EOF'
|
||||||
echo "FATAL: ${k} not found at column 0 — release signing will fail."
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
exit 1
|
|
||||||
fi
|
buildscript {
|
||||||
done
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/gradle-plugin' }
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath('com.android.tools.build:gradle')
|
||||||
|
classpath('com.facebook.react:react-native-gradle-plugin')
|
||||||
|
classpath('org.jetbrains.kotlin:kotlin-gradle-plugin')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven { url 'https://www.jitpack.io' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/public' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/google' }
|
||||||
|
maven { url 'https://maven.aliyun.com/repository/central' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply plugin: "expo-root-project"
|
||||||
|
apply plugin: "com.facebook.react.rootproject"
|
||||||
|
BUILD_EOF
|
||||||
|
|
||||||
|
# Update gradle.properties
|
||||||
|
cat > gradle.properties << 'PROPS_EOF'
|
||||||
|
org.gradle.daemon=false
|
||||||
|
org.gradle.parallel=true
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
org.gradle.workers.max=4
|
||||||
|
org.gradle.jvmargs=-Xmx8g -XX:MaxMetaspaceSize=1g -XX:+UseG1GC -XX:SoftRefLRUPolicyMSPerMB=0 -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.enableJetifier=false
|
||||||
|
android.useAndroidX=true
|
||||||
|
hermesEnabled=true
|
||||||
|
reactNativeArchitectures=arm64-v8a
|
||||||
|
newArchEnabled=true
|
||||||
|
edgeToEdgeEnabled=true
|
||||||
|
expo.gif.enabled=true
|
||||||
|
expo.webp.enabled=true
|
||||||
|
expo.webp.animated=false
|
||||||
|
expo.useLegacyPackaging=false
|
||||||
|
PROPS_EOF
|
||||||
|
|
||||||
- name: Build Android release APK (arm64 only)
|
- name: Build Android release APK (arm64 only)
|
||||||
run: |
|
run: |
|
||||||
cd android
|
cd android
|
||||||
chmod +x gradlew
|
chmod +x gradlew
|
||||||
taskset -c 0-7 ./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=1 -Dkotlin.daemon.jvm.options="-Xmx2g,XX:MaxMetaspaceSize=1g"
|
./gradlew :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --max-workers=4 --parallel
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Upload APK artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: withyou-android-release-apk
|
name: carrot-bbs-android-release-apk
|
||||||
path: android/app/build/outputs/apk/release/app-release.apk
|
path: android/app/build/outputs/apk/release/app-release.apk
|
||||||
|
|
||||||
- name: Resolve runtime version
|
|
||||||
id: runtime
|
|
||||||
run: |
|
|
||||||
RUNTIME_VERSION="$(node -p "require('./app.config.js').runtimeVersion")"
|
|
||||||
echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Resolved runtimeVersion: ${RUNTIME_VERSION}"
|
|
||||||
|
|
||||||
- name: Upload APK to Updates Server
|
|
||||||
env:
|
|
||||||
OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }}
|
|
||||||
UPDATES_SERVER_URL: https://updates.littlelan.cn
|
|
||||||
run: |
|
|
||||||
test -n "${OTA_AUTH_TOKEN}" || (echo "Missing secret OTA_AUTH_TOKEN" && exit 1)
|
|
||||||
|
|
||||||
APK_PATH="android/app/build/outputs/apk/release/app-release.apk"
|
|
||||||
RUNTIME_VERSION="${{ steps.runtime.outputs.runtime_version }}"
|
|
||||||
|
|
||||||
echo "Uploading APK for runtime version: ${RUNTIME_VERSION}"
|
|
||||||
|
|
||||||
HTTP_CODE=$(curl -s -o response.json -w "%{http_code}" \
|
|
||||||
-X POST \
|
|
||||||
"${UPDATES_SERVER_URL}/admin/apk/upload?runtimeVersion=${RUNTIME_VERSION}" \
|
|
||||||
-H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \
|
|
||||||
-H "Content-Type: application/vnd.android.package-archive" \
|
|
||||||
--data-binary @"${APK_PATH}")
|
|
||||||
|
|
||||||
echo "HTTP Response Code: ${HTTP_CODE}"
|
|
||||||
cat response.json
|
|
||||||
|
|
||||||
if [ "${HTTP_CODE}" -ne 200 ]; then
|
|
||||||
echo "Failed to upload APK"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "APK uploaded successfully!"
|
|
||||||
|
|
||||||
build-and-push-web:
|
build-and-push-web:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -51,6 +51,3 @@ dist-android-update.zip
|
|||||||
# Backend
|
# Backend
|
||||||
backend/data/
|
backend/data/
|
||||||
backend/logs/
|
backend/logs/
|
||||||
|
|
||||||
doc/
|
|
||||||
docs/
|
|
||||||
|
|||||||
10
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Screenshots
|
||||||
|
screenshots/
|
||||||
9
.idea/frontend.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
448
.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,448 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@expo/dom-webview/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/@expo/dom-webview/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@expo/log-box/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/@expo/log-box/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@react-native-async-storage/async-storage/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/@react-native-async-storage/async-storage/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/@react-native/gradle-plugin" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/@react-native/gradle-plugin" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-application/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-application/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-background-fetch/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-background-fetch/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-constants/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-constants/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-constants/scripts" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-constants/scripts" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-client/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-dev-client/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-launcher/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-dev-launcher/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-dev-launcher/expo-dev-launcher-gradle-plugin" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-menu-interface/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-dev-menu-interface/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-dev-menu/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-dev-menu/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-eas-client/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-eas-client/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-file-system/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-file-system/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-font/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-font/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-haptics/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-haptics/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image-loader/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-image-loader/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image-picker/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-image-picker/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-image/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-image/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-json-utils/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-json-utils/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-linear-gradient/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-linear-gradient/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-linking/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-linking/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-manifests/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-manifests/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-media-library/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-media-library/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-autolinking/android/expo-gradle-plugin" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-modules-autolinking/android/expo-gradle-plugin" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-core/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-modules-core/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-modules-core/expo-module-gradle-plugin" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-modules-core/expo-module-gradle-plugin" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-notifications/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-notifications/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-router/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-router/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-sqlite/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-sqlite/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-structured-headers/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-structured-headers/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-system-ui/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-system-ui/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-task-manager/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-task-manager/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates-interface/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-updates-interface/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-updates/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-updates/expo-updates-gradle-plugin" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-updates/expo-updates-gradle-plugin" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo-video/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo-video/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-asset/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-asset/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-keep-awake/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/expo/node_modules/expo-keep-awake/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-gesture-handler/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native-gesture-handler/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-pager-view/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native-pager-view/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-reanimated/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native-reanimated/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-safe-area-context/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native-safe-area-context/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-screens/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native-screens/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/react-native-worklets/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/react-native-worklets/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/node_modules/unimodules-app-loader/android" />
|
||||||
|
<option name="gradleJvm" value="ms-21" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$/node_modules/unimodules-app-loader/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/frontend.iml" filepath="$PROJECT_DIR$/.idea/frontend.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
376
.kilo/package-lock.json
generated
@@ -1,376 +0,0 @@
|
|||||||
{
|
|
||||||
"name": ".kilo",
|
|
||||||
"lockfileVersion": 3,
|
|
||||||
"requires": true,
|
|
||||||
"packages": {
|
|
||||||
"": {
|
|
||||||
"dependencies": {
|
|
||||||
"@kilocode/plugin": "7.2.31"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@kilocode/plugin": {
|
|
||||||
"version": "7.2.31",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@kilocode/plugin/-/plugin-7.2.31.tgz",
|
|
||||||
"integrity": "sha512-KmKTTIly7hRlJdXKhqZ/j/brvTPh0z0UTjWSjJWq5fqf4pATgYGn7G0g3ZjILnN7MUkkZXuljgqExTEeQJHGkQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@kilocode/sdk": "7.2.31",
|
|
||||||
"effect": "4.0.0-beta.48",
|
|
||||||
"zod": "4.1.8"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@opentui/core": ">=0.1.100",
|
|
||||||
"@opentui/solid": ">=0.1.100"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@opentui/core": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@opentui/solid": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@kilocode/sdk": {
|
|
||||||
"version": "7.2.31",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@kilocode/sdk/-/sdk-7.2.31.tgz",
|
|
||||||
"integrity": "sha512-Sx05yv+3TIlc6M4Ze+YGgKCLoIg8B0WRE15JXDriVncT+wz7M6+e+4mjNWkwfsuywdeOTYPfeFViqX8iO7ckKA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"cross-spawn": "7.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@standard-schema/spec": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/@standard-schema/spec/-/spec-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/cross-spawn": {
|
|
||||||
"version": "7.0.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
|
||||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"path-key": "^3.1.0",
|
|
||||||
"shebang-command": "^2.0.0",
|
|
||||||
"which": "^2.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-libc": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/effect": {
|
|
||||||
"version": "4.0.0-beta.48",
|
|
||||||
"resolved": "https://registry.npmmirror.com/effect/-/effect-4.0.0-beta.48.tgz",
|
|
||||||
"integrity": "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@standard-schema/spec": "^1.1.0",
|
|
||||||
"fast-check": "^4.6.0",
|
|
||||||
"find-my-way-ts": "^0.1.6",
|
|
||||||
"ini": "^6.0.0",
|
|
||||||
"kubernetes-types": "^1.30.0",
|
|
||||||
"msgpackr": "^1.11.9",
|
|
||||||
"multipasta": "^0.2.7",
|
|
||||||
"toml": "^4.1.1",
|
|
||||||
"uuid": "^13.0.0",
|
|
||||||
"yaml": "^2.8.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fast-check": {
|
|
||||||
"version": "4.7.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/fast-check/-/fast-check-4.7.0.tgz",
|
|
||||||
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/dubzzz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/fast-check"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"pure-rand": "^8.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/find-my-way-ts": {
|
|
||||||
"version": "0.1.6",
|
|
||||||
"resolved": "https://registry.npmmirror.com/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz",
|
|
||||||
"integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/ini": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/ini/-/ini-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": "^20.17.0 || >=22.9.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/isexe": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/kubernetes-types": {
|
|
||||||
"version": "1.30.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/kubernetes-types/-/kubernetes-types-1.30.0.tgz",
|
|
||||||
"integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/msgpackr": {
|
|
||||||
"version": "1.11.12",
|
|
||||||
"resolved": "https://registry.npmmirror.com/msgpackr/-/msgpackr-1.11.12.tgz",
|
|
||||||
"integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optionalDependencies": {
|
|
||||||
"msgpackr-extract": "^3.0.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/msgpackr-extract": {
|
|
||||||
"version": "3.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
|
||||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"node-gyp-build-optional-packages": "5.2.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
|
||||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
|
||||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
|
||||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
|
||||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
|
||||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/multipasta": {
|
|
||||||
"version": "0.2.7",
|
|
||||||
"resolved": "https://registry.npmmirror.com/multipasta/-/multipasta-0.2.7.tgz",
|
|
||||||
"integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/node-gyp-build-optional-packages": {
|
|
||||||
"version": "5.2.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
|
||||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"node-gyp-build-optional-packages": "bin.js",
|
|
||||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
|
||||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/path-key": {
|
|
||||||
"version": "3.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
|
|
||||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/pure-rand": {
|
|
||||||
"version": "8.4.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/pure-rand/-/pure-rand-8.4.0.tgz",
|
|
||||||
"integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://github.com/sponsors/dubzzz"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/fast-check"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/shebang-command": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"shebang-regex": "^3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shebang-regex": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/toml": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/toml/-/toml-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/uuid": {
|
|
||||||
"version": "13.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/uuid/-/uuid-13.0.1.tgz",
|
|
||||||
"integrity": "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw==",
|
|
||||||
"funding": [
|
|
||||||
"https://github.com/sponsors/broofa",
|
|
||||||
"https://github.com/sponsors/ctavan"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"bin": {
|
|
||||||
"uuid": "dist-node/bin/uuid"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"isexe": "^2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"node-which": "bin/node-which"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/yaml": {
|
|
||||||
"version": "2.8.4",
|
|
||||||
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz",
|
|
||||||
"integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"yaml": "bin.mjs"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.6"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/eemeli"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zod": {
|
|
||||||
"version": "4.1.8",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
231
ARCHITECTURE_REFACTOR_PLAN.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# 架构修复方案文档
|
||||||
|
|
||||||
|
## 1. 重构目标
|
||||||
|
|
||||||
|
### 1.1 核心原则
|
||||||
|
- **单一职责**:每个模块只负责一个明确的功能
|
||||||
|
- **依赖倒置**:高层模块不依赖低层模块,都依赖抽象
|
||||||
|
- **开闭原则**:对扩展开放,对修改关闭
|
||||||
|
|
||||||
|
### 1.2 目标架构
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── core/ # 核心业务逻辑(框架无关)
|
||||||
|
│ ├── entities/ # 业务实体
|
||||||
|
│ ├── repositories/ # 仓库接口
|
||||||
|
│ └── usecases/ # 业务用例
|
||||||
|
├── data/ # 数据层
|
||||||
|
│ ├── datasources/ # 数据源实现(API、DB、Cache)
|
||||||
|
│ ├── repositories/ # 仓库实现
|
||||||
|
│ └── mappers/ # 数据映射
|
||||||
|
├── presentation/ # 表现层
|
||||||
|
│ ├── components/ # UI组件
|
||||||
|
│ ├── screens/ # 屏幕组件
|
||||||
|
│ ├── stores/ # 状态管理
|
||||||
|
│ └── hooks/ # 自定义hooks
|
||||||
|
└── infrastructure/ # 基础设施
|
||||||
|
├── navigation/ # 导航
|
||||||
|
├── theme/ # 主题
|
||||||
|
└── services/ # 外部服务
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 具体修复方案
|
||||||
|
|
||||||
|
### 2.1 messageManager.ts 重构方案
|
||||||
|
|
||||||
|
#### 现状问题
|
||||||
|
- 1500+行,上帝对象
|
||||||
|
- 混合了 WebSocket、数据库、状态管理、UI通知
|
||||||
|
- 与SQLite深度耦合
|
||||||
|
|
||||||
|
#### 目标结构
|
||||||
|
```
|
||||||
|
src/data/
|
||||||
|
├── datasources/
|
||||||
|
│ ├── WebSocketClient.ts # WebSocket连接管理
|
||||||
|
│ └── MessageDataSource.ts # 消息数据源
|
||||||
|
├── repositories/
|
||||||
|
│ └── MessageRepository.ts # 消息仓库实现
|
||||||
|
└── mappers/
|
||||||
|
└── MessageMapper.ts # 消息数据映射
|
||||||
|
|
||||||
|
src/core/
|
||||||
|
├── entities/
|
||||||
|
│ └── Message.ts # 消息实体
|
||||||
|
├── repositories/
|
||||||
|
│ └── IMessageRepository.ts # 仓库接口
|
||||||
|
└── usecases/
|
||||||
|
├── ProcessMessageUseCase.ts
|
||||||
|
├── SyncMessagesUseCase.ts
|
||||||
|
└── UpdateReadReceiptUseCase.ts
|
||||||
|
|
||||||
|
src/presentation/stores/
|
||||||
|
└── messageStore.ts # 简化的消息状态管理
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 重构步骤
|
||||||
|
1. 提取 WebSocketClient - 纯连接管理
|
||||||
|
2. 创建 MessageRepository - 数据访问抽象
|
||||||
|
3. 创建 UseCases - 业务逻辑
|
||||||
|
4. 重写 messageStore - 仅负责UI状态
|
||||||
|
5. 删除旧的 messageManager.ts
|
||||||
|
|
||||||
|
### 2.2 乐观更新工具函数方案
|
||||||
|
|
||||||
|
#### 现状问题
|
||||||
|
- userStore.ts 中重复6+次相同模式
|
||||||
|
- 代码冗余,难以维护
|
||||||
|
|
||||||
|
#### 目标API
|
||||||
|
```typescript
|
||||||
|
// utils/optimisticUpdate.ts
|
||||||
|
export interface OptimisticUpdateOptions<T, R> {
|
||||||
|
store: StoreApi<any>;
|
||||||
|
getCurrentState: () => T;
|
||||||
|
optimisticUpdate: (current: T) => T;
|
||||||
|
apiCall: () => Promise<R>;
|
||||||
|
onSuccess?: (result: R, current: T) => T;
|
||||||
|
onError?: (error: any, original: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function optimisticUpdate<T, R>(
|
||||||
|
options: OptimisticUpdateOptions<T, R>
|
||||||
|
): Promise<R>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 使用示例
|
||||||
|
```typescript
|
||||||
|
// 重构前(重复代码)
|
||||||
|
set(state => ({ posts: updatedPosts }));
|
||||||
|
try {
|
||||||
|
const result = await postService.like(postId);
|
||||||
|
// ...
|
||||||
|
} catch (error) {
|
||||||
|
set(state => ({ posts: originalPosts }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重构后(简洁)
|
||||||
|
await optimisticUpdate({
|
||||||
|
store,
|
||||||
|
getCurrentState: () => store.getState().posts,
|
||||||
|
optimisticUpdate: (posts) => updatePostLikes(posts, postId, true),
|
||||||
|
apiCall: () => postService.like(postId),
|
||||||
|
onSuccess: (result, posts) => updatePostWithServerData(posts, result),
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 useResponsive.ts 拆分方案
|
||||||
|
|
||||||
|
#### 现状问题
|
||||||
|
- 485行,43个导出
|
||||||
|
- 功能混杂:断点检测、响应式值计算、平台检测
|
||||||
|
|
||||||
|
#### 目标结构
|
||||||
|
```
|
||||||
|
src/presentation/hooks/responsive/
|
||||||
|
├── useBreakpoint.ts # 断点检测
|
||||||
|
├── useResponsiveValue.ts # 响应式值映射
|
||||||
|
├── useOrientation.ts # 方向检测
|
||||||
|
├── usePlatform.ts # 平台检测
|
||||||
|
├── useScreenSize.ts # 屏幕尺寸
|
||||||
|
└── index.ts # 统一导出
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 各Hook职责
|
||||||
|
- `useBreakpoint`: 返回当前断点 (mobile/tablet/desktop/wide)
|
||||||
|
- `useResponsiveValue`: 根据断点返回不同值
|
||||||
|
- `useOrientation`: 返回 portrait/landscape
|
||||||
|
- `usePlatform`: 返回 ios/android/web
|
||||||
|
- `useScreenSize`: 返回 width/height
|
||||||
|
|
||||||
|
### 2.4 MainNavigator 解耦方案
|
||||||
|
|
||||||
|
#### 现状问题
|
||||||
|
- 1118行,直接依赖store
|
||||||
|
- 混合了导航配置和业务逻辑
|
||||||
|
|
||||||
|
#### 目标结构
|
||||||
|
```
|
||||||
|
src/infrastructure/navigation/
|
||||||
|
├── MainNavigator.tsx # 简化后的导航器
|
||||||
|
├── AuthNavigator.tsx # 认证导航
|
||||||
|
├── TabNavigator.tsx # 标签导航
|
||||||
|
├── navigationService.ts # 导航服务(解耦层)
|
||||||
|
├── navigationTypes.ts # 类型定义
|
||||||
|
└── hooks/
|
||||||
|
└── useNavigationState.ts # 导航状态hook
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 解耦策略
|
||||||
|
1. 创建 NavigationService - 提供导航操作的抽象
|
||||||
|
2. Store通过NavigationService触发导航,而非直接操作
|
||||||
|
3. MainNavigator只负责渲染,不处理业务逻辑
|
||||||
|
|
||||||
|
### 2.5 Repository 层创建方案
|
||||||
|
|
||||||
|
#### 现状问题
|
||||||
|
- 服务层直接操作数据库
|
||||||
|
- 没有明确的数据访问边界
|
||||||
|
|
||||||
|
#### 目标结构
|
||||||
|
```
|
||||||
|
src/data/repositories/
|
||||||
|
├── MessageRepository.ts # 消息数据访问
|
||||||
|
├── PostRepository.ts # 帖子数据访问
|
||||||
|
├── UserRepository.ts # 用户数据访问
|
||||||
|
└── interfaces/
|
||||||
|
├── IMessageRepository.ts
|
||||||
|
├── IPostRepository.ts
|
||||||
|
└── IUserRepository.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 职责划分
|
||||||
|
- **Repository**: 数据访问抽象,定义接口
|
||||||
|
- **DataSource**: 具体数据源实现(API、SQLite、Cache)
|
||||||
|
- **Service**: 业务逻辑编排,使用Repository
|
||||||
|
|
||||||
|
## 3. 实施计划
|
||||||
|
|
||||||
|
### 阶段1:基础设施(第1-2天)
|
||||||
|
- [ ] 创建目录结构
|
||||||
|
- [ ] 实现乐观更新工具函数
|
||||||
|
- [ ] 拆分 useResponsive hooks
|
||||||
|
|
||||||
|
### 阶段2:数据层重构(第3-5天)
|
||||||
|
- [ ] 创建 Repository 接口和实现
|
||||||
|
- [ ] 创建 DataSource 层
|
||||||
|
- [ ] 迁移数据库操作到 Repository
|
||||||
|
|
||||||
|
### 阶段3:核心业务重构(第6-10天)
|
||||||
|
- [ ] 重构 messageManager → UseCases + Store
|
||||||
|
- [ ] 创建 WebSocketClient
|
||||||
|
- [ ] 实现消息同步逻辑
|
||||||
|
|
||||||
|
### 阶段4:表现层重构(第11-12天)
|
||||||
|
- [ ] 解耦 MainNavigator
|
||||||
|
- [ ] 重构 userStore 使用乐观更新工具
|
||||||
|
- [ ] 更新所有屏幕组件
|
||||||
|
|
||||||
|
### 阶段5:测试与优化(第13-14天)
|
||||||
|
- [ ] 单元测试
|
||||||
|
- [ ] 集成测试
|
||||||
|
- [ ] 性能优化
|
||||||
|
|
||||||
|
## 4. 风险与应对
|
||||||
|
|
||||||
|
| 风险 | 影响 | 应对措施 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| 重构引入bug | 高 | 每个模块重构后必须测试,保持API兼容 |
|
||||||
|
| 开发时间超期 | 中 | 分阶段交付,优先核心功能 |
|
||||||
|
| 团队成员不熟悉新架构 | 中 | 编写详细文档,代码审查 |
|
||||||
|
| 性能下降 | 低 | 添加性能监控,及时优化 |
|
||||||
|
|
||||||
|
## 5. 验收标准
|
||||||
|
|
||||||
|
- [ ] messageManager.ts 被删除,功能拆分到多个小模块
|
||||||
|
- [ ] userStore.ts 中无重复乐观更新代码
|
||||||
|
- [ ] useResponsive.ts 拆分为5+个专注hook
|
||||||
|
- [ ] MainNavigator.tsx 行数 < 300
|
||||||
|
- [ ] 所有单元测试通过
|
||||||
|
- [ ] 无TypeScript错误
|
||||||
|
- [ ] 性能不劣化(启动时间、内存占用)
|
||||||
138
App.tsx
@@ -1,7 +1,137 @@
|
|||||||
/**
|
/**
|
||||||
* 应用入口已由 Expo Router 接管(package.json main: expo-router/entry)。
|
* 萝卜BBS - 主应用入口
|
||||||
* 全局 Provider 与根布局见 app/_layout.tsx。
|
* 配置导航、主题、状态管理等
|
||||||
*/
|
*/
|
||||||
export default function App() {
|
|
||||||
return null;
|
import React, { useEffect, useRef } from 'react';
|
||||||
|
import { AppState, AppStateStatus, Platform, StyleSheet } from 'react-native';
|
||||||
|
import { StatusBar } from 'expo-status-bar';
|
||||||
|
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||||||
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
|
import { PaperProvider } from 'react-native-paper';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import * as Notifications from 'expo-notifications';
|
||||||
|
|
||||||
|
// 配置通知处理器 - 必须在应用启动时设置
|
||||||
|
Notifications.setNotificationHandler({
|
||||||
|
handleNotification: async () => ({
|
||||||
|
shouldShowAlert: true, // 即使在后台也要显示通知
|
||||||
|
shouldPlaySound: true, // 播放声音
|
||||||
|
shouldSetBadge: true, // 设置角标
|
||||||
|
shouldShowBanner: true, // 显示横幅
|
||||||
|
shouldShowList: true, // 显示通知列表
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
import MainNavigator from './src/navigation/MainNavigator';
|
||||||
|
import { paperTheme } from './src/theme';
|
||||||
|
import AppPromptBar from './src/components/common/AppPromptBar';
|
||||||
|
import AppDialogHost from './src/components/common/AppDialogHost';
|
||||||
|
import { installAlertOverride } from './src/services/alertOverride';
|
||||||
|
// 数据库初始化移到 authStore 中,根据用户ID创建用户专属数据库
|
||||||
|
|
||||||
|
// 创建 QueryClient 实例
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
retry: 2,
|
||||||
|
staleTime: 5 * 60 * 1000, // 5分钟
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
installAlertOverride();
|
||||||
|
|
||||||
|
// 注入全局CSS来移除React Native Web在浏览器中的默认focus outline
|
||||||
|
if (Platform.OS === 'web') {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
/* 移除TextInput focus时的浏览器默认outline */
|
||||||
|
input:focus, textarea:focus, select:focus {
|
||||||
|
outline: none !important;
|
||||||
|
outline-width: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除focus-visible时的默认outline */
|
||||||
|
input:focus-visible, textarea:focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除所有:focus相关的默认样式 */
|
||||||
|
*:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||||
|
const notificationResponseListener = useRef<Notifications.EventSubscription | null>(null);
|
||||||
|
|
||||||
|
// 系统通知功能初始化
|
||||||
|
useEffect(() => {
|
||||||
|
const initNotifications = async () => {
|
||||||
|
const { systemNotificationService } = await import('./src/services/systemNotificationService');
|
||||||
|
await systemNotificationService.initialize();
|
||||||
|
|
||||||
|
// 初始化后台保活服务
|
||||||
|
const { initBackgroundService } = await import('./src/services/backgroundService');
|
||||||
|
await initBackgroundService();
|
||||||
|
|
||||||
|
// 监听 App 状态变化
|
||||||
|
const subscription = AppState.addEventListener('change', (nextAppState) => {
|
||||||
|
if (
|
||||||
|
appState.current.match(/inactive|background/) &&
|
||||||
|
nextAppState === 'active'
|
||||||
|
) {
|
||||||
|
systemNotificationService.clearBadge();
|
||||||
|
}
|
||||||
|
appState.current = nextAppState;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听通知点击响应
|
||||||
|
notificationResponseListener.current = Notifications.addNotificationResponseReceivedListener(
|
||||||
|
(response) => {
|
||||||
|
const data = response.notification.request.content.data;
|
||||||
|
void data;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听通知到达(用于前台显示时额外处理)
|
||||||
|
const notificationReceivedSubscription = Notifications.addNotificationReceivedListener(
|
||||||
|
(notification) => {
|
||||||
|
void notification;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
subscription.remove();
|
||||||
|
notificationResponseListener.current?.remove();
|
||||||
|
notificationReceivedSubscription.remove();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
initNotifications();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
|
<SafeAreaProvider>
|
||||||
|
<PaperProvider theme={paperTheme}>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<StatusBar style="light" />
|
||||||
|
<MainNavigator />
|
||||||
|
<AppPromptBar />
|
||||||
|
<AppDialogHost />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</PaperProvider>
|
||||||
|
</SafeAreaProvider>
|
||||||
|
</GestureHandlerRootView>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:25-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -6,11 +6,10 @@ COPY package.json package-lock.json ./
|
|||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN node /app/node_modules/.bin/expo export --platform web --output-dir dist-web
|
RUN npx expo export --platform web --output-dir dist-web
|
||||||
|
|
||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
COPY --from=builder /app/dist-web /usr/share/nginx/html
|
COPY --from=builder /app/dist-web /usr/share/nginx/html
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|||||||
@@ -1,27 +1,10 @@
|
|||||||
const appJson = require('./app.json');
|
const appJson = require('./app.json');
|
||||||
const { execSync } = require('child_process');
|
|
||||||
|
|
||||||
const isDevVariant = process.env.APP_VARIANT === 'dev';
|
const isDevVariant = process.env.APP_VARIANT === 'dev';
|
||||||
const releaseApiBaseUrl = 'https://withyou.littlelan.cn/api/v1';
|
const releaseApiBaseUrl = 'https://bbs.littlelan.cn/api/v1';
|
||||||
const releaseUpdatesBaseUrl = 'https://updates.littlelan.cn';
|
const releaseUpdatesBaseUrl = 'https://updates.littlelan.cn';
|
||||||
const devApiBaseUrl = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.31.238:8080/api/v1';
|
const devApiBaseUrl = process.env.EXPO_PUBLIC_API_BASE_URL || 'http://192.168.31.238:8080/api/v1';
|
||||||
|
|
||||||
function getCommitCount() {
|
|
||||||
try {
|
|
||||||
return execSync('git rev-list --count HEAD', { encoding: 'utf-8' }).trim();
|
|
||||||
} catch {
|
|
||||||
return '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在 commit count 上加偏移,保证 versionCode / buildNumber / runtimeVersion
|
|
||||||
// 始终大于历史最大值(之前的最后一个版本是 5547),避免被系统/Play Store
|
|
||||||
// 误判为降级。后续即使 commit count 重置或换仓库,偏移也能保证单调递增。
|
|
||||||
const BUILD_NUMBER_OFFSET = 100000;
|
|
||||||
|
|
||||||
const commitCount = getCommitCount();
|
|
||||||
const buildNumber = String(parseInt(commitCount, 10) + BUILD_NUMBER_OFFSET);
|
|
||||||
|
|
||||||
function toManifestUrl(baseUrl, portOverride) {
|
function toManifestUrl(baseUrl, portOverride) {
|
||||||
const parsed = new URL(baseUrl);
|
const parsed = new URL(baseUrl);
|
||||||
if (portOverride != null) {
|
if (portOverride != null) {
|
||||||
@@ -40,73 +23,27 @@ const devUpdatesUrl =
|
|||||||
|
|
||||||
const expo = appJson.expo;
|
const expo = appJson.expo;
|
||||||
|
|
||||||
// 检测是否为 Web 平台
|
|
||||||
const isWeb = process.env.EXPO_TARGET === 'web' || process.env.PLATFORM === 'web';
|
|
||||||
|
|
||||||
// 原生平台特有的插件(在 Web 端会报错)
|
|
||||||
const nativeOnlyPlugins = [
|
|
||||||
'expo-camera',
|
|
||||||
'expo-notifications',
|
|
||||||
'expo-background-fetch',
|
|
||||||
'expo-media-library',
|
|
||||||
'expo-image-picker',
|
|
||||||
'expo-video',
|
|
||||||
'expo-sqlite',
|
|
||||||
'./plugins/withMainActivityConfigChange',
|
|
||||||
];
|
|
||||||
|
|
||||||
// 过滤插件:Web 端排除原生插件
|
|
||||||
const filteredPlugins = isWeb
|
|
||||||
? expo.plugins.filter((plugin) => {
|
|
||||||
const pluginName = Array.isArray(plugin) ? plugin[0] : plugin;
|
|
||||||
return !nativeOnlyPlugins.includes(pluginName);
|
|
||||||
})
|
|
||||||
: expo.plugins;
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...expo,
|
...expo,
|
||||||
name: isDevVariant ? `${expo.name} Dev` : expo.name,
|
name: isDevVariant ? `${expo.name} Dev` : expo.name,
|
||||||
// runtimeVersion 用语义版本(expo.version):同一版本号的所有 build 共享 OTA 通道,
|
runtimeVersion: {
|
||||||
// 改原生代码/配置后应升级 version 来切换通道。
|
policy: 'appVersion',
|
||||||
// versionCode / buildNumber 仍用 commit count + 偏移,保证应用商店版本号单调递增。
|
},
|
||||||
runtimeVersion: appJson.expo.version,
|
|
||||||
updates: {
|
updates: {
|
||||||
...(expo.updates || {}),
|
...(expo.updates || {}),
|
||||||
url: isDevVariant ? devUpdatesUrl : releaseUpdatesUrl,
|
url: isDevVariant ? devUpdatesUrl : releaseUpdatesUrl,
|
||||||
checkAutomatically: 'ON_LOAD',
|
checkAutomatically: 'ON_LOAD',
|
||||||
fallbackToCacheTimeout: 0,
|
fallbackToCacheTimeout: 0,
|
||||||
},
|
},
|
||||||
ios: {
|
ios: expo.ios,
|
||||||
...expo.ios,
|
|
||||||
buildNumber: buildNumber,
|
|
||||||
},
|
|
||||||
android: {
|
android: {
|
||||||
...expo.android,
|
...expo.android,
|
||||||
package: 'cn.qczlit.withyou',
|
package: 'skin.carrot.bbs',
|
||||||
versionCode: parseInt(buildNumber, 10),
|
|
||||||
},
|
},
|
||||||
// Web 端使用过滤后的插件
|
|
||||||
plugins: filteredPlugins,
|
|
||||||
extra: {
|
extra: {
|
||||||
...(expo.extra || {}),
|
...(expo.extra || {}),
|
||||||
appVariant: isDevVariant ? 'dev' : 'release',
|
appVariant: isDevVariant ? 'dev' : 'release',
|
||||||
apiBaseUrl: isDevVariant ? devApiBaseUrl : releaseApiBaseUrl,
|
apiBaseUrl: isDevVariant ? devApiBaseUrl : releaseApiBaseUrl,
|
||||||
updatesUrl: isDevVariant ? devUpdatesUrl : releaseUpdatesUrl,
|
updatesUrl: isDevVariant ? devUpdatesUrl : releaseUpdatesUrl,
|
||||||
},
|
},
|
||||||
// Web 端配置
|
|
||||||
...(isWeb && {
|
|
||||||
web: {
|
|
||||||
...expo.web,
|
|
||||||
build: {
|
|
||||||
...expo.web?.build,
|
|
||||||
// 配置 Web 端别名,避免加载原生模块
|
|
||||||
babel: {
|
|
||||||
dangerouslyAddModulePathsToTranspile: [
|
|
||||||
'livekit-client',
|
|
||||||
'@livekit',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
};
|
};
|
||||||
|
|||||||
100
app.json
@@ -1,12 +1,11 @@
|
|||||||
{
|
{
|
||||||
"expo": {
|
"expo": {
|
||||||
"name": "威友",
|
"name": "萝卜社区",
|
||||||
"slug": "qojo",
|
"slug": "qojo",
|
||||||
"version": "1.0.1",
|
"version": "1.0.10",
|
||||||
"orientation": "default",
|
"orientation": "default",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"userInterfaceStyle": "automatic",
|
"userInterfaceStyle": "light",
|
||||||
"scheme": "withyou",
|
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash-icon.png",
|
"image": "./assets/splash-icon.png",
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
@@ -15,31 +14,28 @@
|
|||||||
"ios": {
|
"ios": {
|
||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSMicrophoneUsageDescription": "允许威友访问您的麦克风以进行语音通话",
|
|
||||||
"NSCameraUsageDescription": "允许威友访问您的相机以进行视频通话",
|
|
||||||
"UIBackgroundModes": [
|
"UIBackgroundModes": [
|
||||||
"fetch",
|
"fetch",
|
||||||
"remote-notification",
|
"remote-notification",
|
||||||
"audio"
|
"fetch",
|
||||||
|
"remote-notification"
|
||||||
],
|
],
|
||||||
"ITSAppUsesNonExemptEncryption": false
|
"ITSAppUsesNonExemptEncryption": false
|
||||||
},
|
},
|
||||||
"bundleIdentifier": "cn.qczlit.weiyou"
|
"bundleIdentifier": "skin.carrot.bbs"
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
"googleServicesFile": "./google-services.json",
|
|
||||||
"adaptiveIcon": {
|
"adaptiveIcon": {
|
||||||
"backgroundColor": "#0570F9",
|
"backgroundColor": "#E6F4FE",
|
||||||
"foregroundImage": "./assets/android-icon-foreground.png",
|
"foregroundImage": "./assets/android-icon-foreground.png",
|
||||||
"backgroundImage": "./assets/android-icon-background.png",
|
"backgroundImage": "./assets/android-icon-background.png",
|
||||||
"monochromeImage": "./assets/android-icon-monochrome.png"
|
"monochromeImage": "./assets/android-icon-monochrome.png"
|
||||||
},
|
},
|
||||||
"predictiveBackGestureEnabled": false,
|
"predictiveBackGestureEnabled": false,
|
||||||
"package": "cn.qczlit.withyou",
|
"package": "skin.carrot.bbs",
|
||||||
|
"versionCode": 5,
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"VIBRATE",
|
"VIBRATE",
|
||||||
"RECORD_AUDIO",
|
|
||||||
"CAMERA",
|
|
||||||
"RECEIVE_BOOT_COMPLETED",
|
"RECEIVE_BOOT_COMPLETED",
|
||||||
"WAKE_LOCK",
|
"WAKE_LOCK",
|
||||||
"READ_EXTERNAL_STORAGE",
|
"READ_EXTERNAL_STORAGE",
|
||||||
@@ -52,32 +48,16 @@
|
|||||||
"android.permission.READ_EXTERNAL_STORAGE",
|
"android.permission.READ_EXTERNAL_STORAGE",
|
||||||
"android.permission.WRITE_EXTERNAL_STORAGE",
|
"android.permission.WRITE_EXTERNAL_STORAGE",
|
||||||
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED",
|
"android.permission.READ_MEDIA_VISUAL_USER_SELECTED",
|
||||||
|
"android.permission.ACCESS_MEDIA_LOCATION",
|
||||||
"android.permission.READ_MEDIA_IMAGES",
|
"android.permission.READ_MEDIA_IMAGES",
|
||||||
"android.permission.READ_MEDIA_VIDEO",
|
"android.permission.READ_MEDIA_VIDEO",
|
||||||
"android.permission.READ_MEDIA_AUDIO",
|
"android.permission.READ_MEDIA_AUDIO"
|
||||||
"android.permission.ACCESS_NETWORK_STATE",
|
|
||||||
"android.permission.ACCESS_WIFI_STATE",
|
|
||||||
"android.permission.POST_NOTIFICATIONS"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"./plugins/withCmakeJobLimit",
|
|
||||||
"./plugins/withJcorePatch",
|
|
||||||
"./plugins/withSigning",
|
|
||||||
"./plugins/withMainActivityConfigChange",
|
|
||||||
[
|
|
||||||
"./plugins/withForegroundService",
|
|
||||||
{
|
|
||||||
"notificationTitle": "威友",
|
|
||||||
"notificationBody": "正在后台同步消息",
|
|
||||||
"notificationIcon": "ic_notification",
|
|
||||||
"channelId": "withyou_sync_foreground",
|
|
||||||
"channelName": "消息同步"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"expo-router",
|
"expo-router",
|
||||||
{
|
{
|
||||||
@@ -89,31 +69,30 @@
|
|||||||
],
|
],
|
||||||
"expo-sqlite",
|
"expo-sqlite",
|
||||||
[
|
[
|
||||||
"expo-camera",
|
"expo-notifications",
|
||||||
{
|
{
|
||||||
"cameraPermission": "允许威友访问您的相机以扫描二维码"
|
"sounds": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"expo-background-task",
|
"expo-background-fetch",
|
||||||
{
|
{
|
||||||
"minimumInterval": 15
|
"minimumInterval": 900
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"./plugins/withRemoveAutoStart",
|
|
||||||
[
|
[
|
||||||
"expo-media-library",
|
"expo-media-library",
|
||||||
{
|
{
|
||||||
"photosPermission": "允许威友访问您的照片以发布内容",
|
"photosPermission": "允许萝卜社区访问您的照片以发布内容",
|
||||||
"savePhotosPermission": "允许威友保存照片到您的相册",
|
"savePhotosPermission": "允许萝卜社区保存照片到您的相册",
|
||||||
"isAccessMediaLocationEnabled": false
|
"isAccessMediaLocationEnabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"expo-image-picker",
|
"expo-image-picker",
|
||||||
{
|
{
|
||||||
"photosPermission": "允许威友访问您的照片以选择图片",
|
"photosPermission": "允许萝卜社区访问您的照片以选择图片",
|
||||||
"cameraPermission": "允许威友访问您的相机以拍摄图片",
|
"cameraPermission": "允许萝卜社区访问您的相机以拍摄图片",
|
||||||
"colors": {
|
"colors": {
|
||||||
"cropToolbarColor": "#111827",
|
"cropToolbarColor": "#111827",
|
||||||
"cropToolbarIconColor": "#ffffff",
|
"cropToolbarIconColor": "#ffffff",
|
||||||
@@ -138,44 +117,13 @@
|
|||||||
"proguardRules": "-keep class com.google.android.exoplayer2.** { *; }"
|
"proguardRules": "-keep class com.google.android.exoplayer2.** { *; }"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-font",
|
"expo-font"
|
||||||
[
|
|
||||||
"expo-notifications",
|
|
||||||
{
|
|
||||||
"color": "#E6F4FE",
|
|
||||||
"defaultChannel": "default",
|
|
||||||
"enableBackgroundRemoteNotifications": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"./plugins/withJPush",
|
|
||||||
{
|
|
||||||
"appKey": "2cbe4da12e2c24aa3dca4610",
|
|
||||||
"channel": "developer-default"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"./plugins/withHuaweiPush",
|
|
||||||
"./plugins/withXiaomiPush",
|
|
||||||
"./plugins/withHonorPush",
|
|
||||||
"./plugins/withVivoPush",
|
|
||||||
"./plugins/withOppoPush",
|
|
||||||
"expo-callkit-telecom",
|
|
||||||
[
|
|
||||||
"expo-splash-screen",
|
|
||||||
{
|
|
||||||
"image": "./assets/splash-icon.png",
|
|
||||||
"resizeMode": "contain",
|
|
||||||
"backgroundColor": "#ffffff"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"expo-status-bar"
|
|
||||||
],
|
],
|
||||||
|
"jsEngine": "hermes",
|
||||||
"extra": {
|
"extra": {
|
||||||
"eas": {
|
"eas": {
|
||||||
"projectId": "65540196-d37d-437b-8496-227df0317069"
|
"projectId": "65540196-d37d-437b-8496-227df0317069"
|
||||||
},
|
}
|
||||||
"jpushAppKey": "2cbe4da12e2c24aa3dca4610",
|
|
||||||
"jpushChannel": "developer-default"
|
|
||||||
},
|
},
|
||||||
"owner": "qojo"
|
"owner": "qojo"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,176 +0,0 @@
|
|||||||
import { useMemo, useCallback } from 'react';
|
|
||||||
import { Platform, Pressable, useWindowDimensions } from 'react-native';
|
|
||||||
import { Tabs, usePathname, useRouter } from 'expo-router';
|
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons';
|
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
import type { PressableProps } from 'react-native';
|
|
||||||
|
|
||||||
import { useAppColors, shadows } from '../../../src/theme';
|
|
||||||
import { BREAKPOINTS } from '../../../src/hooks';
|
|
||||||
import { useHomeTabBarVisibilityStore, useTotalUnreadCount, useHomeTabPressStore } from '../../../src/stores';
|
|
||||||
import { hrefHome } from '../../../src/navigation/hrefs';
|
|
||||||
|
|
||||||
const TAB_BAR_HEIGHT = 56;
|
|
||||||
const TAB_BAR_FLOATING_MARGIN = 12;
|
|
||||||
const TAB_BAR_MARGIN = 20;
|
|
||||||
|
|
||||||
const TabBarButton = ({ children, style, ...rest }: PressableProps) => (
|
|
||||||
<Pressable
|
|
||||||
{...rest}
|
|
||||||
style={style}
|
|
||||||
android_ripple={null}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default function TabsLayout() {
|
|
||||||
const colors = useAppColors();
|
|
||||||
const { width } = useWindowDimensions();
|
|
||||||
const insets = useSafeAreaInsets();
|
|
||||||
const pathname = usePathname();
|
|
||||||
const router = useRouter();
|
|
||||||
const unreadCount = useTotalUnreadCount();
|
|
||||||
const scrollHideTabBar = useHomeTabBarVisibilityStore((s) => s.bottomTabBarHiddenByScroll);
|
|
||||||
const triggerHomeTabPress = useHomeTabPressStore((s) => s.triggerPress);
|
|
||||||
const hideTabBar = Platform.OS === 'web' && width >= BREAKPOINTS.desktop;
|
|
||||||
|
|
||||||
const isHomeStackRoute = pathname === '/home' || pathname.startsWith('/home/');
|
|
||||||
|
|
||||||
const handleHomeTabPress = useCallback(
|
|
||||||
(e: { preventDefault: () => void }) => {
|
|
||||||
if (!isHomeStackRoute) return;
|
|
||||||
e.preventDefault();
|
|
||||||
if (pathname === '/home') {
|
|
||||||
triggerHomeTabPress();
|
|
||||||
} else {
|
|
||||||
router.navigate(hrefHome());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[isHomeStackRoute, pathname, triggerHomeTabPress, router]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleAppsTabPress = useCallback(() => {
|
|
||||||
if (pathname.startsWith('/apps/')) {
|
|
||||||
router.replace('/apps');
|
|
||||||
}
|
|
||||||
}, [pathname, router]);
|
|
||||||
|
|
||||||
const handleProfileTabPress = useCallback(() => {
|
|
||||||
if (pathname.startsWith('/profile/')) {
|
|
||||||
router.replace('/profile');
|
|
||||||
}
|
|
||||||
}, [pathname, router]);
|
|
||||||
|
|
||||||
const tabBarStyle = useMemo(() => {
|
|
||||||
if (hideTabBar) {
|
|
||||||
return { display: 'none' as const, height: 0, overflow: 'hidden' as const };
|
|
||||||
}
|
|
||||||
const visibleStyle = {
|
|
||||||
position: 'absolute' as const,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
marginHorizontal: TAB_BAR_MARGIN,
|
|
||||||
bottom: TAB_BAR_FLOATING_MARGIN + insets.bottom,
|
|
||||||
height: TAB_BAR_HEIGHT,
|
|
||||||
borderRadius: 22,
|
|
||||||
backgroundColor: colors.background.paper,
|
|
||||||
...shadows.lg,
|
|
||||||
borderTopWidth: 0,
|
|
||||||
elevation: 100,
|
|
||||||
zIndex: 100,
|
|
||||||
};
|
|
||||||
if (isHomeStackRoute && scrollHideTabBar) {
|
|
||||||
return {
|
|
||||||
...visibleStyle,
|
|
||||||
display: 'none' as const,
|
|
||||||
height: 0,
|
|
||||||
opacity: 0,
|
|
||||||
overflow: 'hidden' as const,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return visibleStyle;
|
|
||||||
}, [hideTabBar, insets.bottom, isHomeStackRoute, scrollHideTabBar, colors]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
screenOptions={{
|
|
||||||
headerShown: false,
|
|
||||||
tabBarActiveTintColor: colors.primary.main,
|
|
||||||
tabBarInactiveTintColor: colors.text.secondary,
|
|
||||||
tabBarStyle,
|
|
||||||
tabBarItemStyle: {
|
|
||||||
borderRadius: 16,
|
|
||||||
marginHorizontal: 2,
|
|
||||||
paddingVertical: 1,
|
|
||||||
},
|
|
||||||
tabBarButton: (props) => <TabBarButton {...props} />,
|
|
||||||
tabBarShowLabel: true,
|
|
||||||
tabBarLabelStyle: { fontSize: 11, fontWeight: '600', marginTop: -1 },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="home"
|
|
||||||
options={{
|
|
||||||
title: '首页',
|
|
||||||
tabBarIcon: ({ color, focused }) => (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name={focused ? 'home' : 'home-outline'}
|
|
||||||
size={focused ? 24 : 22}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
listeners={{
|
|
||||||
tabPress: handleHomeTabPress,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="apps"
|
|
||||||
options={{
|
|
||||||
title: '应用',
|
|
||||||
tabBarIcon: ({ color, focused }) => (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name={focused ? 'view-grid' : 'view-grid-outline'}
|
|
||||||
size={focused ? 24 : 22}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
listeners={{
|
|
||||||
tabPress: handleAppsTabPress,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="messages"
|
|
||||||
options={{
|
|
||||||
title: '消息',
|
|
||||||
tabBarBadge: unreadCount > 0 ? (unreadCount > 99 ? 99 : unreadCount) : undefined,
|
|
||||||
tabBarIcon: ({ color, focused }) => (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name={focused ? 'message-text' : 'message-text-outline'}
|
|
||||||
size={focused ? 24 : 22}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Tabs.Screen
|
|
||||||
name="profile"
|
|
||||||
options={{
|
|
||||||
title: '我的',
|
|
||||||
tabBarIcon: ({ color, focused }) => (
|
|
||||||
<MaterialCommunityIcons
|
|
||||||
name={focused ? 'account' : 'account-outline'}
|
|
||||||
size={focused ? 24 : 22}
|
|
||||||
color={color}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
listeners={{
|
|
||||||
tabPress: handleProfileTabPress,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function AppsStackLayout() {
|
|
||||||
return <Stack screenOptions={{ headerShown: false }} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { AppsScreen } from '../../../../src/screens/apps';
|
|
||||||
|
|
||||||
export default function AppsRoute() {
|
|
||||||
return <AppsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function MaterialsStackLayout() {
|
|
||||||
return (
|
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen name="index" />
|
|
||||||
<Stack.Screen name="subject" />
|
|
||||||
<Stack.Screen name="detail" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { MaterialDetailScreen } from '../../../../../src/screens/material';
|
|
||||||
|
|
||||||
export default function MaterialDetailRoute() {
|
|
||||||
return <MaterialDetailScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { MaterialsScreen } from '../../../../../src/screens/material';
|
|
||||||
|
|
||||||
export default function MaterialsRoute() {
|
|
||||||
return <MaterialsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { SubjectMaterialsScreen } from '../../../../../src/screens/material';
|
|
||||||
|
|
||||||
export default function SubjectMaterialsRoute() {
|
|
||||||
return <SubjectMaterialsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function ScheduleStackLayout() {
|
|
||||||
return (
|
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen name="index" />
|
|
||||||
<Stack.Screen
|
|
||||||
name="course"
|
|
||||||
options={{
|
|
||||||
headerShown: false,
|
|
||||||
presentation: 'transparentModal',
|
|
||||||
animation: 'fade',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { CourseDetailScreen } from '../../../../../src/screens/schedule';
|
|
||||||
|
|
||||||
export default function CourseDetailRoute() {
|
|
||||||
return <CourseDetailScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ScheduleScreen } from '../../../../../src/screens/schedule';
|
|
||||||
|
|
||||||
export default function ScheduleRoute() {
|
|
||||||
return <ScheduleScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function HomeStackLayout() {
|
|
||||||
return <Stack screenOptions={{ headerShown: false }} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { HomeScreen } from '../../../../src/screens/home';
|
|
||||||
|
|
||||||
export default function HomeRoute() {
|
|
||||||
return <HomeScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { SearchScreen } from '../../../../src/screens/home';
|
|
||||||
|
|
||||||
export default function HomeSearchRoute() {
|
|
||||||
return <SearchScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function MessagesStackLayout() {
|
|
||||||
return <Stack screenOptions={{ headerShown: false }} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { MessageListScreen } from '../../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function MessagesRoute() {
|
|
||||||
return <MessageListScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { NotificationsScreen } from '../../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function NotificationsRoute() {
|
|
||||||
return <NotificationsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function ProfileStackLayout() {
|
|
||||||
return (
|
|
||||||
<Stack
|
|
||||||
screenOptions={{
|
|
||||||
headerShown: false,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack.Screen name="index" />
|
|
||||||
<Stack.Screen name="settings" />
|
|
||||||
<Stack.Screen name="edit-profile" />
|
|
||||||
<Stack.Screen name="account-security" />
|
|
||||||
<Stack.Screen name="my-posts" />
|
|
||||||
<Stack.Screen name="bookmarks" />
|
|
||||||
<Stack.Screen name="notification-settings" />
|
|
||||||
<Stack.Screen name="blocked-users" />
|
|
||||||
<Stack.Screen name="chat-settings" />
|
|
||||||
<Stack.Screen name="about" />
|
|
||||||
<Stack.Screen name="help" />
|
|
||||||
<Stack.Screen name="verification" />
|
|
||||||
<Stack.Screen name="data-storage" />
|
|
||||||
<Stack.Screen name="privacy-settings" />
|
|
||||||
<Stack.Screen name="account-deletion" />
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { AboutScreen } from '../../../../src/screens/profile/AboutScreen';
|
|
||||||
|
|
||||||
export default function AboutRoute() {
|
|
||||||
return <AboutScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { AccountDeletionScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function AccountDeletionRoute() {
|
|
||||||
return <AccountDeletionScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { AccountSecurityScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function AccountSecurityRoute() {
|
|
||||||
return <AccountSecurityScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { BlockedUsersScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function BlockedUsersRoute() {
|
|
||||||
return <BlockedUsersScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ProfileScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function BookmarksRoute() {
|
|
||||||
return <ProfileScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ChatSettingsScreen } from '../../../../src/screens/profile/ChatSettingsScreen';
|
|
||||||
|
|
||||||
export default function ChatSettingsRoute() {
|
|
||||||
return <ChatSettingsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { DataStorageScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function DataStorageRoute() {
|
|
||||||
return <DataStorageScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { EditProfileScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function EditProfileRoute() {
|
|
||||||
return <EditProfileScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { HelpScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function HelpRoute() {
|
|
||||||
return <HelpScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ProfileScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function ProfileRoute() {
|
|
||||||
return <ProfileScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ProfileScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function MyPostsRoute() {
|
|
||||||
return <ProfileScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { NotificationSettingsScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function NotificationSettingsRoute() {
|
|
||||||
return <NotificationSettingsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrivacySettingsScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function PrivacySettingsRoute() {
|
|
||||||
return <PrivacySettingsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { SettingsScreen } from '../../../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function SettingsRoute() {
|
|
||||||
return <SettingsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { VerificationSettingsScreen } from '@/screens/profile/VerificationSettingsScreen';
|
|
||||||
|
|
||||||
export default function VerificationSettingsPage() {
|
|
||||||
return <VerificationSettingsScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ActivityIndicator, View } from 'react-native';
|
|
||||||
import { Redirect } from 'expo-router';
|
|
||||||
|
|
||||||
import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
|
|
||||||
import { messageManager, useAuthStore } from '../../src/stores';
|
|
||||||
import { useRegisterPushDevice } from '../../src/hooks';
|
|
||||||
import { hrefAuthLogin } from '../../src/navigation/hrefs';
|
|
||||||
import { useAppColors } from '../../src/theme';
|
|
||||||
|
|
||||||
export default function AppLayout() {
|
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
||||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
|
||||||
const userID = useAuthStore((s) => s.currentUser?.id);
|
|
||||||
const colors = useAppColors();
|
|
||||||
const [verified, setVerified] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 冷启动 token 校验:仅鉴权分组 (app) 需要。
|
|
||||||
// 公开页(/privacy、/terms)和 (auth) 分组在根布局直接渲染,不受此校验影响,
|
|
||||||
// 因此冷启动 401 不会把公开页顶到 /login。
|
|
||||||
fetchCurrentUser().finally(() => setVerified(true));
|
|
||||||
}, [fetchCurrentUser]);
|
|
||||||
|
|
||||||
useRegisterPushDevice(isAuthenticated, userID);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
messageManager.initialize();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 持久化状态显示已登录则立即渲染(后台静默校验)
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return <AppRouteStack />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未登录且校验未完成,显示 loading
|
|
||||||
if (!verified) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.background.default,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验完成仍未登录,跳转登录页
|
|
||||||
return <Redirect href={hrefAuthLogin()} />;
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { ActivityIndicator, View, useWindowDimensions } from 'react-native';
|
|
||||||
import { Redirect } from 'expo-router';
|
|
||||||
|
|
||||||
import { AppDesktopShell } from '../../src/app-navigation/AppDesktopShell';
|
|
||||||
import { AppRouteStack } from '../../src/app-navigation/AppRouteStack';
|
|
||||||
import { BREAKPOINTS } from '../../src/hooks';
|
|
||||||
import { messageManager, useAuthStore } from '../../src/stores';
|
|
||||||
import { hrefAuthLogin } from '../../src/navigation/hrefs';
|
|
||||||
import { useAppColors } from '../../src/theme';
|
|
||||||
|
|
||||||
export default function AppLayoutWeb() {
|
|
||||||
const { width } = useWindowDimensions();
|
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
||||||
const fetchCurrentUser = useAuthStore((s) => s.fetchCurrentUser);
|
|
||||||
const useDesktopShell = width >= BREAKPOINTS.desktop;
|
|
||||||
const colors = useAppColors();
|
|
||||||
const [verified, setVerified] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// 冷启动 token 校验:仅鉴权分组 (app) 需要。
|
|
||||||
// 公开页(/privacy、/terms)和 (auth) 分组在根布局直接渲染,不受此校验影响。
|
|
||||||
fetchCurrentUser().finally(() => setVerified(true));
|
|
||||||
}, [fetchCurrentUser]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
messageManager.initialize();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 持久化状态显示已登录则立即渲染(后台静默校验)
|
|
||||||
if (isAuthenticated) {
|
|
||||||
if (useDesktopShell) {
|
|
||||||
return <AppDesktopShell />;
|
|
||||||
}
|
|
||||||
return <AppRouteStack />;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未登录且校验未完成,显示 loading
|
|
||||||
if (!verified) {
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
flex: 1,
|
|
||||||
justifyContent: 'center',
|
|
||||||
alignItems: 'center',
|
|
||||||
backgroundColor: colors.background.default,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ActivityIndicator size="large" color={colors.primary.main} />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 校验完成仍未登录,跳转登录页
|
|
||||||
return <Redirect href={hrefAuthLogin()} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ChatScreen } from '../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function ChatRoute() {
|
|
||||||
return <ChatScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { MessageSearchScreen } from '../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function MessageSearchRoute() {
|
|
||||||
return <MessageSearchScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrivateChatInfoScreen } from '../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function PrivateChatInfoRoute() {
|
|
||||||
return <PrivateChatInfoScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { GroupInfoScreen } from '../../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function GroupInfoRoute() {
|
|
||||||
return <GroupInfoScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { GroupMembersScreen } from '../../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function GroupMembersRoute() {
|
|
||||||
return <GroupMembersScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { CreateGroupScreen } from '../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function CreateGroupRoute() {
|
|
||||||
return <CreateGroupScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import GroupInviteDetailScreen from '../../../src/screens/message/GroupInviteDetailScreen';
|
|
||||||
|
|
||||||
export default function GroupInviteRoute() {
|
|
||||||
return <GroupInviteDetailScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { JoinGroupScreen } from '../../../src/screens/message';
|
|
||||||
|
|
||||||
export default function JoinGroupRoute() {
|
|
||||||
return <JoinGroupScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import GroupRequestDetailScreen from '../../../src/screens/message/GroupRequestDetailScreen';
|
|
||||||
|
|
||||||
export default function GroupRequestRoute() {
|
|
||||||
return <GroupRequestDetailScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { CreatePostScreen } from '../../../src/screens/create';
|
|
||||||
|
|
||||||
export default function CreatePostRoute() {
|
|
||||||
return <CreatePostScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { QRCodeConfirmScreen } from '../../../../src/screens/auth/QRCodeConfirmScreen';
|
|
||||||
|
|
||||||
export default function QrLoginConfirmRoute() {
|
|
||||||
return <QRCodeConfirmScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import FollowListScreen from '../../../../src/screens/profile/FollowListScreen';
|
|
||||||
|
|
||||||
export default function FollowListRoute() {
|
|
||||||
return <FollowListScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { Stack } from 'expo-router';
|
|
||||||
|
|
||||||
export default function AuthLayout() {
|
|
||||||
return <Stack screenOptions={{ headerShown: false }} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { ForgotPasswordScreen } from '../../src/screens/auth';
|
|
||||||
|
|
||||||
export default function ForgotPasswordRoute() {
|
|
||||||
return <ForgotPasswordScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { LoginScreen } from '../../src/screens/auth';
|
|
||||||
|
|
||||||
export default function LoginRoute() {
|
|
||||||
return <LoginScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { RegisterScreen } from '../../src/screens/auth';
|
|
||||||
|
|
||||||
export default function RegisterRoute() {
|
|
||||||
return <RegisterScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { VerificationFormScreen } from '@/screens/auth/VerificationFormScreen';
|
|
||||||
|
|
||||||
export default function VerificationFormPage() {
|
|
||||||
return <VerificationFormScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { VerificationGuideScreen } from '@/screens/auth/VerificationGuideScreen';
|
|
||||||
|
|
||||||
export default function VerificationGuidePage() {
|
|
||||||
return <VerificationGuideScreen />;
|
|
||||||
}
|
|
||||||
307
app/_layout.tsx
@@ -1,307 +0,0 @@
|
|||||||
import '../src/polyfills';
|
|
||||||
import React, { useEffect, useRef } from 'react';
|
|
||||||
import { AppState, AppStateStatus, Platform, View, ActivityIndicator } from 'react-native';
|
|
||||||
import { Stack, useRouter } from 'expo-router';
|
|
||||||
import { StatusBar } from 'expo-status-bar';
|
|
||||||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
|
||||||
import { PaperProvider } from 'react-native-paper';
|
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
||||||
import * as SystemUI from 'expo-system-ui';
|
|
||||||
import { useFonts } from 'expo-font';
|
|
||||||
|
|
||||||
import { registerNotificationPresentationHandler } from '@/services/notification';
|
|
||||||
import { EventSubscriber } from '../src/infrastructure/EventSubscriber';
|
|
||||||
import {
|
|
||||||
ThemeBootstrap,
|
|
||||||
useAppColors,
|
|
||||||
usePaperThemeFromStore,
|
|
||||||
useResolvedColorScheme,
|
|
||||||
} from '../src/theme';
|
|
||||||
|
|
||||||
import AppPromptBar from '../src/components/common/AppPromptBar';
|
|
||||||
import AppDialogHost from '../src/components/common/AppDialogHost';
|
|
||||||
import { installAlertOverride } from '@/services/ui';
|
|
||||||
import { checkForAPKUpdate } from '@/services/platform';
|
|
||||||
import { CallScreen, IncomingCallModal, FloatingCallWindow } from '../src/components/call';
|
|
||||||
import { jpushService } from '@/services/notification/jpushService';
|
|
||||||
import { callKeepService } from '@/services/callkeep';
|
|
||||||
import { callStore } from '@/stores/call';
|
|
||||||
import * as hrefs from '../src/navigation/hrefs';
|
|
||||||
|
|
||||||
registerNotificationPresentationHandler();
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
|
||||||
defaultOptions: {
|
|
||||||
queries: {
|
|
||||||
retry: 2,
|
|
||||||
staleTime: 5 * 60 * 1000,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
installAlertOverride();
|
|
||||||
|
|
||||||
if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
input:focus, textarea:focus, select:focus {
|
|
||||||
outline: none !important;
|
|
||||||
outline-width: 0 !important;
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
input:focus-visible, textarea:focus-visible {
|
|
||||||
outline: none !important;
|
|
||||||
}
|
|
||||||
*:focus { outline: none !important; }
|
|
||||||
*:focus-visible { outline: none !important; }
|
|
||||||
/* 修复移动端滑动问题 - 仅对根容器限制 */
|
|
||||||
html, body {
|
|
||||||
overscroll-behavior: none;
|
|
||||||
touch-action: manipulation;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
/* React Native Web 生成的滚动容器 - 允许双向滑动 */
|
|
||||||
[class*="css-view"] {
|
|
||||||
touch-action: auto;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
/* 水平滚动容器 */
|
|
||||||
[data-horizontal-scroll="true"],
|
|
||||||
div[style*="overflow-x"],
|
|
||||||
div[style*="overflow: scroll"],
|
|
||||||
div[style*="overflow: auto"] {
|
|
||||||
touch-action: pan-x pan-y;
|
|
||||||
-webkit-overflow-scrolling: touch;
|
|
||||||
}
|
|
||||||
/* 手势区域 - 允许滑动手势 */
|
|
||||||
[data-gesture-area="true"] {
|
|
||||||
touch-action: pan-x pan-y;
|
|
||||||
}
|
|
||||||
/* 图片查看器手势区域 */
|
|
||||||
.react-native-image-viewer,
|
|
||||||
[data-image-viewer="true"] {
|
|
||||||
touch-action: pinch-zoom pan-x pan-y;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SystemChrome() {
|
|
||||||
const colors = useAppColors();
|
|
||||||
useEffect(() => {
|
|
||||||
void SystemUI.setBackgroundColorAsync(colors.background.default);
|
|
||||||
if (Platform.OS === 'web' && typeof document !== 'undefined') {
|
|
||||||
const meta = document.querySelector('meta[name="theme-color"]');
|
|
||||||
if (meta) {
|
|
||||||
meta.setAttribute('content', colors.background.default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [colors.background.default]);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationBootstrap() {
|
|
||||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
|
||||||
const permissionRequested = useRef(false);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const initNotifications = async () => {
|
|
||||||
const { loadNotificationPreferences } = await import('@/services/notification');
|
|
||||||
await loadNotificationPreferences();
|
|
||||||
const { systemNotificationService } = await import('@/services/notification');
|
|
||||||
await systemNotificationService.initialize();
|
|
||||||
const { initBackgroundService } = await import('@/services/background');
|
|
||||||
// 默认静默模式,不会注册后台任务,不会触发自启动
|
|
||||||
await initBackgroundService();
|
|
||||||
|
|
||||||
const subscription = AppState.addEventListener('change', (nextAppState) => {
|
|
||||||
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
|
|
||||||
systemNotificationService.clearBadge();
|
|
||||||
}
|
|
||||||
appState.current = nextAppState;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen for JPush notification taps — navigate to the relevant screen
|
|
||||||
jpushService.onNotification((message) => {
|
|
||||||
console.log('[NotificationBootstrap] JPush notification:', message.notificationEventType, message);
|
|
||||||
if (message.notificationEventType !== 'notificationOpened') return;
|
|
||||||
|
|
||||||
// 点击任意一条通知即清除通知栏所有通知(与 QQ 一致:点一条清全部)
|
|
||||||
// JPush 的 notificationOpened 在通知被点击时触发,此时清除系统通知栏中
|
|
||||||
// 本应用的所有通知,避免用户需要逐条点击消除。
|
|
||||||
systemNotificationService.clearAllNotifications().catch((error) => {
|
|
||||||
console.warn('[NotificationBootstrap] clearAllNotifications failed:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
const extras = message.extras || {};
|
|
||||||
const notifType = extras.notification_type || message.notificationType;
|
|
||||||
|
|
||||||
if (notifType === 'chat_message' || notifType === 'chat') {
|
|
||||||
const conversationId = extras.conversation_id || extras.conversationId;
|
|
||||||
const senderId = extras.sender_id || extras.senderId;
|
|
||||||
const conversationType = extras.conversation_type || extras.conversationType;
|
|
||||||
const groupId = extras.group_id || extras.groupId;
|
|
||||||
const groupName = extras.group_name || extras.groupName;
|
|
||||||
const groupAvatar = extras.group_avatar || extras.groupAvatar;
|
|
||||||
|
|
||||||
if (conversationId) {
|
|
||||||
const isGroup = conversationType === 'group';
|
|
||||||
// 使用 navigate 而非 push,避免:
|
|
||||||
// 1. 在已有聊天页上创建重复实例(用户需要退出两次)
|
|
||||||
// 2. 从帖子等页面打开聊天通知后返回到帖子(而非消息列表)
|
|
||||||
router.navigate(
|
|
||||||
hrefs.hrefChat({
|
|
||||||
conversationId,
|
|
||||||
userId: isGroup ? undefined : senderId,
|
|
||||||
isGroupChat: isGroup,
|
|
||||||
groupId: isGroup ? groupId : undefined,
|
|
||||||
groupName: isGroup ? groupName : undefined,
|
|
||||||
groupAvatar: isGroup ? groupAvatar : undefined,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
router.navigate(hrefs.hrefNotifications());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
router.navigate(hrefs.hrefNotifications());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
subscription.remove();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestPermissionOnLaunch = async () => {
|
|
||||||
if (permissionRequested.current) return;
|
|
||||||
permissionRequested.current = true;
|
|
||||||
|
|
||||||
if (Platform.OS === 'web') return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const hasPermission = await jpushService.checkNotificationPermission();
|
|
||||||
if (!hasPermission) {
|
|
||||||
await jpushService.requestNotificationPermission();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('[NotificationBootstrap] request permission on launch failed:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
initNotifications();
|
|
||||||
requestPermissionOnLaunch();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function APKUpdateBootstrap() {
|
|
||||||
const hasChecked = useRef(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (hasChecked.current) return;
|
|
||||||
hasChecked.current = true;
|
|
||||||
|
|
||||||
// 延迟执行,避免与启动流程冲突
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
checkForAPKUpdate().catch((error) => {
|
|
||||||
console.error('APK update check failed:', error);
|
|
||||||
});
|
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function CallKeepBootstrap() {
|
|
||||||
useEffect(() => {
|
|
||||||
if (Platform.OS === 'web') return;
|
|
||||||
|
|
||||||
callKeepService.initialize({
|
|
||||||
onSystemAnswered: () => {
|
|
||||||
callStore.getState().handleSystemAnswer();
|
|
||||||
},
|
|
||||||
onSystemEnded: () => {
|
|
||||||
callStore.getState().endCall('ended');
|
|
||||||
},
|
|
||||||
onSystemMuted: (muted: boolean) => {
|
|
||||||
callStore.getState().handleSystemMuted(muted);
|
|
||||||
},
|
|
||||||
onIncomingCallFromPush: (session) => {
|
|
||||||
callStore.getState().handleIncomingFromPush(session);
|
|
||||||
},
|
|
||||||
onOutgoingStarted: () => {
|
|
||||||
// Outgoing call accepted by system — no action needed,
|
|
||||||
// the WS call_invited / call_accepted flow handles everything
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
callKeepService.dispose();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ThemedStack() {
|
|
||||||
const router = useRouter();
|
|
||||||
const resolved = useResolvedColorScheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
|
|
||||||
<SystemChrome />
|
|
||||||
<EventSubscriber />
|
|
||||||
<NotificationBootstrap />
|
|
||||||
<APKUpdateBootstrap />
|
|
||||||
<CallKeepBootstrap />
|
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
|
||||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="(auth)" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="(app)" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="privacy" options={{ headerShown: false }} />
|
|
||||||
<Stack.Screen name="terms" options={{ headerShown: false }} />
|
|
||||||
</Stack>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ThemedProviders({ children }: { children: React.ReactNode }) {
|
|
||||||
const theme = usePaperThemeFromStore();
|
|
||||||
return <PaperProvider theme={theme}>{children}</PaperProvider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RootLayout() {
|
|
||||||
const [fontsLoaded] = useFonts({});
|
|
||||||
|
|
||||||
if (!fontsLoaded) {
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
|
|
||||||
<ActivityIndicator />
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
||||||
<SafeAreaProvider>
|
|
||||||
<ThemedProviders>
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<ThemeBootstrap />
|
|
||||||
<ThemedStack />
|
|
||||||
<AppPromptBar />
|
|
||||||
<AppDialogHost />
|
|
||||||
<IncomingCallModal />
|
|
||||||
<CallScreen />
|
|
||||||
<FloatingCallWindow />
|
|
||||||
</QueryClientProvider>
|
|
||||||
</ThemedProviders>
|
|
||||||
</SafeAreaProvider>
|
|
||||||
</GestureHandlerRootView>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Redirect } from 'expo-router';
|
|
||||||
|
|
||||||
import { useAuthStore } from '../src/stores';
|
|
||||||
import { hrefAuthWelcome, hrefHome } from '../src/navigation/hrefs';
|
|
||||||
|
|
||||||
export default function Index() {
|
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
|
||||||
if (isAuthenticated) {
|
|
||||||
return <Redirect href={hrefHome()} />;
|
|
||||||
}
|
|
||||||
return <Redirect href={hrefAuthWelcome()} />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PostDetailScreen } from '../../src/screens/home';
|
|
||||||
|
|
||||||
export default function PostDetailRoute() {
|
|
||||||
return <PostDetailScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { PrivacyPolicyScreen } from '../src/screens/profile/PrivacyPolicyScreen';
|
|
||||||
|
|
||||||
export default function PrivacyRoute() {
|
|
||||||
return <PrivacyPolicyScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { TermsOfServiceScreen } from '../src/screens/profile/TermsOfServiceScreen';
|
|
||||||
|
|
||||||
export default function TermsRoute() {
|
|
||||||
return <TermsOfServiceScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { TradeDetailScreen } from '../../src/screens/trade/TradeDetailScreen';
|
|
||||||
import { useLocalSearchParams } from 'expo-router';
|
|
||||||
|
|
||||||
export default function TradeDetailRoute() {
|
|
||||||
const { tradeId } = useLocalSearchParams<{ tradeId: string }>();
|
|
||||||
return <TradeDetailScreen tradeId={tradeId} />;
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
||||||
import { CreateTradeScreen } from '../../../src/screens/trade/CreateTradeScreen';
|
|
||||||
import { tradeService } from '../../../src/services/trade/tradeService';
|
|
||||||
import { Loading } from '../../../src/components/common';
|
|
||||||
import { View, Alert } from 'react-native';
|
|
||||||
import type { TradeItemDetailDTO } from '../../../src/types/trade';
|
|
||||||
|
|
||||||
export default function EditTradeRoute() {
|
|
||||||
const { tradeId } = useLocalSearchParams<{ tradeId: string }>();
|
|
||||||
const router = useRouter();
|
|
||||||
const [item, setItem] = useState<TradeItemDetailDTO | null>(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!tradeId) return;
|
|
||||||
tradeService.getTradeItem(tradeId)
|
|
||||||
.then(data => setItem(data))
|
|
||||||
.catch(() => Alert.alert('错误', '加载商品信息失败'))
|
|
||||||
.finally(() => setLoading(false));
|
|
||||||
}, [tradeId]);
|
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
|
||||||
router.back();
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
const handleSuccess = useCallback(() => {
|
|
||||||
router.back();
|
|
||||||
}, [router]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading fullScreen />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!item) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CreateTradeScreen
|
|
||||||
onClose={handleClose}
|
|
||||||
onSuccess={handleSuccess}
|
|
||||||
editItem={item}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { UserScreen } from '../../src/screens/profile';
|
|
||||||
|
|
||||||
export default function UserProfileRoute() {
|
|
||||||
return <UserScreen />;
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import { WelcomeScreen } from '../src/screens/auth';
|
|
||||||
|
|
||||||
export default function WelcomeRoute() {
|
|
||||||
return <WelcomeScreen />;
|
|
||||||
}
|
|
||||||
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.5 KiB |
BIN
assets/icon.png
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 895 KiB |
|
Before Width: | Height: | Size: 385 KiB After Width: | Height: | Size: 895 KiB |
1725
docs/OPTIMIZATION_DESIGN.md
Normal file
3
eas.json
@@ -27,9 +27,6 @@
|
|||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"autoIncrement": true,
|
"autoIncrement": true,
|
||||||
"android": {
|
|
||||||
"buildType": "apk"
|
|
||||||
},
|
|
||||||
"ios": {
|
"ios": {
|
||||||
"resourceClass": "m-medium"
|
"resourceClass": "m-medium"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"project_info": {
|
|
||||||
"project_number": "000000000000",
|
|
||||||
"project_id": "withyou-dummy",
|
|
||||||
"storage_bucket": "withyou-dummy.appspot.com"
|
|
||||||
},
|
|
||||||
"client": [
|
|
||||||
{
|
|
||||||
"client_info": {
|
|
||||||
"mobilesdk_app_id": "1:000000000000:android:0000000000000000",
|
|
||||||
"android_client_info": {
|
|
||||||
"package_name": "cn.qczlit.withyou"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"api_key": [
|
|
||||||
{
|
|
||||||
"current_key": "AIzaSyDummyKeyForFirebaseInitOnly"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"configuration_version": "1"
|
|
||||||
}
|
|
||||||
9
index.ts
@@ -1 +1,8 @@
|
|||||||
import 'expo-router/entry';
|
import { registerRootComponent } from 'expo';
|
||||||
|
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
|
||||||
|
// It also ensures that whether you load the app in Expo Go or in a native build,
|
||||||
|
// the environment is set up appropriately
|
||||||
|
registerRootComponent(App);
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
/**
|
|
||||||
* Jest 配置
|
|
||||||
*
|
|
||||||
* 仅对消息状态核心逻辑(store 幂等性 / 实时事件管道 / 未读计数原子性)做单测,
|
|
||||||
* 不覆盖 RN / Expo UI。所有平台依赖(@/services/core、@/database、@/core/events、
|
|
||||||
* @/stores/auth、@/services/message)在测试中通过 moduleNameMapper 映射到 mock。
|
|
||||||
*/
|
|
||||||
const { pathsToModuleNameMapper } = require('ts-jest');
|
|
||||||
|
|
||||||
const tsconfig = require('./tsconfig.json');
|
|
||||||
const tsconfigBase = require('expo/tsconfig.base.json');
|
|
||||||
|
|
||||||
// 合并 base + 项目 paths
|
|
||||||
const paths = {
|
|
||||||
...(tsconfigBase.compilerOptions.paths || {}),
|
|
||||||
...(tsconfig.compilerOptions.paths || {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
preset: 'ts-jest',
|
|
||||||
testEnvironment: 'node',
|
|
||||||
roots: ['<rootDir>/src'],
|
|
||||||
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
||||||
moduleNameMapper: {
|
|
||||||
// 平台依赖统一映射到 mock(必须放在 @/* 通配之前,否则会被通配吞掉)
|
|
||||||
'^@/services/core$': '<rootDir>/src/stores/message/__tests__/mocks/coreMock.ts',
|
|
||||||
'^@/services/message$': '<rootDir>/src/stores/message/__tests__/mocks/messageServiceMock.ts',
|
|
||||||
'^@/database$': '<rootDir>/src/stores/message/__tests__/mocks/databaseMock.ts',
|
|
||||||
'^@/core/events/EventBus$': '<rootDir>/src/stores/message/__tests__/mocks/eventBusMock.ts',
|
|
||||||
'^@/stores/auth/authStore$': '<rootDir>/src/stores/message/__tests__/mocks/authStoreMock.ts',
|
|
||||||
'^@react-native-async-storage/async-storage$':
|
|
||||||
'<rootDir>/src/stores/message/__tests__/mocks/asyncStorageMock.ts',
|
|
||||||
// @/* 路径别名 → 真实 src(通配兜底,放最后)
|
|
||||||
...pathsToModuleNameMapper(paths, { prefix: '<rootDir>/' }),
|
|
||||||
},
|
|
||||||
transform: {
|
|
||||||
'^.+\\.tsx?$': [
|
|
||||||
'ts-jest',
|
|
||||||
{
|
|
||||||
tsconfig: '<rootDir>/src/stores/message/__tests__/tsconfig.json',
|
|
||||||
isolatedModules: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
clearMocks: true,
|
|
||||||
};
|
|
||||||
@@ -1,72 +1,11 @@
|
|||||||
/** @type {import('expo/metro-config').MetroConfig} */
|
/** @type {import('expo/metro-config').MetroConfig} */
|
||||||
const { getDefaultConfig } = require('expo/metro-config');
|
const { getDefaultConfig } = require('expo/metro-config');
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const config = getDefaultConfig(__dirname);
|
const config = getDefaultConfig(__dirname);
|
||||||
|
|
||||||
// Add wasm asset support
|
// Add wasm asset support
|
||||||
config.resolver.assetExts.push('wasm');
|
config.resolver.assetExts.push('wasm');
|
||||||
|
|
||||||
// Support TypeScript path aliases (@/ -> ./src/)
|
|
||||||
config.resolver.alias = {
|
|
||||||
'@': path.resolve(__dirname, 'src'),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure platform-specific files are resolved correctly
|
|
||||||
config.resolver.platforms = ['ios', 'android', 'native', 'web'];
|
|
||||||
|
|
||||||
// Web shims for native-only React Native APIs that don't exist in react-native-web.
|
|
||||||
// Packages like react-native-pager-view import codegenNativeComponent from
|
|
||||||
// react-native/Libraries/Utilities/codegenNativeComponent, which calls
|
|
||||||
// requireNativeComponent internally. On web (react-native-web), these APIs
|
|
||||||
// don't exist, causing "(0 , _reactNativeWebDistIndex.requireNativeComponent)
|
|
||||||
// is not a function".
|
|
||||||
const webShimPaths = [
|
|
||||||
'react-native/Libraries/Utilities/codegenNativeComponent',
|
|
||||||
'react-native/Libraries/Utilities/codegenNativeCommands',
|
|
||||||
'react-native/Libraries/Types/CodegenTypes',
|
|
||||||
'react-native/Libraries/ReactNative/requireNativeComponent',
|
|
||||||
];
|
|
||||||
|
|
||||||
// Packages that should be entirely replaced with web shims on web platform.
|
|
||||||
// These packages import requireNativeComponent directly from 'react-native'
|
|
||||||
// (which resolves to react-native-web on web) and don't have web implementations.
|
|
||||||
const webPackageShims = {
|
|
||||||
};
|
|
||||||
|
|
||||||
const originalResolveRequest = config.resolver.resolveRequest;
|
|
||||||
const shimDir = path.resolve(__dirname, 'web-shims');
|
|
||||||
|
|
||||||
config.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
||||||
// Redirect entire native-only packages to web shims
|
|
||||||
if (platform === 'web' && webPackageShims[moduleName]) {
|
|
||||||
return {
|
|
||||||
type: 'sourceFile',
|
|
||||||
filePath: webPackageShims[moduleName],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect specific React Native internal modules to web shims
|
|
||||||
if (platform === 'web' && webShimPaths.some((p) => moduleName.endsWith(p))) {
|
|
||||||
// For requireNativeComponent, the path doesn't follow the Libraries/* pattern
|
|
||||||
if (moduleName.endsWith('react-native/Libraries/ReactNative/requireNativeComponent')) {
|
|
||||||
return {
|
|
||||||
type: 'sourceFile',
|
|
||||||
filePath: path.join(shimDir, 'react-native', 'Libraries', 'ReactNative', 'requireNativeComponent.js'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const shimName = moduleName.replace('react-native/Libraries/', '');
|
|
||||||
return {
|
|
||||||
type: 'sourceFile',
|
|
||||||
filePath: path.join(shimDir, 'react-native', 'Libraries', shimName + '.js'),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (originalResolveRequest) {
|
|
||||||
return originalResolveRequest(context, moduleName, platform);
|
|
||||||
}
|
|
||||||
return context.resolveRequest(context, moduleName, platform);
|
|
||||||
};
|
|
||||||
|
|
||||||
// NOTE: enhanceMiddleware is marked as deprecated in Metro's types,
|
// NOTE: enhanceMiddleware is marked as deprecated in Metro's types,
|
||||||
// but there's currently no official alternative for custom dev server headers.
|
// but there's currently no official alternative for custom dev server headers.
|
||||||
// For production, configure COEP/COOP headers on your hosting platform.
|
// For production, configure COEP/COOP headers on your hosting platform.
|
||||||
|
|||||||
19
nginx.conf
@@ -1,19 +0,0 @@
|
|||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name _;
|
|
||||||
root /usr/share/nginx/html;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
|
||||||
gzip_min_length 256;
|
|
||||||
}
|
|
||||||
9452
package-lock.json
generated
129
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "with_you",
|
"name": "carrot_bbs",
|
||||||
"version": "1.0.1",
|
"version": "1.0.0",
|
||||||
"main": "expo-router/entry",
|
"main": "index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "expo start",
|
"start": "expo start",
|
||||||
"android": "expo run:android",
|
"android": "expo run:android",
|
||||||
@@ -11,91 +11,58 @@
|
|||||||
"ios:simulator": "eas build --platform ios --profile ios-simulator",
|
"ios:simulator": "eas build --platform ios --profile ios-simulator",
|
||||||
"ios:preview": "eas build --platform ios --profile preview",
|
"ios:preview": "eas build --platform ios --profile preview",
|
||||||
"ios:prod": "eas build --platform ios --profile production",
|
"ios:prod": "eas build --platform ios --profile production",
|
||||||
"ios:submit": "eas submit --platform ios --profile production",
|
"ios:submit": "eas submit --platform ios --profile production"
|
||||||
"test": "jest",
|
|
||||||
"test:watch": "jest --watch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/ui": "~56.0.18",
|
|
||||||
"@expo/vector-icons": "^15.1.1",
|
"@expo/vector-icons": "^15.1.1",
|
||||||
"@livekit/react-native": "^2.11.0",
|
"@react-native-async-storage/async-storage": "^2.2.0",
|
||||||
"@livekit/react-native-webrtc": "^144.1.0",
|
"@react-navigation/bottom-tabs": "^7.15.2",
|
||||||
"@react-native-async-storage/async-storage": "^3.1.1",
|
"@react-navigation/material-top-tabs": "^7.4.16",
|
||||||
"@shopify/flash-list": "^2.3.1",
|
"@react-navigation/native": "^7.1.31",
|
||||||
"@tanstack/react-query": "^5.100.14",
|
"@react-navigation/native-stack": "^7.14.2",
|
||||||
"@types/react": "~19.2.14",
|
"@shopify/flash-list": "2.0.2",
|
||||||
"axios": "^1.16.1",
|
"@tanstack/react-query": "^5.90.21",
|
||||||
"date-fns": "^4.4.0",
|
"axios": "^1.13.6",
|
||||||
"expo": "~56.0.12",
|
"date-fns": "^4.1.0",
|
||||||
"expo-background-task": "~56.0.19",
|
"expo": "~55.0.4",
|
||||||
"expo-callkit-telecom": "^0.3.9",
|
"expo-background-fetch": "~55.0.9",
|
||||||
"expo-camera": "~56.0.8",
|
"expo-constants": "~55.0.7",
|
||||||
"expo-constants": "~56.0.18",
|
"expo-dev-client": "~55.0.10",
|
||||||
"expo-dev-client": "~56.0.20",
|
"expo-file-system": "~55.0.10",
|
||||||
"expo-document-picker": "~56.0.4",
|
"expo-font": "~55.0.4",
|
||||||
"expo-file-system": "~56.0.8",
|
"expo-haptics": "~55.0.8",
|
||||||
"expo-font": "~56.0.7",
|
"expo-image": "^55.0.5",
|
||||||
"expo-haptics": "~56.0.3",
|
"expo-image-picker": "^55.0.10",
|
||||||
"expo-image": "~56.0.6",
|
"expo-linear-gradient": "^55.0.8",
|
||||||
"expo-image-picker": "~56.0.18",
|
"expo-media-library": "~55.0.9",
|
||||||
"expo-intent-launcher": "~56.0.4",
|
"expo-notifications": "^55.0.10",
|
||||||
"expo-linear-gradient": "~56.0.4",
|
"expo-router": "^55.0.4",
|
||||||
"expo-linking": "~56.0.14",
|
"expo-sqlite": "~55.0.10",
|
||||||
"expo-media-library": "~56.0.7",
|
"expo-status-bar": "~55.0.4",
|
||||||
"expo-notifications": "~56.0.18",
|
"expo-system-ui": "^55.0.9",
|
||||||
"expo-router": "~56.2.11",
|
"expo-task-manager": "~55.0.9",
|
||||||
"expo-splash-screen": "~56.0.10",
|
"expo-updates": "^55.0.12",
|
||||||
"expo-sqlite": "~56.0.5",
|
"expo-video": "^55.0.10",
|
||||||
"expo-status-bar": "~56.0.4",
|
"react": "19.2.0",
|
||||||
"expo-system-ui": "~56.0.5",
|
"react-dom": "19.2.0",
|
||||||
"expo-task-manager": "~56.0.19",
|
"react-native": "0.83.2",
|
||||||
"expo-updates": "~56.0.19",
|
"react-native-gesture-handler": "^2.30.0",
|
||||||
"expo-video": "~56.1.4",
|
"react-native-pager-view": "^8.0.0",
|
||||||
"jcore-react-native": "^2.3.6",
|
"react-native-paper": "^5.15.0",
|
||||||
"jpush-react-native": "^3.2.7",
|
"react-native-reanimated": "^4.2.1",
|
||||||
"katex": "^0.17.0",
|
"react-native-safe-area-context": "~5.6.2",
|
||||||
"livekit-client": "^2.19.2",
|
"react-native-screens": "~4.23.0",
|
||||||
"markdown-it": "^14.2.0",
|
|
||||||
"pako": "^2.1.0",
|
|
||||||
"prismjs": "^1.30.0",
|
|
||||||
"react": "19.2.3",
|
|
||||||
"react-dom": "19.2.3",
|
|
||||||
"react-native": "0.85.3",
|
|
||||||
"react-native-gesture-handler": "^3.0.0",
|
|
||||||
"react-native-pager-view": "^8.0.2",
|
|
||||||
"react-native-paper": "^5.15.3",
|
|
||||||
"react-native-reanimated": "~4.3.1",
|
|
||||||
"react-native-safe-area-context": "~5.8.0",
|
|
||||||
"react-native-screens": "4.25.2",
|
|
||||||
"react-native-share": "^12.3.1",
|
|
||||||
"react-native-sse": "^1.2.1",
|
"react-native-sse": "^1.2.1",
|
||||||
"react-native-web": "^0.21.0",
|
"react-native-web": "^0.21.0",
|
||||||
"react-native-webview": "13.16.1",
|
"react-native-worklets": "0.7.2",
|
||||||
"react-native-worklets": "^0.8.3",
|
"zod": "^4.3.6",
|
||||||
"zod": "^4.4.3",
|
"zustand": "^5.0.11"
|
||||||
"zustand": "^5.0.14"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@react-native-community/cli": "^20.1.3",
|
"@react-native-community/cli": "^20.1.2",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/react": "~19.2.2",
|
||||||
"@types/pako": "^2.0.4",
|
|
||||||
"@types/react": "~19.2.16",
|
|
||||||
"@types/react-native-vector-icons": "^6.4.18",
|
"@types/react-native-vector-icons": "^6.4.18",
|
||||||
"jest": "^29.7.0",
|
"typescript": "~5.9.2"
|
||||||
"ts-jest": "^29.1.2",
|
|
||||||
"typescript": "~6.0.3"
|
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true
|
||||||
"expo": {
|
|
||||||
"install": {
|
|
||||||
"exclude": [
|
|
||||||
"expo-image",
|
|
||||||
"@react-native-async-storage/async-storage",
|
|
||||||
"@shopify/flash-list",
|
|
||||||
"react-native-pager-view",
|
|
||||||
"react-native-gesture-handler",
|
|
||||||
"react-native-safe-area-context"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
plugins {
|
|
||||||
kotlin("jvm") version "2.1.20" apply false
|
|
||||||
id("java-gradle-plugin")
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
[versions]
|
|
||||||
agp = "8.12.0"
|
|
||||||
gson = "2.8.9"
|
|
||||||
guava = "31.0.1-jre"
|
|
||||||
javapoet = "1.13.0"
|
|
||||||
junit = "4.13.2"
|
|
||||||
kotlin = "2.1.20"
|
|
||||||
assertj = "3.25.1"
|
|
||||||
ktfmt = "0.22.0"
|
|
||||||
|
|
||||||
[libraries]
|
|
||||||
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
|
||||||
android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
|
|
||||||
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
|
|
||||||
guava = { module = "com.google.guava:guava", version.ref = "guava" }
|
|
||||||
javapoet = { module = "com.squareup:javapoet", version.ref = "javapoet" }
|
|
||||||
junit = {module = "junit:junit", version.ref = "junit" }
|
|
||||||
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
|
|
||||||
|
|
||||||
[plugins]
|
|
||||||
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
|
||||||
ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" }
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
|
||||||
distributionPath=wrapper/dists
|
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
|
|
||||||
networkTimeout=10000
|
|
||||||
validateDistributionUrl=true
|
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
|
||||||
zipStorePath=wrapper/dists
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
||||||
*
|
|
||||||
* This source code is licensed under the MIT license found in the
|
|
||||||
* LICENSE file in the root directory of this source tree.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
||||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
|
||||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
alias(libs.plugins.kotlin.jvm)
|
|
||||||
alias(libs.plugins.ktfmt)
|
|
||||||
id("java-gradle-plugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
google()
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
gradlePlugin {
|
|
||||||
plugins {
|
|
||||||
create("react") {
|
|
||||||
id = "com.facebook.react"
|
|
||||||
implementationClass = "com.facebook.react.ReactPlugin"
|
|
||||||
}
|
|
||||||
create("reactrootproject") {
|
|
||||||
id = "com.facebook.react.rootproject"
|
|
||||||
implementationClass = "com.facebook.react.ReactRootProjectPlugin"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "com.facebook.react"
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation(project(":shared"))
|
|
||||||
|
|
||||||
implementation(gradleApi())
|
|
||||||
|
|
||||||
// The KGP/AGP version is defined by React Native Gradle plugin.
|
|
||||||
// Therefore we specify an implementation dep rather than a compileOnly.
|
|
||||||
implementation(libs.kotlin.gradle.plugin)
|
|
||||||
implementation(libs.android.gradle.plugin)
|
|
||||||
|
|
||||||
implementation(libs.gson)
|
|
||||||
implementation(libs.guava)
|
|
||||||
implementation(libs.javapoet)
|
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
|
||||||
testImplementation(libs.assertj)
|
|
||||||
testImplementation(project(":shared-testutil"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// We intentionally don't build for Java 17 as users will see a cryptic bytecode version
|
|
||||||
// error first. Instead we produce a Java 11-compatible Gradle Plugin, so that AGP can print their
|
|
||||||
// nice message showing that JDK 11 (or 17) is required first
|
|
||||||
java { targetCompatibility = JavaVersion.VERSION_11 }
|
|
||||||
|
|
||||||
kotlin { jvmToolchain(17) }
|
|
||||||
|
|
||||||
tasks.withType<KotlinCompile>().configureEach {
|
|
||||||
compilerOptions {
|
|
||||||
apiVersion.set(KotlinVersion.KOTLIN_1_8)
|
|
||||||
// See comment above on JDK 11 support
|
|
||||||
jvmTarget.set(JvmTarget.JVM_11)
|
|
||||||
allWarningsAsErrors.set(
|
|
||||||
project.properties["enableWarningsAsErrors"]?.toString()?.toBoolean() ?: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
|
||||||
testLogging {
|
|
||||||
exceptionFormat = TestExceptionFormat.FULL
|
|
||||||
showExceptions = true
|
|
||||||
showCauses = true
|
|
||||||
showStackTraces = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"agcgw":{"url":"connect-drcn.dbankcloud.cn","backurl":"connect-drcn.hispace.hicloud.com","websocketurl":"connect-ws-drcn.hispace.dbankcloud.cn","websocketbackurl":"connect-ws-drcn.hispace.dbankcloud.com"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"30086000618737115","product_id":"101653523864112478","client_id":"1949443370896080512","client_secret":"CA50EF8C69075FAEFECF2D9F0F3ED101DC3264E0BFE1CC5E5B0272E7FF8215A1","project_id":"101653523864112478","app_id":"117704941","api_key":"DgEDAIkxmMyXBT6vYYW8ZWWXBMJweDbdmmKLYo5MvgH2lV8jPSIZt5WVpjd0/nZ0dk9gAwHf+dWIySILr3QQd7W80H8NNvuCUAQPig==","package_name":"cn.qczlit.withyou"},"oauth_client":{"client_id":"117704941","client_type":1},"app_info":{"app_id":"117704941","package_name":"cn.qczlit.withyou"},"service":{"analytics":{"collector_url":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drcn.ai.dbankcloud.com,ml-api-drcn.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drcn.cloud.huawei.com"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"CN","configuration_version":"3.0","appInfos":[{"package_name":"cn.qczlit.withyou","client":{"app_id":"117704941"},"oauth_client":{"client_id":"117704941","client_type":1},"app_info":{"app_id":"117704941","package_name":"cn.qczlit.withyou"}}]}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
const { withDangerousMod } = require('@expo/config-plugins');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const MAX_JOBS = '3';
|
|
||||||
|
|
||||||
// React Native's hermes-engine build hardcodes
|
|
||||||
// Runtime.getRuntime().availableProcessors()
|
|
||||||
// for `cmake --build -j`, ignoring the CMAKE_BUILD_PARALLEL_LEVEL env var.
|
|
||||||
// On high-core CI runners this exhausts memory.
|
|
||||||
// This plugin caps it during prebuild.
|
|
||||||
const withCmakeJobLimit = (config) =>
|
|
||||||
withDangerousMod(config, [
|
|
||||||
'android',
|
|
||||||
async (config) => {
|
|
||||||
const root = config.modRequest.projectRoot;
|
|
||||||
|
|
||||||
const hermesPath = path.join(
|
|
||||||
root,
|
|
||||||
'node_modules',
|
|
||||||
'react-native',
|
|
||||||
'ReactAndroid',
|
|
||||||
'hermes-engine',
|
|
||||||
'build.gradle.kts',
|
|
||||||
);
|
|
||||||
if (fs.existsSync(hermesPath)) {
|
|
||||||
let content = fs.readFileSync(hermesPath, 'utf-8');
|
|
||||||
content = content.replace(
|
|
||||||
/Runtime\.getRuntime\(\)\.availableProcessors\(\)\.toString\(\)/,
|
|
||||||
`"${MAX_JOBS}"`,
|
|
||||||
);
|
|
||||||
fs.writeFileSync(hermesPath, content);
|
|
||||||
console.log(`[withCmakeJobLimit] capped hermes-engine jobs to ${MAX_JOBS}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
module.exports = withCmakeJobLimit;
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
const { withAndroidManifest, withDangerousMod } = require('@expo/config-plugins');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const FOREGROUND_SERVICE_PERMISSION = 'android.permission.FOREGROUND_SERVICE';
|
|
||||||
const FOREGROUND_SERVICE_DATA_SYNC = 'android.permission.FOREGROUND_SERVICE_DATA_SYNC';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Expo Config Plugin:为 Android 添加前台服务保活
|
|
||||||
*
|
|
||||||
* 功能:
|
|
||||||
* 1. 添加 FOREGROUND_SERVICE 相关权限
|
|
||||||
* 2. 在 AndroidManifest 中声明 Service
|
|
||||||
* 3. 生成 Kotlin 原生服务代码
|
|
||||||
* 4. 生成 React Native Bridge 模块
|
|
||||||
*
|
|
||||||
* 参考:Element Android 的 GuardAndroidService
|
|
||||||
*/
|
|
||||||
const withForegroundService = (config, options = {}) => {
|
|
||||||
const {
|
|
||||||
notificationTitle = '威友',
|
|
||||||
notificationBody = '正在后台同步消息',
|
|
||||||
notificationIcon = 'ic_notification',
|
|
||||||
channelId = 'withyou_sync_foreground',
|
|
||||||
channelName = '消息同步',
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
// 1. 添加权限和 Service 声明到 AndroidManifest
|
|
||||||
config = withAndroidManifest(config, (config) => {
|
|
||||||
const manifest = config.modResults;
|
|
||||||
|
|
||||||
// 添加权限声明
|
|
||||||
const permissions = [
|
|
||||||
FOREGROUND_SERVICE_PERMISSION,
|
|
||||||
FOREGROUND_SERVICE_DATA_SYNC,
|
|
||||||
];
|
|
||||||
|
|
||||||
permissions.forEach((permission) => {
|
|
||||||
const existingPermission = manifest.manifest['uses-permission']?.find(
|
|
||||||
(p) => p.$['android:name'] === permission
|
|
||||||
);
|
|
||||||
if (!existingPermission) {
|
|
||||||
if (!manifest.manifest['uses-permission']) {
|
|
||||||
manifest.manifest['uses-permission'] = [];
|
|
||||||
}
|
|
||||||
manifest.manifest['uses-permission'].push({
|
|
||||||
$: { 'android:name': permission },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加 Service 声明
|
|
||||||
if (!manifest.manifest.application[0].service) {
|
|
||||||
manifest.manifest.application[0].service = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingService = manifest.manifest.application[0].service.find(
|
|
||||||
(s) => s.$['android:name'] === '.SyncForegroundService'
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!existingService) {
|
|
||||||
manifest.manifest.application[0].service.push({
|
|
||||||
$: {
|
|
||||||
'android:name': '.SyncForegroundService',
|
|
||||||
'android:enabled': 'true',
|
|
||||||
'android:exported': 'false',
|
|
||||||
'android:foregroundServiceType': 'dataSync',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 生成 Kotlin 原生代码
|
|
||||||
config = withDangerousMod(config, [
|
|
||||||
'android',
|
|
||||||
async (config) => {
|
|
||||||
const projectRoot = config.modRequest.projectRoot;
|
|
||||||
const packageName = config.android.package;
|
|
||||||
const kotlinPath = path.join(
|
|
||||||
projectRoot,
|
|
||||||
'android/app/src/main/java',
|
|
||||||
packageName.replace(/\./g, '/')
|
|
||||||
);
|
|
||||||
|
|
||||||
// 确保目录存在
|
|
||||||
if (!fs.existsSync(kotlinPath)) {
|
|
||||||
fs.mkdirSync(kotlinPath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成 SyncForegroundService.kt
|
|
||||||
const serviceCode = generateServiceCode(packageName, {
|
|
||||||
notificationTitle,
|
|
||||||
notificationBody,
|
|
||||||
notificationIcon,
|
|
||||||
channelId,
|
|
||||||
channelName,
|
|
||||||
});
|
|
||||||
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(kotlinPath, 'SyncForegroundService.kt'),
|
|
||||||
serviceCode
|
|
||||||
);
|
|
||||||
|
|
||||||
// 生成 ForegroundServiceModule.kt (React Native Bridge)
|
|
||||||
const moduleCode = generateModuleCode(packageName);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(kotlinPath, 'ForegroundServiceModule.kt'),
|
|
||||||
moduleCode
|
|
||||||
);
|
|
||||||
|
|
||||||
// 生成 ForegroundServicePackage.kt
|
|
||||||
const packageCode = generatePackageCode(packageName);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(kotlinPath, 'ForegroundServicePackage.kt'),
|
|
||||||
packageCode
|
|
||||||
);
|
|
||||||
|
|
||||||
// 修改 MainApplication.kt 注册模块
|
|
||||||
const mainAppPath = path.join(kotlinPath, 'MainApplication.kt');
|
|
||||||
if (fs.existsSync(mainAppPath)) {
|
|
||||||
let content = fs.readFileSync(mainAppPath, 'utf-8');
|
|
||||||
|
|
||||||
// 添加 import
|
|
||||||
if (!content.includes('ForegroundServicePackage')) {
|
|
||||||
content = content.replace(
|
|
||||||
/package .*\n/,
|
|
||||||
`$&\nimport ${packageName}.ForegroundServicePackage\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 在 packages 列表中添加
|
|
||||||
if (!content.includes('ForegroundServicePackage()')) {
|
|
||||||
// 查找 PackageList(this).packages.apply 块
|
|
||||||
content = content.replace(
|
|
||||||
/(PackageList\(this\)\.packages\.apply\s*\{)/,
|
|
||||||
`$1\n // 前台服务模块\n add(ForegroundServicePackage())`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.writeFileSync(mainAppPath, content);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建通知图标目录和文件
|
|
||||||
const drawablePath = path.join(projectRoot, 'android/app/src/main/res/drawable');
|
|
||||||
if (!fs.existsSync(drawablePath)) {
|
|
||||||
fs.mkdirSync(drawablePath, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const iconPath = path.join(drawablePath, 'ic_notification.xml');
|
|
||||||
if (!fs.existsSync(iconPath)) {
|
|
||||||
fs.writeFileSync(iconPath, generateNotificationIcon());
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成前台服务 Kotlin 代码
|
|
||||||
*/
|
|
||||||
function generateServiceCode(packageName, options) {
|
|
||||||
const { notificationTitle, notificationBody, notificationIcon, channelId, channelName } = options;
|
|
||||||
|
|
||||||
return `package ${packageName}
|
|
||||||
|
|
||||||
import android.app.Notification
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.app.Service
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.pm.ServiceInfo
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 前台同步服务
|
|
||||||
*
|
|
||||||
* 在通知栏显示常驻通知,防止应用进程被系统杀死
|
|
||||||
* 类似 Element Android 的 GuardAndroidService 和 V2Ray 的保活机制
|
|
||||||
*/
|
|
||||||
class SyncForegroundService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val NOTIFICATION_ID = 1001
|
|
||||||
const val CHANNEL_ID = "${channelId}"
|
|
||||||
const val CHANNEL_NAME = "${channelName}"
|
|
||||||
|
|
||||||
const val ACTION_START = "${packageName}.foreground.START"
|
|
||||||
const val ACTION_STOP = "${packageName}.foreground.STOP"
|
|
||||||
const val ACTION_UPDATE = "${packageName}.foreground.UPDATE"
|
|
||||||
|
|
||||||
const val EXTRA_TITLE = "title"
|
|
||||||
const val EXTRA_BODY = "body"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动前台服务
|
|
||||||
*/
|
|
||||||
fun start(context: Context, title: String = "${notificationTitle}", body: String = "${notificationBody}") {
|
|
||||||
val intent = Intent(context, SyncForegroundService::class.java).apply {
|
|
||||||
action = ACTION_START
|
|
||||||
putExtra(EXTRA_TITLE, title)
|
|
||||||
putExtra(EXTRA_BODY, body)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
android.util.Log.e("SyncForegroundService", "启动前台服务失败", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止前台服务
|
|
||||||
*/
|
|
||||||
fun stop(context: Context) {
|
|
||||||
val intent = Intent(context, SyncForegroundService::class.java).apply {
|
|
||||||
action = ACTION_STOP
|
|
||||||
}
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新通知内容
|
|
||||||
*/
|
|
||||||
fun update(context: Context, title: String, body: String) {
|
|
||||||
val intent = Intent(context, SyncForegroundService::class.java).apply {
|
|
||||||
action = ACTION_UPDATE
|
|
||||||
putExtra(EXTRA_TITLE, title)
|
|
||||||
putExtra(EXTRA_BODY, body)
|
|
||||||
}
|
|
||||||
context.startService(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var notificationTitle: String = "${notificationTitle}"
|
|
||||||
private var notificationBody: String = "${notificationBody}"
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
createNotificationChannel()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
when (intent?.action) {
|
|
||||||
ACTION_START -> {
|
|
||||||
notificationTitle = intent.getStringExtra(EXTRA_TITLE) ?: "${notificationTitle}"
|
|
||||||
notificationBody = intent.getStringExtra(EXTRA_BODY) ?: "${notificationBody}"
|
|
||||||
startForegroundCompat()
|
|
||||||
}
|
|
||||||
ACTION_STOP -> {
|
|
||||||
stopForegroundCompat()
|
|
||||||
stopSelf()
|
|
||||||
}
|
|
||||||
ACTION_UPDATE -> {
|
|
||||||
notificationTitle = intent.getStringExtra(EXTRA_TITLE) ?: notificationTitle
|
|
||||||
notificationBody = intent.getStringExtra(EXTRA_BODY) ?: notificationBody
|
|
||||||
updateNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// START_STICKY: 服务被杀后自动重启
|
|
||||||
return START_STICKY
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动前台服务(兼容 Android Q+)
|
|
||||||
*/
|
|
||||||
private fun startForegroundCompat() {
|
|
||||||
val notification = buildNotification()
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
||||||
startForeground(
|
|
||||||
NOTIFICATION_ID,
|
|
||||||
notification,
|
|
||||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
startForeground(NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止前台服务(兼容 Android N+)
|
|
||||||
*/
|
|
||||||
private fun stopForegroundCompat() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
|
||||||
} else {
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
stopForeground(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建通知渠道
|
|
||||||
*/
|
|
||||||
private fun createNotificationChannel() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
val channel = NotificationChannel(
|
|
||||||
CHANNEL_ID,
|
|
||||||
CHANNEL_NAME,
|
|
||||||
NotificationManager.IMPORTANCE_MIN // 最低重要性,不打扰用户
|
|
||||||
).apply {
|
|
||||||
description = "后台消息同步服务"
|
|
||||||
setSound(null, null)
|
|
||||||
setShowBadge(false)
|
|
||||||
enableLights(false)
|
|
||||||
enableVibration(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
|
||||||
manager.createNotificationChannel(channel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 构建通知
|
|
||||||
*/
|
|
||||||
private fun buildNotification(): Notification {
|
|
||||||
// 点击通知打开应用
|
|
||||||
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
|
||||||
this,
|
|
||||||
0,
|
|
||||||
launchIntent,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
return NotificationCompat.Builder(this, CHANNEL_ID)
|
|
||||||
.setContentTitle(notificationTitle)
|
|
||||||
.setContentText(notificationBody)
|
|
||||||
.setSmallIcon(R.drawable.${notificationIcon})
|
|
||||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
||||||
.setOngoing(true) // 不可滑动清除
|
|
||||||
.setShowWhen(false)
|
|
||||||
.setContentIntent(pendingIntent)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新通知内容
|
|
||||||
*/
|
|
||||||
private fun updateNotification() {
|
|
||||||
val manager = getSystemService(NotificationManager::class.java)
|
|
||||||
manager.notify(NOTIFICATION_ID, buildNotification())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 React Native Bridge 模块代码
|
|
||||||
*/
|
|
||||||
function generateModuleCode(packageName) {
|
|
||||||
return `package ${packageName}
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
|
||||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
||||||
import com.facebook.react.bridge.ReactMethod
|
|
||||||
import com.facebook.react.bridge.Promise
|
|
||||||
|
|
||||||
/**
|
|
||||||
* React Native Bridge: 前台服务控制模块
|
|
||||||
*/
|
|
||||||
class ForegroundServiceModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
|
|
||||||
|
|
||||||
override fun getName(): String = "ForegroundService"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动前台服务
|
|
||||||
*/
|
|
||||||
@ReactMethod
|
|
||||||
fun start(title: String, body: String, promise: Promise) {
|
|
||||||
try {
|
|
||||||
SyncForegroundService.start(reactApplicationContext, title, body)
|
|
||||||
promise.resolve(true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
promise.reject("FOREGROUND_SERVICE_ERROR", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止前台服务
|
|
||||||
*/
|
|
||||||
@ReactMethod
|
|
||||||
fun stop(promise: Promise) {
|
|
||||||
try {
|
|
||||||
SyncForegroundService.stop(reactApplicationContext)
|
|
||||||
promise.resolve(true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
promise.reject("FOREGROUND_SERVICE_ERROR", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新通知内容
|
|
||||||
*/
|
|
||||||
@ReactMethod
|
|
||||||
fun update(title: String, body: String, promise: Promise) {
|
|
||||||
try {
|
|
||||||
SyncForegroundService.update(reactApplicationContext, title, body)
|
|
||||||
promise.resolve(true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
promise.reject("FOREGROUND_SERVICE_ERROR", e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成 React Native Package 代码
|
|
||||||
*/
|
|
||||||
function generatePackageCode(packageName) {
|
|
||||||
return `package ${packageName}
|
|
||||||
|
|
||||||
import com.facebook.react.ReactPackage
|
|
||||||
import com.facebook.react.bridge.NativeModule
|
|
||||||
import com.facebook.react.bridge.ReactApplicationContext
|
|
||||||
import com.facebook.react.uimanager.ViewManager
|
|
||||||
|
|
||||||
class ForegroundServicePackage : ReactPackage {
|
|
||||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
|
|
||||||
return listOf(ForegroundServiceModule(reactContext))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成通知图标 XML
|
|
||||||
*/
|
|
||||||
function generateNotificationIcon() {
|
|
||||||
return `<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<!-- 背景圆 -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#4CAF50"
|
|
||||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z"/>
|
|
||||||
<!-- 对勾符号 -->
|
|
||||||
<path
|
|
||||||
android:fillColor="#FFFFFF"
|
|
||||||
android:pathData="M10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
|
|
||||||
</vector>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = withForegroundService;
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
const { withDangerousMod } = require('@expo/config-plugins');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// 荣耀厂商推送通道的 Expo config plugin.
|
|
||||||
//
|
|
||||||
// 客户端职责(依据极光官方文档 https://docs.jiguang.cn/jpush/client/Android/android_3rd_guide):
|
|
||||||
// 1) 添加 cn.jiguang.sdk.plugin:honor 依赖
|
|
||||||
// 2) 注入 manifestPlaceholders: HONOR_APPID
|
|
||||||
// 3) 添加荣耀 Maven 仓库 https://developer.hihonor.com/repo 到 settings.gradle 和根 build.gradle
|
|
||||||
// 关键: 必须同时加到 settings.gradle 的 dependencyResolutionManagement 块(CI build 走这条路径),
|
|
||||||
// 和根 build.gradle 的 buildscript+allprojects(本地 build 兜底)。
|
|
||||||
// 4) 添加 Proguard 规则 (com.hihonor.push.** 保留 + 5 条 -keepattributes)
|
|
||||||
//
|
|
||||||
// 实现说明:所有文件改动统一用 withDangerousMod 直接读写磁盘。
|
|
||||||
// 原因:Expo SDK 56+ 的 withSettingsGradle / withProjectBuildGradle / withAppBuildGradle hook 链中,
|
|
||||||
// 第二个以后的 plugin 写回 modResults.contents 会被静默忽略。withDangerousMod 走文件系统串行执行,
|
|
||||||
// 多 vendor plugin 共存可靠。
|
|
||||||
const withHonorPush = (config, options = {}) => {
|
|
||||||
const {
|
|
||||||
jpushVersion = '6.1.0',
|
|
||||||
appId = '104562917',
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const honorRepoUrl = 'https://developer.hihonor.com/repo';
|
|
||||||
|
|
||||||
config = withDangerousMod(config, [
|
|
||||||
'android',
|
|
||||||
async (config) => {
|
|
||||||
const settingsGradlePath = path.join(
|
|
||||||
config.modRequest.platformProjectRoot,
|
|
||||||
'settings.gradle'
|
|
||||||
);
|
|
||||||
const rootBuildGradlePath = path.join(
|
|
||||||
config.modRequest.platformProjectRoot,
|
|
||||||
'build.gradle'
|
|
||||||
);
|
|
||||||
const appBuildGradlePath = path.join(
|
|
||||||
config.modRequest.platformProjectRoot,
|
|
||||||
'app',
|
|
||||||
'build.gradle'
|
|
||||||
);
|
|
||||||
const proguardFilePath = path.join(
|
|
||||||
config.modRequest.platformProjectRoot,
|
|
||||||
'app',
|
|
||||||
'proguard-rules.pro'
|
|
||||||
);
|
|
||||||
|
|
||||||
// --- 1. settings.gradle: pluginManagement + dependencyResolutionManagement ---
|
|
||||||
if (fs.existsSync(settingsGradlePath)) {
|
|
||||||
let settings = fs.readFileSync(settingsGradlePath, 'utf-8');
|
|
||||||
let settingsChanged = false;
|
|
||||||
const repoLine = ` maven { url '${honorRepoUrl}' }`;
|
|
||||||
|
|
||||||
// pluginManagement.repositories
|
|
||||||
if (!settings.match(/pluginManagement\s*\{[\s\S]*?developer\.hihonor\.com\/repo/)) {
|
|
||||||
const hasRepositoriesInPM = /pluginManagement\s*\{[\s\S]*?repositories\s*\{/.test(settings);
|
|
||||||
if (hasRepositoriesInPM) {
|
|
||||||
settings = settings.replace(
|
|
||||||
/(pluginManagement\s*\{[\s\S]*?repositories\s*\{)/,
|
|
||||||
`$1\n ${repoLine}`
|
|
||||||
);
|
|
||||||
settingsChanged = true;
|
|
||||||
} else if (settings.includes('pluginManagement')) {
|
|
||||||
settings = settings.replace(
|
|
||||||
/(pluginManagement\s*\{)/,
|
|
||||||
`$1\n repositories {\n gradlePluginPortal()\n google()\n mavenCentral()\n ${repoLine}\n }`
|
|
||||||
);
|
|
||||||
settingsChanged = true;
|
|
||||||
} else {
|
|
||||||
settings =
|
|
||||||
`pluginManagement {\n repositories {\n ${repoLine}\n }\n}\n\n` + settings;
|
|
||||||
settingsChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// dependencyResolutionManagement (Gradle 7+, takes precedence over allprojects in CI)
|
|
||||||
if (!settings.match(/dependencyResolutionManagement[\s\S]*developer\.hihonor\.com\/repo/)) {
|
|
||||||
const depMgmtPattern = /dependencyResolutionManagement\s*\{[\s\S]*?repositories\s*\{/;
|
|
||||||
if (depMgmtPattern.test(settings)) {
|
|
||||||
settings = settings.replace(
|
|
||||||
depMgmtPattern,
|
|
||||||
`$&\n maven { url '${honorRepoUrl}' }`
|
|
||||||
);
|
|
||||||
settingsChanged = true;
|
|
||||||
} else {
|
|
||||||
// Block doesn't exist — fallback: create with Huawei + Honor + standard repos.
|
|
||||||
const block = `\ndependencyResolutionManagement {\n repositories {\n maven { url 'https://developer.huawei.com/repo/' }\n maven { url '${honorRepoUrl}' }\n google()\n mavenCentral()\n maven { url 'https://www.jitpack.io' }\n }\n}\n`;
|
|
||||||
const pluginsBlockEnd = /(\nplugins\s*\{[\s\S]*?\n\})/;
|
|
||||||
if (pluginsBlockEnd.test(settings)) {
|
|
||||||
settings = settings.replace(pluginsBlockEnd, `$1\n${block}`);
|
|
||||||
} else {
|
|
||||||
settings = settings + block;
|
|
||||||
}
|
|
||||||
settingsChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (settingsChanged) {
|
|
||||||
fs.writeFileSync(settingsGradlePath, settings);
|
|
||||||
console.log('[withHonorPush] updated settings.gradle (pluginManagement + dependencyResolutionManagement)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 2. 根 build.gradle: buildscript + allprojects repositories ---
|
|
||||||
if (fs.existsSync(rootBuildGradlePath)) {
|
|
||||||
let gradle = fs.readFileSync(rootBuildGradlePath, 'utf-8');
|
|
||||||
let changed = false;
|
|
||||||
const repoLine = ` maven { url '${honorRepoUrl}' }`;
|
|
||||||
|
|
||||||
if (!gradle.match(/buildscript\s*\{[\s\S]*?developer\.hihonor\.com\/repo/)) {
|
|
||||||
const bsRepoPattern = /(buildscript\s*\{[\s\S]*?repositories\s*\{)/;
|
|
||||||
if (bsRepoPattern.test(gradle)) {
|
|
||||||
gradle = gradle.replace(bsRepoPattern, `$1\n${repoLine}`);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gradle.match(/allprojects\s*\{[\s\S]*?developer\.hihonor\.com\/repo/)) {
|
|
||||||
const allProjectsPattern = /(allprojects\s*\{[\s\S]*?repositories\s*\{)/;
|
|
||||||
if (allProjectsPattern.test(gradle)) {
|
|
||||||
gradle = gradle.replace(allProjectsPattern, `$1\n${repoLine}`);
|
|
||||||
changed = true;
|
|
||||||
} else {
|
|
||||||
const allProjectsBlock = `\nallprojects {\n repositories {${repoLine}\n }\n}\n`;
|
|
||||||
gradle = gradle + allProjectsBlock;
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
fs.writeFileSync(rootBuildGradlePath, gradle);
|
|
||||||
console.log('[withHonorPush] updated root build.gradle (buildscript + allprojects repos)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 3. app/build.gradle: AAR 依赖 + manifestPlaceholders ---
|
|
||||||
if (fs.existsSync(appBuildGradlePath)) {
|
|
||||||
let gradle = fs.readFileSync(appBuildGradlePath, 'utf-8');
|
|
||||||
let changed = false;
|
|
||||||
|
|
||||||
if (!gradle.includes('cn.jiguang.sdk.plugin:honor')) {
|
|
||||||
const depsPattern = /dependencies\s*\{/;
|
|
||||||
if (depsPattern.test(gradle)) {
|
|
||||||
gradle = gradle.replace(
|
|
||||||
depsPattern,
|
|
||||||
`dependencies {\n implementation 'cn.jiguang.sdk.plugin:honor:${jpushVersion}'`
|
|
||||||
);
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifestPlaceholderRegex = /manifestPlaceholders\s*=\s*\[([\s\S]*?)\]/;
|
|
||||||
const match = gradle.match(manifestPlaceholderRegex);
|
|
||||||
|
|
||||||
const requiredPlaceholders = {
|
|
||||||
HONOR_APPID: appId,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const existingBlock = match[1];
|
|
||||||
const existingEntries = {};
|
|
||||||
const entryRegex = /(\w+)\s*:\s*"([^"]*)"/g;
|
|
||||||
let entryMatch;
|
|
||||||
while ((entryMatch = entryRegex.exec(existingBlock)) !== null) {
|
|
||||||
existingEntries[entryMatch[1]] = entryMatch[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
let placeholdersChanged = false;
|
|
||||||
for (const [key, value] of Object.entries(requiredPlaceholders)) {
|
|
||||||
if (existingEntries[key] !== value) {
|
|
||||||
existingEntries[key] = value;
|
|
||||||
placeholdersChanged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (placeholdersChanged) {
|
|
||||||
const entriesStr = Object.entries(existingEntries)
|
|
||||||
.map(([k, v]) => `${k}: "${v}"`)
|
|
||||||
.join(',\n ');
|
|
||||||
const newBlock = `\n manifestPlaceholders = [\n ${entriesStr}\n ]`;
|
|
||||||
gradle = gradle.replace(manifestPlaceholderRegex, newBlock.trim());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
} else if (gradle.includes('REACT_NATIVE_RELEASE_LEVEL')) {
|
|
||||||
const entriesStr = Object.entries(requiredPlaceholders)
|
|
||||||
.map(([k, v]) => `${k}: "${v}"`)
|
|
||||||
.join(',\n ');
|
|
||||||
const block = `\n manifestPlaceholders = [\n ${entriesStr}\n ]\n`;
|
|
||||||
const lines = gradle.split('\n');
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
if (lines[i].includes('REACT_NATIVE_RELEASE_LEVEL')) {
|
|
||||||
lines.splice(i + 1, 0, block);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
gradle = lines.join('\n');
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
fs.writeFileSync(appBuildGradlePath, gradle);
|
|
||||||
console.log('[withHonorPush] updated app/build.gradle (AAR + manifestPlaceholders)');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- 4. proguard-rules.pro ---
|
|
||||||
if (fs.existsSync(proguardFilePath)) {
|
|
||||||
let proguard = fs.readFileSync(proguardFilePath, 'utf-8');
|
|
||||||
if (!proguard.includes('-keep class com.hihonor.push.**')) {
|
|
||||||
const honorRules = `
|
|
||||||
# JPush Honor vendor channel — keep rules (https://docs.jiguang.cn/jpush/client/Android/android_3rd_guide)
|
|
||||||
-ignorewarnings
|
|
||||||
-keepattributes *Annotation*
|
|
||||||
-keepattributes Exceptions
|
|
||||||
-keepattributes InnerClasses
|
|
||||||
-keepattributes Signature
|
|
||||||
-keepattributes SourceFile,LineNumberTable
|
|
||||||
-keep class com.hihonor.push.**{*;}
|
|
||||||
`;
|
|
||||||
fs.writeFileSync(proguardFilePath, proguard + honorRules);
|
|
||||||
console.log('[withHonorPush] appended Honor Proguard rules to proguard-rules.pro');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return config;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = withHonorPush;
|
|
||||||