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
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -796,7 +796,7 @@ export const HomeScreen: React.FC = () => {
|
||||
const authorId = item.author?.id || '';
|
||||
const isPostAuthor = currentUser?.id === authorId;
|
||||
return (
|
||||
<View style={styles.gridItem}>
|
||||
<View style={{ marginBottom: 2, paddingHorizontal: 1 }}>
|
||||
<PostCard
|
||||
post={item}
|
||||
variant="grid"
|
||||
|
||||
@@ -53,8 +53,8 @@ export function MarketView({
|
||||
const isAuth = useIsAuthenticated();
|
||||
const { width } = useResponsive();
|
||||
|
||||
const gap = useResponsiveSpacing({ xs: 6, sm: 8, md: 10, lg: 12, xl: 16 });
|
||||
const padding = useResponsiveSpacing({ xs: 8, sm: 10, md: 12, lg: 16, xl: 20 });
|
||||
const gap = useResponsiveSpacing({ xs: 4, sm: 6, md: 8, lg: 12, xl: 16 });
|
||||
const padding = useResponsiveSpacing({ xs: 8, sm: 12, md: 16, lg: 24, xl: 32 });
|
||||
const styles = useMemo(() => createMarketStyles(colors, gap, padding), [colors, gap, padding]);
|
||||
|
||||
const [items, setItems] = useState<TradeItemDTO[]>([]);
|
||||
@@ -163,7 +163,7 @@ export function MarketView({
|
||||
}, [items, isAuth]);
|
||||
|
||||
const renderItem = useCallback<ListRenderItem<TradeItemDTO>>(({ item }) => (
|
||||
<View style={{ marginBottom: gap }}>
|
||||
<View style={{ marginBottom: 2, paddingHorizontal: 1 }}>
|
||||
<TradeCard
|
||||
item={item}
|
||||
variant="grid"
|
||||
@@ -171,7 +171,7 @@ export function MarketView({
|
||||
onFavorite={handleFavorite}
|
||||
/>
|
||||
</View>
|
||||
), [handlePressItem, handleFavorite, gap]);
|
||||
), [handlePressItem, handleFavorite]);
|
||||
|
||||
const keyExtractor = useCallback((item: TradeItemDTO) => item.id, []);
|
||||
|
||||
|
||||
@@ -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 = () => {
|
||||
</ScrollView>
|
||||
</View>
|
||||
) : (
|
||||
<PanGestureHandler
|
||||
activeOffsetX={[-8, 8]}
|
||||
failOffsetY={[-80, 80]}
|
||||
onHandlerStateChange={handleWeekSwipe}
|
||||
>
|
||||
<GestureDetector gesture={weekSwipeGesture}>
|
||||
<View style={styles.scheduleBodyGestureLayer}>
|
||||
<ScrollView
|
||||
style={styles.scheduleBody}
|
||||
@@ -809,21 +808,14 @@ export const ScheduleScreen: React.FC = () => {
|
||||
contentContainerStyle={styles.scheduleBodyContent}
|
||||
>
|
||||
{renderTimeColumn()}
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
nestedScrollEnabled
|
||||
contentContainerStyle={{ flexGrow: 1 }}
|
||||
>
|
||||
<View style={styles.daysContainer}>
|
||||
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
|
||||
renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1)
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
<View style={styles.daysContainer}>
|
||||
{VISIBLE_DAY_VALUES.map((dayOfWeek, index) =>
|
||||
renderDayColumn(dayOfWeek, index, index === VISIBLE_DAY_VALUES.length - 1)
|
||||
)}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</PanGestureHandler>
|
||||
</GestureDetector>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user