From 97912f6c13f2f2f7625ee8420b1d784256d7b816 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 29 Jan 2026 22:24:05 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=E3=83=9E=E3=83=83=E3=83=97=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/eas-build.yml | 68 +++++++ .github/workflows/eas-submit.yml | 50 +++++ app.config.ts | 1 + app/(tabs)/_layout.tsx | 7 + app/(tabs)/logs.tsx | 22 +-- app/(tabs)/map.tsx | 327 +++++++++++++++++++++++++++++++ app/(tabs)/timeline.tsx | 22 +-- constants/map-colors.ts | 27 +++ hooks/use-device-trajectory.ts | 70 +++++++ lib/location-store.tsx | 6 +- package.json | 3 +- pnpm-lock.yaml | 27 +++ 12 files changed, 604 insertions(+), 26 deletions(-) create mode 100644 .github/workflows/eas-build.yml create mode 100644 .github/workflows/eas-submit.yml create mode 100644 app/(tabs)/map.tsx create mode 100644 constants/map-colors.ts create mode 100644 hooks/use-device-trajectory.ts diff --git a/.github/workflows/eas-build.yml b/.github/workflows/eas-build.yml new file mode 100644 index 0000000..d5c92ad --- /dev/null +++ b/.github/workflows/eas-build.yml @@ -0,0 +1,68 @@ +name: EAS Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + inputs: + profile: + description: 'Build profile' + required: true + default: 'preview' + type: choice + options: + - development + - preview + - production + platform: + description: 'Platform' + required: true + default: 'all' + type: choice + options: + - all + - ios + - android + +jobs: + build: + name: EAS Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install dependencies + run: pnpm install + + - name: Setup EAS + uses: expo/expo-github-action@v8 + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Build (Push to main) + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: eas build --profile production --platform all --non-interactive + + - name: Build (Pull Request) + if: github.event_name == 'pull_request' + run: eas build --profile preview --platform all --non-interactive + + - name: Build (Manual) + if: github.event_name == 'workflow_dispatch' + run: eas build --profile ${{ inputs.profile }} --platform ${{ inputs.platform }} --non-interactive diff --git a/.github/workflows/eas-submit.yml b/.github/workflows/eas-submit.yml new file mode 100644 index 0000000..e6a7f13 --- /dev/null +++ b/.github/workflows/eas-submit.yml @@ -0,0 +1,50 @@ +name: EAS Submit + +on: + workflow_dispatch: + inputs: + platform: + description: 'Platform' + required: true + default: 'all' + type: choice + options: + - all + - ios + - android + +jobs: + submit: + name: Submit to Store + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Install dependencies + run: pnpm install + + - name: Setup EAS + uses: expo/expo-github-action@v8 + with: + eas-version: latest + token: ${{ secrets.EXPO_TOKEN }} + + - name: Submit to stores + run: eas submit --profile production --platform ${{ inputs.platform }} --non-interactive + env: + # iOS + EXPO_APPLE_ID: ${{ secrets.EXPO_APPLE_ID }} + EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }} + # Android + EXPO_ANDROID_SERVICE_ACCOUNT_KEY: ${{ secrets.EXPO_ANDROID_SERVICE_ACCOUNT_KEY }} diff --git a/app.config.ts b/app.config.ts index 744eff0..4b7460e 100644 --- a/app.config.ts +++ b/app.config.ts @@ -69,6 +69,7 @@ const config: ExpoConfig = { }, plugins: [ "expo-router", + "react-native-maps", [ "expo-splash-screen", { diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx index 1702e8e..b26c41c 100644 --- a/app/(tabs)/_layout.tsx +++ b/app/(tabs)/_layout.tsx @@ -42,6 +42,13 @@ export default function TabLayout() { tabBarIcon: ({ color }) => , }} /> + , + }} + /> { return { - height: heightValue.value, + maxHeight: maxHeightValue.value, opacity: opacityValue.value, overflow: "hidden" as const, }; @@ -87,10 +87,10 @@ export default function LogsScreen() { return Array.from(deviceSet).sort(); }, [state.logs]); - // デバイスフィルターがある場合は高さを増やす - const contentHeight = logDeviceIds.length > 0 - ? ACCORDION_CONTENT_HEIGHT_WITH_DEVICE - : ACCORDION_CONTENT_HEIGHT_BASE; + // デバイスフィルターがある場合は最大高さを増やす + const maxContentHeight = logDeviceIds.length > 0 + ? ACCORDION_MAX_HEIGHT_WITH_DEVICE + : ACCORDION_MAX_HEIGHT_BASE; // フィルタリングされたログ const filteredLogs = useMemo(() => { @@ -226,9 +226,9 @@ export default function LogsScreen() { }; rotateValue.value = withTiming(newValue ? 180 : 0, animConfig); - heightValue.value = withTiming(newValue ? contentHeight : 0, animConfig); + maxHeightValue.value = withTiming(newValue ? maxContentHeight : 0, animConfig); opacityValue.value = withTiming(newValue ? 1 : 0, { duration: newValue ? 250 : 150 }); - }, [isFilterExpanded, rotateValue, heightValue, opacityValue, contentHeight]); + }, [isFilterExpanded, rotateValue, maxHeightValue, opacityValue, maxContentHeight]); // 検索クリア const handleClearSearch = useCallback(() => { diff --git a/app/(tabs)/map.tsx b/app/(tabs)/map.tsx new file mode 100644 index 0000000..476eb7f --- /dev/null +++ b/app/(tabs)/map.tsx @@ -0,0 +1,327 @@ +import { useState, useCallback, useRef, useEffect } from "react"; +import { + Text, + View, + TouchableOpacity, + StyleSheet, + Platform, + ScrollView, +} from "react-native"; +import * as Haptics from "expo-haptics"; +import Animated, { + useAnimatedStyle, + withTiming, + useSharedValue, + Easing, +} from "react-native-reanimated"; + +import { ScreenContainer } from "@/components/screen-container"; +import { ConnectionStatusBadge } from "@/components/connection-status"; +import { useLocation } from "@/lib/location-store"; +import { cn } from "@/lib/utils"; +import { + useDeviceTrajectory, + getAllCoordinates, +} from "@/hooks/use-device-trajectory"; +import { getDeviceColor } from "@/constants/map-colors"; + +// react-native-maps は Web では使えないので条件付きインポート +let MapView: typeof import("react-native-maps").default | null = null; +let Polyline: typeof import("react-native-maps").Polyline | null = null; +let Marker: typeof import("react-native-maps").Marker | null = null; + +if (Platform.OS !== "web") { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const Maps = require("react-native-maps"); + MapView = Maps.default; + Polyline = Maps.Polyline; + Marker = Maps.Marker; +} + +type MapViewRef = import("react-native-maps").default; + +// アコーディオンコンテンツの最大高さ(アニメーション用) +const ACCORDION_MAX_HEIGHT = 200; + +export default function MapScreen() { + const { state } = useLocation(); + const [selectedDevices, setSelectedDevices] = useState>(new Set()); + const [isFilterExpanded, setIsFilterExpanded] = useState(true); + const mapRef = useRef(null); + + // アニメーション用の共有値(開いた状態で初期化) + const rotateValue = useSharedValue(180); + const maxHeightValue = useSharedValue(ACCORDION_MAX_HEIGHT); + const opacityValue = useSharedValue(1); + + // 軌跡データを計算 + const trajectories = useDeviceTrajectory(state.updates, selectedDevices); + + // 矢印の回転アニメーション + const arrowStyle = useAnimatedStyle(() => { + return { + transform: [{ rotate: `${rotateValue.value}deg` }], + }; + }); + + // コンテンツの高さアニメーション + const contentStyle = useAnimatedStyle(() => { + return { + maxHeight: maxHeightValue.value, + opacity: opacityValue.value, + overflow: "hidden" as const, + }; + }); + + // デバイス選択が変わったらカメラ調整 + useEffect(() => { + if (Platform.OS === "web" || !mapRef.current) return; + + const allCoords = getAllCoordinates(trajectories); + if (allCoords.length === 0) return; + + mapRef.current.fitToCoordinates(allCoords, { + edgePadding: { top: 50, right: 50, bottom: 50, left: 50 }, + animated: true, + }); + }, [trajectories]); + + // デバイスフィルターの選択/解除 + const handleDeviceSelect = useCallback((value: string | null) => { + if (Platform.OS !== "web") { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + if (value === null) { + setSelectedDevices(new Set()); + } else { + setSelectedDevices((prev) => { + const newSet = new Set(prev); + if (newSet.has(value)) { + newSet.delete(value); + } else { + newSet.add(value); + } + return newSet; + }); + } + }, []); + + const toggleFilter = useCallback(() => { + if (Platform.OS !== "web") { + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + } + + const newValue = !isFilterExpanded; + setIsFilterExpanded(newValue); + + // アニメーション設定 + const animConfig = { + duration: 250, + easing: Easing.bezier(0.4, 0, 0.2, 1), + }; + + rotateValue.value = withTiming(newValue ? 180 : 0, animConfig); + maxHeightValue.value = withTiming(newValue ? ACCORDION_MAX_HEIGHT : 0, animConfig); + opacityValue.value = withTiming(newValue ? 1 : 0, { duration: newValue ? 250 : 150 }); + }, [isFilterExpanded, rotateValue, maxHeightValue, opacityValue]); + + // Web用のフォールバック + if (Platform.OS === "web") { + return ( + + + + マップ + + + + 🗺️ + Web未対応 + + マップ機能はiOS/Androidアプリでのみ利用可能です + + + + + ); + } + + return ( + + + {/* Header */} + + マップ + + + + {/* Device Filter Accordion */} + + {/* Accordion Header */} + + + + + デバイス + + {selectedDevices.size > 0 && ( + + + {selectedDevices.size}件選択中 + + + )} + + + ▼ + + + + + {/* Accordion Content with Animation */} + + + + {state.deviceIds.length > 0 ? ( + + {/* 選択解除ボタン */} + handleDeviceSelect(null)} + activeOpacity={0.7} + style={styles.filterButton} + > + + + 選択解除 + + + + {state.deviceIds.map((device) => { + const deviceColor = getDeviceColor(device, state.deviceIds); + const isSelected = selectedDevices.has(device); + return ( + handleDeviceSelect(device)} + activeOpacity={0.7} + style={styles.filterButton} + > + + + {device} + + + + ); + })} + + ) : ( + デバイスがありません + )} + + + + + + {/* Map View */} + + {selectedDevices.size === 0 ? ( + + 📍 + + デバイスを選択してください + + + 上のフィルターからデバイスを選択すると軌跡が表示されます + + + ) : MapView ? ( + + {trajectories.map((trajectory) => ( + + {Polyline && trajectory.coordinates.length > 1 && ( + + )} + {Marker && trajectory.latestPosition && ( + + )} + + ))} + + ) : null} + + + + ); +} + +const styles = StyleSheet.create({ + accordionHeader: { + minHeight: 48, + justifyContent: "center", + }, + arrowIcon: { + fontSize: 12, + color: "#687076", + }, + filterScrollContent: { + gap: 8, + }, + filterButton: { + flexShrink: 0, + marginBottom: 4, + }, + map: { + flex: 1, + }, +}); diff --git a/app/(tabs)/timeline.tsx b/app/(tabs)/timeline.tsx index 4d14596..35c05ca 100644 --- a/app/(tabs)/timeline.tsx +++ b/app/(tabs)/timeline.tsx @@ -34,9 +34,9 @@ const MOVING_STATES: { value: MovingState; label: string }[] = [ { value: "moving", label: "移動中" }, ]; -// アコーディオンコンテンツの高さ -const ACCORDION_CONTENT_HEIGHT_BASE = 80; // 状態フィルターのみ -const ACCORDION_CONTENT_HEIGHT_WITH_DEVICE = 150; // 状態 + デバイスフィルター +// アコーディオンコンテンツの最大高さ(アニメーション用) +const ACCORDION_MAX_HEIGHT_BASE = 200; // 状態フィルターのみ +const ACCORDION_MAX_HEIGHT_WITH_DEVICE = 300; // 状態 + デバイスフィルター export default function TimelineScreen() { const { state, clearUpdates } = useLocation(); @@ -50,7 +50,7 @@ export default function TimelineScreen() { // アニメーション用の共有値 const rotateValue = useSharedValue(0); - const heightValue = useSharedValue(0); + const maxHeightValue = useSharedValue(0); const opacityValue = useSharedValue(0); // 矢印の回転アニメーション @@ -63,16 +63,16 @@ export default function TimelineScreen() { // コンテンツの高さアニメーション const contentStyle = useAnimatedStyle(() => { return { - height: heightValue.value, + maxHeight: maxHeightValue.value, opacity: opacityValue.value, overflow: "hidden" as const, }; }); - // デバイスフィルターがある場合は高さを増やす - const contentHeight = state.deviceIds.length > 0 - ? ACCORDION_CONTENT_HEIGHT_WITH_DEVICE - : ACCORDION_CONTENT_HEIGHT_BASE; + // デバイスフィルターがある場合は最大高さを増やす + const maxContentHeight = state.deviceIds.length > 0 + ? ACCORDION_MAX_HEIGHT_WITH_DEVICE + : ACCORDION_MAX_HEIGHT_BASE; // Filter updates by search query, selected states and devices const filteredUpdates = useMemo(() => { @@ -184,9 +184,9 @@ export default function TimelineScreen() { }; rotateValue.value = withTiming(newValue ? 180 : 0, animConfig); - heightValue.value = withTiming(newValue ? contentHeight : 0, animConfig); + maxHeightValue.value = withTiming(newValue ? maxContentHeight : 0, animConfig); opacityValue.value = withTiming(newValue ? 1 : 0, { duration: newValue ? 250 : 150 }); - }, [isFilterExpanded, rotateValue, heightValue, opacityValue, contentHeight]); + }, [isFilterExpanded, rotateValue, maxHeightValue, opacityValue, maxContentHeight]); // 検索クリア const handleClearSearch = useCallback(() => { diff --git a/constants/map-colors.ts b/constants/map-colors.ts new file mode 100644 index 0000000..bf02052 --- /dev/null +++ b/constants/map-colors.ts @@ -0,0 +1,27 @@ +/** + * マップ上でデバイス軌跡を表示する際の色パレット + */ +export const MAP_TRAJECTORY_COLORS = [ + "#FF6B6B", // 赤 + "#4ECDC4", // ティール + "#45B7D1", // 水色 + "#96CEB4", // ミントグリーン + "#FFEAA7", // 黄色 + "#DDA0DD", // プラム + "#98D8C8", // シーグリーン + "#F7DC6F", // ゴールド + "#BB8FCE", // 紫 + "#85C1E9", // スカイブルー +] as const; + +/** + * デバイスIDからPolylineの色を取得 + * デバイスIDのハッシュ値を使って色を決定する + */ +export function getDeviceColor(deviceId: string, deviceIds: string[]): string { + const index = deviceIds.indexOf(deviceId); + if (index === -1) { + return MAP_TRAJECTORY_COLORS[0]; + } + return MAP_TRAJECTORY_COLORS[index % MAP_TRAJECTORY_COLORS.length]; +} diff --git a/hooks/use-device-trajectory.ts b/hooks/use-device-trajectory.ts new file mode 100644 index 0000000..4d8e577 --- /dev/null +++ b/hooks/use-device-trajectory.ts @@ -0,0 +1,70 @@ +import { useMemo } from "react"; +import type { LocationUpdate } from "@/lib/types/location"; + +export interface Coordinate { + latitude: number; + longitude: number; +} + +export interface DeviceTrajectory { + deviceId: string; + coordinates: Coordinate[]; + latestPosition: Coordinate | null; +} + +/** + * 選択されたデバイスの軌跡データを計算するフック + */ +export function useDeviceTrajectory( + updates: LocationUpdate[], + selectedDevices: Set +): DeviceTrajectory[] { + return useMemo(() => { + if (selectedDevices.size === 0) { + return []; + } + + const deviceMap = new Map(); + + // デバイスごとにupdatesをグループ化 + for (const update of updates) { + if (!selectedDevices.has(update.device)) { + continue; + } + const existing = deviceMap.get(update.device) || []; + existing.push(update); + deviceMap.set(update.device, existing); + } + + // 各デバイスの軌跡を作成 + const trajectories: DeviceTrajectory[] = []; + + for (const [deviceId, deviceUpdates] of deviceMap) { + // timestampでソート(古い順) + const sorted = [...deviceUpdates].sort((a, b) => a.timestamp - b.timestamp); + + const coordinates: Coordinate[] = sorted.map((u) => ({ + latitude: u.coords.latitude, + longitude: u.coords.longitude, + })); + + const latestPosition = + coordinates.length > 0 ? coordinates[coordinates.length - 1] : null; + + trajectories.push({ + deviceId, + coordinates, + latestPosition, + }); + } + + return trajectories; + }, [updates, selectedDevices]); +} + +/** + * 全ての座標を含む領域を計算 + */ +export function getAllCoordinates(trajectories: DeviceTrajectory[]): Coordinate[] { + return trajectories.flatMap((t) => t.coordinates); +} diff --git a/lib/location-store.tsx b/lib/location-store.tsx index ebf9d5b..8b929e3 100644 --- a/lib/location-store.tsx +++ b/lib/location-store.tsx @@ -36,9 +36,9 @@ function locationReducer(state: LocationState, action: LocationAction): Location case "ADD_UPDATE": { const update = action.payload; const newUpdates = [update, ...state.updates].slice(0, MAX_UPDATES); - const deviceIds = Array.from( - new Set([update.device, ...state.deviceIds]) - ); + const deviceIds = state.deviceIds.includes(update.device) + ? state.deviceIds + : [...state.deviceIds, update.device]; return { ...state, updates: newUpdates, diff --git a/package.json b/package.json index 634e255..22c4545 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "concurrently -k \"pnpm dev:server\" \"pnpm dev:metro\"", "dev:server": "cross-env NODE_ENV=development tsx watch server/_core/index.ts", - "dev:metro": "cross-env EXPO_USE_METRO_WORKSPACE_ROOT=1 npx expo start --web --port ${EXPO_PORT:-8081}", + "dev:metro": "cross-env EXPO_USE_METRO_WORKSPACE_ROOT=1 npx expo start --port ${EXPO_PORT:-8081}", "build": "esbuild server/_core/index.ts --platform=node --packages=external --bundle --format=esm --outdir=dist", "start": "NODE_ENV=production node dist/index.js", "check": "tsc --noEmit", @@ -59,6 +59,7 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", + "react-native-maps": "^1.26.20", "react-native-reanimated": "~4.1.6", "react-native-safe-area-context": "~5.6.2", "react-native-screens": "~4.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f12d2a6..d324edb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -125,6 +125,9 @@ importers: react-native-gesture-handler: specifier: ~2.28.0 version: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-maps: + specifier: ^1.26.20 + version: 1.26.20(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) react-native-reanimated: specifier: ~4.1.6 version: 4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) @@ -2190,6 +2193,9 @@ packages: '@types/express@4.17.25': resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -5121,6 +5127,17 @@ packages: react: '*' react-native: '*' + react-native-maps@1.26.20: + resolution: {integrity: sha512-kWibDz6wLLQ0685gOEFz5jdzm4miD7PMeVdtZV7ilgftDcusC2iy7SueBJpHF0LKCoOSa1BEUiKqpx1dBMSNpA==} + engines: {node: '>= 20.19.4'} + peerDependencies: + react: '>= 18.3.1' + react-native: '>= 0.76.0' + react-native-web: '>= 0.11' + peerDependenciesMeta: + react-native-web: + optional: true + react-native-reanimated@4.1.6: resolution: {integrity: sha512-F+ZJBYiok/6Jzp1re75F/9aLzkgoQCOh4yxrnwATa8392RvM3kx+fiXXFvwcgE59v48lMwd9q0nzF1oJLXpfxQ==} peerDependencies: @@ -8123,6 +8140,8 @@ snapshots: '@types/qs': 6.14.0 '@types/serve-static': 1.15.10 + '@types/geojson@7946.0.16': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 22.19.3 @@ -11432,6 +11451,14 @@ snapshots: react: 19.1.0 react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + react-native-maps@1.26.20(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + dependencies: + '@types/geojson': 7946.0.16 + react: 19.1.0 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0) + optionalDependencies: + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-native-reanimated@4.1.6(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): dependencies: '@babel/core': 7.28.5 From c122793c16db20d74c1fe4739e379000ce6a849d Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 29 Jan 2026 22:24:40 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E7=8F=BE=E7=8A=B6=E4=B8=8D=E8=A6=81?= =?UTF-8?q?=E3=81=AA=E5=A4=89=E6=9B=B4=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/eas-build.yml | 68 -------------------------------- .github/workflows/eas-submit.yml | 50 ----------------------- 2 files changed, 118 deletions(-) delete mode 100644 .github/workflows/eas-build.yml delete mode 100644 .github/workflows/eas-submit.yml diff --git a/.github/workflows/eas-build.yml b/.github/workflows/eas-build.yml deleted file mode 100644 index d5c92ad..0000000 --- a/.github/workflows/eas-build.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: EAS Build - -on: - push: - branches: - - main - pull_request: - branches: - - main - workflow_dispatch: - inputs: - profile: - description: 'Build profile' - required: true - default: 'preview' - type: choice - options: - - development - - preview - - production - platform: - description: 'Platform' - required: true - default: 'all' - type: choice - options: - - all - - ios - - android - -jobs: - build: - name: EAS Build - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Install dependencies - run: pnpm install - - - name: Setup EAS - uses: expo/expo-github-action@v8 - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Build (Push to main) - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - run: eas build --profile production --platform all --non-interactive - - - name: Build (Pull Request) - if: github.event_name == 'pull_request' - run: eas build --profile preview --platform all --non-interactive - - - name: Build (Manual) - if: github.event_name == 'workflow_dispatch' - run: eas build --profile ${{ inputs.profile }} --platform ${{ inputs.platform }} --non-interactive diff --git a/.github/workflows/eas-submit.yml b/.github/workflows/eas-submit.yml deleted file mode 100644 index e6a7f13..0000000 --- a/.github/workflows/eas-submit.yml +++ /dev/null @@ -1,50 +0,0 @@ -name: EAS Submit - -on: - workflow_dispatch: - inputs: - platform: - description: 'Platform' - required: true - default: 'all' - type: choice - options: - - all - - ios - - android - -jobs: - submit: - name: Submit to Store - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 10 - - - name: Install dependencies - run: pnpm install - - - name: Setup EAS - uses: expo/expo-github-action@v8 - with: - eas-version: latest - token: ${{ secrets.EXPO_TOKEN }} - - - name: Submit to stores - run: eas submit --profile production --platform ${{ inputs.platform }} --non-interactive - env: - # iOS - EXPO_APPLE_ID: ${{ secrets.EXPO_APPLE_ID }} - EXPO_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.EXPO_APPLE_APP_SPECIFIC_PASSWORD }} - # Android - EXPO_ANDROID_SERVICE_ACCOUNT_KEY: ${{ secrets.EXPO_ANDROID_SERVICE_ACCOUNT_KEY }} From 45401b14959a5bb003c066297a958cb58539a443 Mon Sep 17 00:00:00 2001 From: Tsubasa SEKIGUCHI Date: Thu, 29 Jan 2026 22:33:50 +0900 Subject: [PATCH 3/3] =?UTF-8?q?=E3=83=9E=E3=83=83=E3=83=97=E7=94=BB?= =?UTF-8?q?=E9=9D=A2=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3=E3=83=9E?= =?UTF-8?q?=E3=83=83=E3=83=94=E3=83=B3=E3=82=B0=E8=BF=BD=E5=8A=A0=E3=81=A8?= =?UTF-8?q?=E3=82=AA=E3=83=BC=E3=83=90=E3=83=BC=E3=83=AC=E3=82=A4=E6=8F=8F?= =?UTF-8?q?=E7=94=BB=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IconSymbolのMAPPINGに"map.fill"エントリを追加し、Android/Webでのランタイムエラーを修正。 MapView内のMarker/PolylineをViewではなくFragmentで囲むことで、 react-native-mapsのオーバーレイが正しく描画されるよう修正。 Co-Authored-By: Claude Opus 4.5 --- app/(tabs)/map.tsx | 6 +++--- components/ui/icon-symbol.tsx | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/(tabs)/map.tsx b/app/(tabs)/map.tsx index 476eb7f..cde46a7 100644 --- a/app/(tabs)/map.tsx +++ b/app/(tabs)/map.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useRef, useEffect } from "react"; +import { useState, useCallback, useRef, useEffect, Fragment } from "react"; import { Text, View, @@ -279,7 +279,7 @@ export default function MapScreen() { }} > {trajectories.map((trajectory) => ( - + {Polyline && trajectory.coordinates.length > 1 && ( )} - + ))} ) : null} diff --git a/components/ui/icon-symbol.tsx b/components/ui/icon-symbol.tsx index ec3909b..ee612e1 100644 --- a/components/ui/icon-symbol.tsx +++ b/components/ui/icon-symbol.tsx @@ -21,6 +21,7 @@ const MAPPING = { "clock.fill": "schedule", "location.fill": "location-on", "doc.text.fill": "article", + "map.fill": "map", } as IconMapping; /**