From f5f9c3a619bfdd4e4130b46da005f6df3ecbce25 Mon Sep 17 00:00:00 2001 From: lan Date: Tue, 5 May 2026 17:46:31 +0800 Subject: [PATCH] ci: add iOS OTA publishing and refactor gesture handling - Add `ota-ios` job to GitHub Actions workflow to support iOS OTA updates - Refactor `ScheduleScreen` to use `GestureDetector` and `Gesture` from `react-native-gesture-handler` instead of `PanGestureHandler` - Update `TradeCard` styles and layout spacing in `HomeScreen` and `MarketView` - Refactor `MarketView` to use updated responsive spacing values --- .gitea/workflows/build.yml | 80 ++++++++++++++++++- .../business/TradeCard/TradeCard.tsx | 11 ++- src/screens/home/HomeScreen.tsx | 2 +- src/screens/home/MarketView.tsx | 8 +- src/screens/schedule/ScheduleScreen.tsx | 58 ++++++-------- 5 files changed, 112 insertions(+), 47 deletions(-) diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 4b3dac2..8f0816c 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -21,7 +21,8 @@ on: env: REGISTRY: code.littlelan.cn IMAGE_NAME: carrot_bbs/frontend-web - OTA_PLATFORM: android + OTA_PLATFORM_ANDROID: android + OTA_PLATFORM_IOS: ios OTA_PUBLISH_URL: https://updates.littlelan.cn/admin/publish OTA_MANIFEST_URL: https://updates.littlelan.cn/api/manifest @@ -82,7 +83,7 @@ jobs: OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }} run: | 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=${OTA_PLATFORM}" \ + curl -fSs -X POST "${OTA_PUBLISH_URL}?runtimeVersion=${{ steps.runtime.outputs.runtime_version }}&platform=${OTA_PLATFORM_ANDROID}" \ -H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \ -H "Content-Type: application/zip" \ --data-binary @"dist-android-update.zip" @@ -90,7 +91,7 @@ jobs: - name: Verify OTA manifest run: | REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \ - -H "expo-platform: ${OTA_PLATFORM}" \ + -H "expo-platform: ${OTA_PLATFORM_ANDROID}" \ -H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \ -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])")" @@ -99,6 +100,79 @@ jobs: echo "Local bundle: ${LOCAL}" test "${REMOTE}" = "${LOCAL}" + ota-ios: + runs-on: ubuntu-latest + if: github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.publish_ota == 'true') + permissions: + contents: read + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: '25.6.1' + registry-url: 'https://registry.npmmirror.com' + + - name: Remove deprecated always-auth npm config + run: | + if [ -f ~/.npmrc ]; then + sed -i '/always-auth/d' ~/.npmrc + fi + + - name: Install dependencies + run: npm ci + + - name: Resolve runtime version + id: runtime + run: | + RUNTIME_VERSION="$(node -p "require('./app.json').expo.version")" + echo "runtime_version=${RUNTIME_VERSION}" >> "$GITHUB_OUTPUT" + echo "Resolved runtimeVersion: ${RUNTIME_VERSION}" + + - name: Export iOS update bundle + run: | + rm -rf dist-ios dist-ios-update.zip + npx expo export --platform ios --output-dir dist-ios + npx expo config --type public --json > dist-ios/expoConfig.json + + - name: Archive iOS update bundle + run: | + python - <<'PY' + import os + import zipfile + + with zipfile.ZipFile('dist-ios-update.zip', 'w', zipfile.ZIP_DEFLATED) as zf: + for root, _, files in os.walk('dist-ios'): + for name in files: + src = os.path.join(root, name) + arc = os.path.relpath(src, 'dist-ios') + zf.write(src, arc) + PY + + - name: Publish OTA + env: + OTA_AUTH_TOKEN: ${{ secrets.OTA_AUTH_TOKEN }} + run: | + 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=${OTA_PLATFORM_IOS}" \ + -H "Authorization: Bearer ${OTA_AUTH_TOKEN}" \ + -H "Content-Type: application/zip" \ + --data-binary @"dist-ios-update.zip" + + - name: Verify OTA manifest + run: | + REMOTE="$(curl -sS "${OTA_MANIFEST_URL}" \ + -H "expo-platform: ${OTA_PLATFORM_IOS}" \ + -H "expo-runtime-version: ${{ steps.runtime.outputs.runtime_version }}" \ + -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])")" + LOCAL="$(python -c "import json; m=json.load(open('dist-ios/metadata.json')); print(m['fileMetadata']['ios']['bundle'].split('/')[-1])")" + echo "Remote bundle: ${REMOTE}" + echo "Local bundle: ${LOCAL}" + test "${REMOTE}" = "${LOCAL}" + build-android-apk: runs-on: android-builder container: diff --git a/src/components/business/TradeCard/TradeCard.tsx b/src/components/business/TradeCard/TradeCard.tsx index 243033d..8d65332 100644 --- a/src/components/business/TradeCard/TradeCard.tsx +++ b/src/components/business/TradeCard/TradeCard.tsx @@ -22,14 +22,13 @@ function createTradeCardStyles(colors: AppColors) { return StyleSheet.create({ gridCard: { backgroundColor: colors.background.paper, - borderRadius: borderRadius.xl, + borderRadius: borderRadius.lg, overflow: 'hidden', - // subtle shadow for depth shadowColor: '#000', - shadowOffset: { width: 0, height: 1 }, - shadowOpacity: 0.06, - shadowRadius: 3, - elevation: 2, + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 6, + elevation: 3, }, listCard: { backgroundColor: colors.background.paper, diff --git a/src/screens/home/HomeScreen.tsx b/src/screens/home/HomeScreen.tsx index 3f3c79f..cc1a3c2 100644 --- a/src/screens/home/HomeScreen.tsx +++ b/src/screens/home/HomeScreen.tsx @@ -796,7 +796,7 @@ export const HomeScreen: React.FC = () => { const authorId = item.author?.id || ''; const isPostAuthor = currentUser?.id === authorId; return ( - + createMarketStyles(colors, gap, padding), [colors, gap, padding]); const [items, setItems] = useState([]); @@ -163,7 +163,7 @@ export function MarketView({ }, [items, isAuth]); const renderItem = useCallback>(({ item }) => ( - + - ), [handlePressItem, handleFavorite, gap]); + ), [handlePressItem, handleFavorite]); const keyExtractor = useCallback((item: TradeItemDTO) => item.id, []); diff --git a/src/screens/schedule/ScheduleScreen.tsx b/src/screens/schedule/ScheduleScreen.tsx index d71eb9e..bf8ae53 100644 --- a/src/screens/schedule/ScheduleScreen.tsx +++ b/src/screens/schedule/ScheduleScreen.tsx @@ -18,8 +18,8 @@ import { ActivityIndicator, Platform, } from 'react-native'; -import { PanGestureHandler, State } from 'react-native-gesture-handler'; -import type { PanGestureHandlerStateChangeEvent } from 'react-native-gesture-handler'; +import { GestureDetector, Gesture } from 'react-native-gesture-handler'; +import { runOnJS } from 'react-native-reanimated'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { MaterialCommunityIcons } from '@expo/vector-icons'; import { blurActiveElement } from '../../infrastructure/platform'; @@ -305,20 +305,23 @@ export const ScheduleScreen: React.FC = () => { [courses] ); - const handleWeekSwipe = useCallback( - ({ nativeEvent }: PanGestureHandlerStateChangeEvent) => { - if (nativeEvent.oldState !== State.ACTIVE && nativeEvent.oldState !== State.BEGAN) return; - const { translationX, translationY } = nativeEvent; - if (Math.abs(translationX) < SWIPE_THRESHOLD) return; - if (Math.abs(translationX) <= Math.abs(translationY) * 0.8) return; + const goNextWeek = useCallback(() => setCurrentWeek(prev => Math.min(prev + 1, weeks.length)), [weeks.length]); + const goPrevWeek = useCallback(() => setCurrentWeek(prev => Math.max(prev - 1, 1)), [weeks.length]); - if (translationX < 0) { - setCurrentWeek(prev => Math.min(prev + 1, weeks.length)); - } else { - setCurrentWeek(prev => Math.max(prev - 1, 1)); - } - }, - [weeks.length] + const weekSwipeGesture = useMemo(() => + Gesture.Pan() + .activeOffsetX([-8, 8]) + .failOffsetY([-80, 80]) + .onEnd((e) => { + if (Math.abs(e.translationX) < SWIPE_THRESHOLD) return; + if (Math.abs(e.translationX) <= Math.abs(e.translationY) * 0.8) return; + if (e.translationX < 0) { + runOnJS(goNextWeek)(); + } else { + runOnJS(goPrevWeek)(); + } + }), + [goNextWeek, goPrevWeek] ); const webTouchStartRef = useRef<{ x: number; y: number; time: number } | null>(null); @@ -797,11 +800,7 @@ export const ScheduleScreen: React.FC = () => { ) : ( - + { contentContainerStyle={styles.scheduleBodyContent} > {renderTimeColumn()} - - - {VISIBLE_DAY_VALUES.map((dayOfWeek, index) => - renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1) - )} - - + + {VISIBLE_DAY_VALUES.map((dayOfWeek, index) => + renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1) + )} + - + ) ); };