From 368e03b32b10b0c459e60af2caa9c8ffafa792d5 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 15 Jul 2026 17:46:28 +0700 Subject: [PATCH] Firrt commit --- IMPLEMENTTATION_PLAN.md | 88 + INSTALL.md | 42 + TECH_DESIGN.md | 1088 ++++++++++++ app.json | 52 +- metro.config.js | 6 + nativewind-env.d.ts | 6 + package-lock.json | 1542 ++++++++++++++++- package.json | 18 +- src/app/(auth)/_layout.tsx | 9 + src/app/(auth)/login.tsx | 36 + src/app/(tabs)/_layout.tsx | 52 + src/app/(tabs)/analytics.tsx | 105 ++ src/app/(tabs)/history.tsx | 125 ++ src/app/(tabs)/index.tsx | 211 +++ src/app/_layout.tsx | 44 +- src/app/explore.tsx | 180 -- src/app/index.tsx | 98 -- src/app/modal.tsx | 43 + src/app/replay/[id].tsx | 297 ++++ src/components/animated-icon.module.css | 6 - src/components/animated-icon.tsx | 148 -- src/components/animated-icon.web.tsx | 108 -- src/components/app-tabs.tsx | 32 - src/components/app-tabs.web.tsx | 115 -- src/components/common/Button.tsx | 31 + src/components/external-link.tsx | 25 - src/components/hint-row.tsx | 35 - src/components/themed-text.tsx | 73 - src/components/themed-view.tsx | 16 - src/components/ui/collapsible.tsx | 65 - src/components/web-badge.tsx | 43 - src/constants/Colors.ts | 17 + src/constants/Config.ts | 28 + src/constants/theme.ts | 65 - src/database/repositories/mediaRepository.ts | 13 + .../repositories/workoutRepository.ts | 19 + src/database/sqlite.ts | 51 + src/global.css | 4 + src/hooks/use-color-scheme.ts | 1 - src/hooks/use-color-scheme.web.ts | 21 - src/hooks/use-theme.ts | 14 - src/hooks/useBackgroundGPS.ts | 60 + src/hooks/useBluetoothBLE.ts | 31 + src/hooks/useCalendar.ts | 29 + src/hooks/useWeather.ts | 18 + src/services/bleService.ts | 33 + src/services/weatherService.ts | 32 + src/store/useWorkoutStore.ts | 128 ++ src/types/database.types.ts | 33 + src/types/workout.types.ts | 29 + src/utils/calculations.ts | 41 + src/utils/date.ts | 30 + src/utils/fileSystem.ts | 38 + tailwind.config.js | 9 + tsconfig.json | 3 +- 55 files changed, 4420 insertions(+), 1066 deletions(-) create mode 100644 IMPLEMENTTATION_PLAN.md create mode 100644 INSTALL.md create mode 100644 TECH_DESIGN.md create mode 100644 metro.config.js create mode 100644 nativewind-env.d.ts create mode 100644 src/app/(auth)/_layout.tsx create mode 100644 src/app/(auth)/login.tsx create mode 100644 src/app/(tabs)/_layout.tsx create mode 100644 src/app/(tabs)/analytics.tsx create mode 100644 src/app/(tabs)/history.tsx create mode 100644 src/app/(tabs)/index.tsx delete mode 100644 src/app/explore.tsx delete mode 100644 src/app/index.tsx create mode 100644 src/app/modal.tsx create mode 100644 src/app/replay/[id].tsx delete mode 100644 src/components/animated-icon.module.css delete mode 100644 src/components/animated-icon.tsx delete mode 100644 src/components/animated-icon.web.tsx delete mode 100644 src/components/app-tabs.tsx delete mode 100644 src/components/app-tabs.web.tsx create mode 100644 src/components/common/Button.tsx delete mode 100644 src/components/external-link.tsx delete mode 100644 src/components/hint-row.tsx delete mode 100644 src/components/themed-text.tsx delete mode 100644 src/components/themed-view.tsx delete mode 100644 src/components/ui/collapsible.tsx delete mode 100644 src/components/web-badge.tsx create mode 100644 src/constants/Colors.ts create mode 100644 src/constants/Config.ts delete mode 100644 src/constants/theme.ts create mode 100644 src/database/repositories/mediaRepository.ts create mode 100644 src/database/repositories/workoutRepository.ts create mode 100644 src/database/sqlite.ts delete mode 100644 src/hooks/use-color-scheme.ts delete mode 100644 src/hooks/use-color-scheme.web.ts delete mode 100644 src/hooks/use-theme.ts create mode 100644 src/hooks/useBackgroundGPS.ts create mode 100644 src/hooks/useBluetoothBLE.ts create mode 100644 src/hooks/useCalendar.ts create mode 100644 src/hooks/useWeather.ts create mode 100644 src/services/bleService.ts create mode 100644 src/services/weatherService.ts create mode 100644 src/store/useWorkoutStore.ts create mode 100644 src/types/database.types.ts create mode 100644 src/types/workout.types.ts create mode 100644 src/utils/calculations.ts create mode 100644 src/utils/date.ts create mode 100644 src/utils/fileSystem.ts create mode 100644 tailwind.config.js diff --git a/IMPLEMENTTATION_PLAN.md b/IMPLEMENTTATION_PLAN.md new file mode 100644 index 0000000..f4d9aea --- /dev/null +++ b/IMPLEMENTTATION_PLAN.md @@ -0,0 +1,88 @@ +# Kế hoạch Thiết lập Cấu trúc và Tập tin Cơ bản Dự án GeoSport Log + +Dự án này sẽ chuyển đổi phiên bản Web Prototype của GeoSport Log thành một ứng dụng di động chạy trên iOS và Android sử dụng React Native (Expo SDK), TypeScript, SQLite và NativeWind (Tailwind CSS cho Mobile). + +## User Review Required + +> [!IMPORTANT] +> Chúng ta sẽ khởi tạo dự án Expo sử dụng template mặc định (hỗ trợ sẵn TypeScript & Expo Router) ngay tại thư mục hiện tại (`/home/locpham/geosportlog`). +> Một số tập tin mặc định của Expo sẽ được thay thế hoặc tổ chức lại theo đúng cấu trúc thư mục quy định tại mục 3 của `TECH_DESIGN.md`. + +## Open Questions + +Không có câu hỏi mở. Chúng ta sẽ tiến hành cấu hình theo đúng đặc tả kỹ thuật chi tiết của `TECH_DESIGN.md`. + +## Proposed Changes + +Chúng ta sẽ khởi tạo cấu trúc thư mục chính xác như sau: +- `assets/` - Tài nguyên tĩnh. +- `src/` - Thư mục gốc chứa mã nguồn. + - `app/` - Các màn hình định tuyến bằng Expo Router (Auth, Tabs, Replay). + - `components/` - Các thành phần giao diện dùng chung (Common, Map, Replay, Analytics). + - `constants/` - Quản lý Colors và Config. + - `database/` - Cơ sở dữ liệu SQLite và Repositories. + - `hooks/` - Các Custom Hooks cho GPS, BLE, Weather, Calendar. + - `services/` - Kết nối BLE và Weather API. + - `store/` - Zustand global state. + - `types/` - Type Safety cho database và workout. + - `utils/` - Các hàm tiện ích thuần logic. + +Các tập tin cấu hình cốt lõi và mã nguồn cơ bản sẽ được tạo lập: + +### Cấu hình Dự án + +#### [MODIFY] [app.json](file:///home/locpham/geosportlog/app.json) +Cập nhật quyền định vị chạy ngầm (iOS & Android), Camera, Photo Library, Micro và các plugin cần thiết theo mục 2 của `TECH_DESIGN.md`. + +#### [MODIFY] [tsconfig.json](file:///home/locpham/geosportlog/tsconfig.json) +Cấu hình alias `@/*` ánh xạ tới thư mục `/src/*` theo mục 4 của `TECH_DESIGN.md`. + +#### [NEW] [tailwind.config.js](file:///home/locpham/geosportlog/tailwind.config.js) +Cấu hình NativeWind để hỗ trợ styling bằng Tailwind CSS trên mobile. + +### Phân lớp Dữ liệu và Quản lý Trạng thái (Persistence & State) + +#### [NEW] [sqlite.ts](file:///home/locpham/geosportlog/src/database/sqlite.ts) +Khởi tạo kết nối SQLite cục bộ, định nghĩa lược đồ (schema) cho bảng `workouts`, `trackpoints`, và `media_attachments` theo thiết kế tại mục 5. + +#### [NEW] [useWorkoutStore.ts](file:///home/locpham/geosportlog/src/store/useWorkoutStore.ts) +Zustand store để quản lý trạng thái tập luyện thời gian thực, tính toán cự ly/năng lượng và lưu trữ dữ liệu vào SQLite theo thiết kế tại mục 6. + +### Phân lớp Màn hình (Expo Router Screens) + +#### [NEW] [index.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/index.tsx) +Màn hình HUD ghi nhận hành trình chạy bộ/đạp xe kết hợp bản đồ GPS thời gian thực và theo dõi chỉ số sức khỏe (BPM, Kcal). + +#### [NEW] [analytics.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/analytics.tsx) +Dashboard thống kê tổng quan và vẽ biểu đồ xu hướng bằng `react-native-chart-kit`. + +#### [NEW] [history.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/history.tsx) +Nhật ký lịch tập sử dụng `react-native-calendars` để đánh dấu các ngày tập và liệt kê danh sách hoạt động. + +#### [NEW] [_layout.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/_layout.tsx) +Cấu hình giao diện và định tuyến cho Bottom Tab Bar. + +#### [NEW] [id.tsx](file:///home/locpham/geosportlog/src/app/replay/[id].tsx) +Màn hình trình phát Cinematic POV chạy dọc theo lộ trình GPS, dừng lại để phát video/ảnh gắn mốc địa lý kèm HUD lớp phủ đẹp mắt. + +#### [NEW] [_layout.tsx](file:///home/locpham/geosportlog/src/app/_layout.tsx) +Root Layout khởi tạo cấu hình chung, DB Connection, và các nhà cung cấp trạng thái. + +### Phân lớp Tiện ích, Kiểu dữ liệu và Các dịch vụ khác (Skeletons) + +Để hoàn thiện bộ khung dự án và tránh lỗi import, chúng ta sẽ tạo các tập tin skeleton sau: +- [workout.types.ts](file:///home/locpham/geosportlog/src/types/workout.types.ts): Định nghĩa kiểu dữ liệu `Trackpoint`, `MediaAttachment`, `Workout`. +- [calculations.ts](file:///home/locpham/geosportlog/src/utils/calculations.ts): Công thức Haversine tính khoảng cách và công thức MET tính Kcal. +- [weatherService.ts](file:///home/locpham/geosportlog/src/services/weatherService.ts): Mock/Call Open-Meteo API lấy thông tin thời tiết. +- [Colors.ts](file:///home/locpham/geosportlog/src/constants/Colors.ts) & [Config.ts](file:///home/locpham/geosportlog/src/constants/Config.ts): Mã màu tối tối giản & cấu hình MET. +- Các tập tin Repository, Service, Hook, Utility trống khác dưới dạng skeleton để nhà phát triển điền tiếp nội dung sau. + +--- + +## Verification Plan + +### Automated Tests +- Chạy `tsc --noEmit` để đảm bảo dự án biên dịch thành công mà không có lỗi kiểu dữ liệu TypeScript. + +### Manual Verification +- Xác minh sự tồn tại của đầy đủ các thư mục và tập tin cơ bản theo đúng thiết kế hệ thống. diff --git a/INSTALL.md b/INSTALL.md new file mode 100644 index 0000000..6c984ff --- /dev/null +++ b/INSTALL.md @@ -0,0 +1,42 @@ +# Tài liệu Tổng kết Thiết lập Cấu trúc dự án GeoSport Log + +Chúng ta đã hoàn thành việc khởi tạo cấu trúc và các tập tin cơ bản của dự án di động GeoSport Log (React Native - Expo SDK 57, TypeScript, SQLite, Zustand, NativeWind) dựa trên đặc tả tại [TECH_DESIGN.md](file:///home/locpham/geosportlog/TECH_DESIGN.md). + +## Các thay đổi đã thực hiện + +### 1. Khởi tạo & Cấu hình Hệ thống +- **Khởi tạo:** Thiết lập dự án Expo SDK 57 (hỗ trợ TypeScript & Expo Router) ngay tại thư mục gốc. +- **Cấu hình Quyền & Plugin:** Cập nhật [app.json](file:///home/locpham/geosportlog/app.json) khai báo đầy đủ quyền định vị chạy ngầm (`UIBackgroundModes`, `FOREGROUND_SERVICE`, `ACCESS_BACKGROUND_LOCATION`), quyền truy cập Camera, Photo Library, Micro và cấu hình định vị chạy ngầm. +- **Cấu hình TypeScript Path Mapping:** Cấu hình [tsconfig.json](file:///home/locpham/geosportlog/tsconfig.json) định nghĩa alias `@/*` trỏ về thư mục `/src/*`. +- **Cấu hình NativeWind (Tailwind CSS):** Thiết lập [tailwind.config.js](file:///home/locpham/geosportlog/tailwind.config.js), [metro.config.js](file:///home/locpham/geosportlog/metro.config.js), [src/global.css](file:///home/locpham/geosportlog/src/global.css) và [nativewind-env.d.ts](file:///home/locpham/geosportlog/nativewind-env.d.ts) để hỗ trợ styling Tailwind CSS hoàn toàn tương thích trên mobile. + +### 2. Thiết lập SQLite Database & Zustand State Store +- **Cơ sở dữ liệu:** Tạo [sqlite.ts](file:///home/locpham/geosportlog/src/database/sqlite.ts) thiết lập kết nối cơ sở dữ liệu và tự động khởi tạo các bảng `workouts`, `trackpoints`, `media_attachments` khi khởi chạy ứng dụng. +- **Quản lý Trạng thái:** Tạo [useWorkoutStore.ts](file:///home/locpham/geosportlog/src/store/useWorkoutStore.ts) quản lý trạng thái phiên chạy ngầm, tính toán cự ly thực tế bằng công thức Haversine, tính năng lượng bằng chỉ số MET, và thực thi câu lệnh SQL để lưu trữ offline. + +### 3. Thiết lập các Màn hình Giao diện (Expo Router) +- **Root Layout:** Cập nhật [src/app/_layout.tsx](file:///home/locpham/geosportlog/src/app/_layout.tsx) tự động gọi khởi tạo SQLite schema khi app load và dẫn hướng tới các phân trang. +- **Tabs Layout:** Tạo [src/app/(tabs)/_layout.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/_layout.tsx) định cấu hình Tab Bar (Ghi HUD, Nhật Ký, Phân Tích) với các biểu tượng Ionicons và màu sắc tối tối giản. +- **HUD Record Screen:** Tạo [index.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/index.tsx) với bản đồ maps và bảng hiển thị chỉ số Kcal, BPM, cự ly và nút điều khiển. +- **History Screen:** Tạo [history.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/history.tsx) hiển thị lịch và danh sách các workout lọc theo ngày. +- **Analytics Screen:** Tạo [analytics.tsx](file:///home/locpham/geosportlog/src/app/(tabs)/analytics.tsx) hiển thị biểu đồ xu hướng bằng kit biểu đồ. +- **Replay Cinematic Screen:** Tạo [[id].tsx](file:///home/locpham/geosportlog/src/app/replay/%5Bid%5D.tsx) vẽ góc nhìn Cinematic POV di chuyển trên bản đồ, tích hợp lớp phủ (HUD overlay) SVG vẽ lộ trình mini phát sáng đỏ và chấm neon xanh lá di động tương thích cho ảnh/video. + +### 4. Các Skeleton & Tiện ích nền tảng khác +- Định nghĩa kiểu dữ liệu TS trong [workout.types.ts](file:///home/locpham/geosportlog/src/types/workout.types.ts) và [database.types.ts](file:///home/locpham/geosportlog/src/types/database.types.ts). +- Thêm tiện ích toán học trong [calculations.ts](file:///home/locpham/geosportlog/src/utils/calculations.ts) và tiện ích ngày tháng trong [date.ts](file:///home/locpham/geosportlog/src/utils/date.ts). +- Cấu hình quản lý tập tin cục bộ trong [fileSystem.ts](file:///home/locpham/geosportlog/src/utils/fileSystem.ts) (sử dụng Expo FileSystem Legacy API mới để tương thích tốt với SDK 57). +- Thêm mockup service thời tiết [weatherService.ts](file:///home/locpham/geosportlog/src/services/weatherService.ts), bluetooth [bleService.ts](file:///home/locpham/geosportlog/src/services/bleService.ts) và các hooks điều khiển (`useBackgroundGPS.ts`, `useBluetoothBLE.ts`, `useWeather.ts`, `useCalendar.ts`). +- Dọn dẹp sạch sẽ các tệp tin boilerplate mặc định của dự án để tránh xung đột định tuyến và lỗi biên dịch. + +--- + +## Kết quả Kiểm thử & Xác minh + +Chúng ta đã chạy trình kiểm thử kiểu dữ liệu TypeScript của dự án: +```bash +npx tsc --noEmit +``` +**Kết quả:** Trình biên dịch TypeScript chạy thành công 100% không gặp bất kỳ lỗi biên dịch hay cảnh báo kiểu dữ liệu nào: +`The command completed successfully. No output` +Dự án đã sẵn sàng để phát triển thêm logic kinh doanh hoặc tiến hành chạy thử trên môi trường thiết bị di động thực tế / giả lập. diff --git a/TECH_DESIGN.md b/TECH_DESIGN.md new file mode 100644 index 0000000..ed78f18 --- /dev/null +++ b/TECH_DESIGN.md @@ -0,0 +1,1088 @@ +# BẢN THIẾT KẾ KIẾN TRÚC & PHÁT TRIỂN ỨNG DỤNG GEOSPORT LOG (iOS & Android)Bản tài liệu này hướng dẫn chi tiết cách chuyển đổi phiên bản Web Prototype của GeoSport Log thành một ứng dụng di động Native/Cross-platform hoàn chỉnh sử dụng React Native (Expo SDK), TypeScript, và NativeWind (Tailwind CSS cho Mobile). +## 1. PHÂN TÍCH & LỰA CHỌN CÔNG NGHỆ (TECH STACK) +Để mang lại trải nghiệm mượt mà nhất trên cả hai hệ điều hành iOS và Android, dự án được xây dựng dựa trên các công nghệ sau: +Framework: React Native với Expo SDK (Hỗ trợ cấu hình tối giản, thư viện cảm biến phần cứng đồng bộ tốt). +Ngôn ngữ: TypeScript giúp kiểm soát chặt chẽ các kiểu dữ liệu tọa độ, lịch trình và chỉ số sức khỏe. +UI/Styling: NativeWind (Tailwind v3/v4) để tái sử dụng toàn bộ tư duy thiết kế tối giản (Minimalism) và hệ màu tối (Dark Mode) từ bản Web. +Bản đồ Native: react-native-maps tích hợp Apple Maps (iOS) và Google Maps (Android) đạt hiệu năng 60fps khi thu phóng và xoay bản đồ. +Lưu trữ Offline: expo-sqlite quản lý dữ liệu log hành trình, tọa độ GPS, nhịp tim và đường dẫn tệp đa phương tiện trực tiếp trên thiết bị. +## 2. QUYỀN HỆ THỐNG & CẤU HÌNH PHẦN CỨNG (app.json) +Để hệ điều hành cho phép theo dõi GPS khi tắt màn hình và chụp ảnh/quay phim gắn vị trí, chúng ta cần cấu hình quyền truy cập hệ thống chi tiết trong tệp app.json của Expo. + +```json +{ + "expo": { + "name": "GeoSport Log", + "slug": "geosport-log", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "dark", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#050811" + }, + "ios": { + "supportsTablet": false, + "bundleIdentifier": "com.geosport.log", + "infoPlist": { + "UIBackgroundModes": ["location"], + "NSLocationWhenInUseUsageDescription": "GeoSport Log cần quyền định vị khi mở ứng dụng để vẽ lộ trình chạy bộ/đạp xe thời gian thực của bạn.", + "NSLocationAlwaysAndWhenInUseUsageDescription": "GeoSport Log cần quyền định vị chạy ngầm để tiếp tục ghi nhận lộ trình của bạn khi màn hình điện thoại tắt hoặc bỏ vào túi.", + "NSCameraUsageDescription": "Cho phép ứng dụng sử dụng Camera để chụp ảnh hoặc quay video gắn mốc GPS trực tiếp trên hành trình di chuyển.", + "NSMicrophoneUsageDescription": "Cho phép ứng dụng sử dụng Micro khi quay video hành trình.", + "NSPhotoLibraryUsageDescription": "Cho phép ứng dụng truy cập thư viện để đính kèm hình ảnh/video phong cảnh của hành trình tập luyện." + } + }, + "android": { + "package": "com.geosport.log", + "permissions": [ + "ACCESS_COARSE_LOCATION", + "ACCESS_FINE_LOCATION", + "ACCESS_BACKGROUND_LOCATION", + "CAMERA", + "RECORD_AUDIO", + "READ_EXTERNAL_STORAGE", + "WRITE_EXTERNAL_STORAGE", + "FOREGROUND_SERVICE" + ] + }, + "plugins": [ + [ + "expo-location", + { + "locationAlwaysAndWhenInUsePermission": "Cho phép GeoSport Log theo dõi hành trình của bạn kể cả khi chạy ngầm dưới nền để tránh mất dấu tracklog." + } + ], + [ + "expo-camera", + { + "cameraPermission": "Cho phép truy cập máy ảnh để chụp hình lưu niệm dọc đường đi." + } + ] + ] + } +} +## 3. CẤU TRÚC THƯ MỤC CHUẨN HOÁ SẢN XUẤT (NO-REFACTOR DIRECTORY) +Để tránh hoàn toàn việc phải tái cấu trúc hệ thống khi mở rộng tính năng (thêm màn hình, thêm cảm biến ngoại vi, đồng bộ đám mây), dự án được xây dựng theo kiến trúc phân lớp hướng đối tượng kết hợp với Expo Router v3 (File-based Routing). +Hệ thống sử dụng TypeScript Path Aliases (@/*) trỏ thẳng về thư mục /src, giải quyết triệt để lỗi "đường dẫn tương đối tương đối" (../../../../). + +geosport-log-mobile/ +├── assets/ # Tài nguyên tĩnh (Fonts, Icons, Splash Screens) +├── src/ +│ ├── app/ # PHÂN LỚP ĐƯỜNG DẪN (Expo Router 3+ Entry Points) +│ │ ├── (auth)/ # Nhóm phân quyền tài khoản (Đăng nhập, đăng ký) +│ │ │ ├── login.tsx +│ │ │ └── _layout.tsx +│ │ ├── (tabs)/ # Nhóm giao diện chính (Bottom Navigation) +│ │ │ ├── index.tsx # Màn hình ghi HUD (Bản đồ GPS, Kcal, Nhịp tim) +│ │ │ ├── history.tsx # Nhật ký Lịch (Workout Calendar) +│ │ │ ├── analytics.tsx # Dashboard Biểu đồ phân tích (Chart.js) +│ │ │ └── _layout.tsx # Custom bottom tab bar styled +│ │ ├── replay/ # Nhóm trình chiếu Cinematic +│ │ │ └── [id].tsx # Màn hình phát Cinematic theo ID hành trình (Gồm overlay Video/Ảnh mới) +│ │ ├── _layout.tsx # Root Layout (Khởi tạo Database, Zustand State Providers) +│ │ └── modal.tsx # Camera overlay sheet / Chọn ảnh mẫu +│ │ +│ ├── components/ # PHÂN LỚP GIAO DIỆN (Tái sử dụng) +│ │ ├── common/ # UI nền tảng (Buttons, Custom Modals, Toasts, Loaders) +│ │ ├── map/ # UI bản đồ (Custom MapView, Polyline, Custom Pins) +│ │ ├── replay/ # UI chiếu phim (Cinematic HUD, Media Player Overlay) +│ │ └── analytics/ # UI biểu đồ (Chart Wrappers, Stat Cards) +│ │ +│ ├── constants/ # PHÂN LỚP HẰNG SỐ & ĐỊNH CẤU HÌNH +│ │ ├── Colors.ts # Hệ thống mã màu tối tối giản (Dark Mode Palette) +│ │ └── Config.ts # Cấu hình MET values, chu kỳ lấy mẫu, Bluetooth UUIDs +│ │ +│ ├── database/ # PHÂN LỚP PERSISTENCE (SQLite Engine) +│ │ ├── sqlite.ts # Trình khởi tạo SQLite, quản lý phiên bản (Migration) +│ │ └── repositories/ # Mô hình Repository cô lập câu lệnh SQL với giao diện UI +│ │ ├── workoutRepository.ts +│ │ └── mediaRepository.ts +│ │ +│ ├── hooks/ # PHÂN LỚP HOOKS TỰ CHẾ (Side Effects & State) +│ │ ├── useBackgroundGPS.ts # Hook kích hoạt & dừng tracking GPS chạy ngầm +│ │ ├── useBluetoothBLE.ts # Hook quét & ghép nối thiết bị đo nhịp tim (ESP32/Cảm biến) +│ │ ├── useWeather.ts # Hook tự động tải dữ liệu thời tiết dựa theo GPS +│ │ └── useCalendar.ts # Hook tính toán dữ liệu hiển thị lịch tuần/tháng +│ │ +│ ├── services/ # PHÂN LỚP KẾT NỐI NGOẠI VI (API & Hardware Clients) +│ │ ├── weatherService.ts # Open-Meteo REST API wrapper +│ │ └── bleService.ts # SDK Bluetooth Low Energy (quản lý kết nối ESP32) +│ │ +│ ├── store/ # PHÂN LỚP STATE TOÀN CỤC (Zustand Engines) +│ │ └── useWorkoutStore.ts # Quản lý phiên ghi, tính Kcal thời gian thực +│ │ +│ ├── types/ # PHÂN LỚP KIỂU TS (Type Safety) +│ │ ├── database.types.ts # Kiểu dữ liệu tương thích Schema SQLite +│ │ └── workout.types.ts # Kiểu dữ liệu Trackpoint, Media, Workout +│ │ +│ └── utils/ # PHÂN LỚP HÀM TIỆN ÍCH PURE LOGIC +│ ├── calculations.ts # Công thức Haversine, công thức MET, tính Kcal +│ ├── date.ts # Định dạng chuỗi ngày tháng, nhãn thời gian +│ └── fileSystem.ts # Expo FileSystem quản lý ghi, nén tệp ảnh/video cục bộ +│ +├── tailwind.config.js # Cấu hình NativeWind (Tailwind CSS cho mobile) +├── tsconfig.json # Cấu hình TypeScript Paths (`@/*` mapping) +└── package.json # Danh sách thư viện liên kết + +## 4. CHUẨN HOÁ CẤU HÌNH TSCONFIG & PATH ALIASES (tsconfig.json) + +Để liên kết các thành phần kiến trúc không bị lỗi đường dẫn khi di chuyển các file, cấu hình tsconfig.json bắt buộc phải map thư mục gốc @/ trỏ thẳng vào /src. + +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + } +} + +## 5. THIẾT KẾ CƠ SỞ DỮ LIỆU SQLITE CỤC BỘ (sqlite.ts) + +Ứng dụng lưu trữ dữ liệu theo mô hình Offline-First. Khi người dùng đi vào vùng núi hoặc mất sóng mạng 4G, mọi tọa độ GPS, ảnh chụp và dữ liệu nhịp tim vẫn được lưu trữ bình thường vào SQLite cục bộ. + +// src/database/sqlite.ts +import * as SQLite from 'expo-sqlite'; + +let db: SQLite.SQLiteDatabase | null = null; + +export const getDBConnection = async (): Promise => { + if (db) return db; + db = await SQLite.openDatabaseAsync('geosport_log.db'); + + // Khởi tạo các bảng dữ liệu nếu chưa tồn tại + await db.execAsync(` + PRAGMA foreign_keys = ON; + + CREATE TABLE IF NOT EXISTS workouts ( + id TEXT PRIMARY KEY NOT NULL, + title TEXT NOT NULL, + type TEXT NOT NULL, /* RUNNING, CYCLING, WALKING */ + date TEXT NOT NULL, /* YYYY-MM-DD */ + start_time INTEGER NOT NULL, /* Timestamp */ + duration INTEGER NOT NULL, /* Seconds */ + distance REAL NOT NULL, /* Km */ + avg_speed REAL NOT NULL, + avg_hr INTEGER, + calories INTEGER NOT NULL + ); + + CREATE TABLE IF NOT EXISTS trackpoints ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + workout_id TEXT NOT NULL, + latitude REAL NOT NULL, + longitude REAL NOT NULL, + speed REAL NOT NULL, + heart_rate INTEGER, + timestamp INTEGER NOT NULL, + FOREIGN KEY (workout_id) REFERENCES workouts (id) ON DELETE CASCADE + ); + + CREATE TABLE IF NOT EXISTS media_attachments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + workout_id TEXT NOT NULL, + latitude REAL NOT NULL, + longitude REAL NOT NULL, + type TEXT NOT NULL, /* IMAGE, VIDEO */ + file_uri TEXT NOT NULL, /* Local File System URI */ + caption TEXT, + timestamp INTEGER NOT NULL, + FOREIGN KEY (workout_id) REFERENCES workouts (id) ON DELETE CASCADE + ); + `); + + return db; +}; + +## 6. TEMPLATE QUẢN LÝ TRẠNG THÁI TOÀN CỤC (useWorkoutStore.ts) + +Sử dụng Zustand để quản lý trạng thái tập luyện thời gian thực, tích hợp bộ xử lý tính toán nhịp tim và lượng Kcal tiêu thụ theo thời gian thực. + +// src/store/useWorkoutStore.ts +import { create } from 'zustand'; +import { getDBConnection } from '../database/sqlite'; +import { Trackpoint, MediaAttachment } from '../types/workout.types'; +import { calculateDistance, calculateMetKcal } from '../utils/calculations'; + +interface WorkoutState { + isTracking: boolean; + workoutType: 'RUNNING' | 'CYCLING' | 'WALKING'; + currentWorkoutId: string | null; + routePoints: Trackpoint[]; + mediaAttachments: MediaAttachment[]; + distance: number; // km + duration: number; // seconds + currentSpeed: number; // km/h + currentHeartRate: number; // bpm + caloriesBurned: number; // kcal + weightKg: number; // mặc định dùng 70kg để tính MET + + startWorkout: (type: 'RUNNING' | 'CYCLING' | 'WALKING') => void; + stopWorkout: () => Promise; + addTrackpoint: (point: Trackpoint) => void; + addMediaAttachment: (media: MediaAttachment) => void; + updateTimer: () => void; +} + +export const useWorkoutStore = create((set, get) => ({ + isTracking: false, + workoutType: 'RUNNING', + currentWorkoutId: null, + routePoints: [], + mediaAttachments: [], + distance: 0, + duration: 0, + currentSpeed: 0, + currentHeartRate: 75, + caloriesBurned: 0, + weightKg: 70, + + startWorkout: (type) => { + const id = `WK_${Date.now()}`; + set({ + isTracking: true, + workoutType: type, + currentWorkoutId: id, + routePoints: [], + mediaAttachments: [], + distance: 0, + duration: 0, + currentSpeed: 0, + currentHeartRate: 75, + caloriesBurned: 0, + }); + }, + + addTrackpoint: (point) => { + const { routePoints, distance, weightKg, workoutType } = get(); + let newDistance = distance; + + if (routePoints.length > 0) { + const lastPt = routePoints[routePoints.length - 1]; + const d = calculateDistance(lastPt.lat, lastPt.lng, point.lat, point.lng); + newDistance += d; + } + + const newlyBurnedKcal = calculateMetKcal(workoutType, weightKg, point.hr, 10); + + set((state) => ({ + routePoints: [...state.routePoints, point], + distance: newDistance, + currentSpeed: point.speed, + currentHeartRate: point.hr, + caloriesBurned: state.caloriesBurned + newlyBurnedKcal, + })); + }, + + addMediaAttachment: (media) => { + set((state) => ({ + mediaAttachments: [...state.mediaAttachments, media], + })); + }, + + updateTimer: () => { + set((state) => ({ duration: state.duration + 1 })); + }, + + stopWorkout: async () => { + const { currentWorkoutId, workoutType, distance, duration, routePoints, mediaAttachments, caloriesBurned } = get(); + if (!currentWorkoutId) return null; + + set({ isTracking: false }); + + try { + const db = await getDBConnection(); + const avgSpeed = duration > 0 ? (distance / (duration / 3600)) : 0; + const totalHR = routePoints.reduce((acc, p) => acc + p.hr, 0); + const avgHR = routePoints.length > 0 ? Math.round(totalHR / routePoints.length) : 75; + const todayStr = new Date().toISOString().split('T')[0]; + + // 1. Lưu vào bảng Workouts + await db.runAsync( + `INSERT INTO workouts (id, title, type, date, start_time, duration, distance, avg_speed, avg_hr, calories) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [currentWorkoutId, `Hành trình ${workoutType}`, workoutType, todayStr, Date.now(), duration, distance, avgSpeed, avgHR, Math.round(caloriesBurned)] + ); + + // 2. Lưu các Trackpoints + for (const p of routePoints) { + await db.runAsync( + `INSERT INTO trackpoints (workout_id, latitude, longitude, speed, heart_rate, timestamp) VALUES (?, ?, ?, ?, ?, ?)`, + [currentWorkoutId, p.lat, p.lng, p.speed, p.hr, p.timestamp] + ); + } + + // 3. Lưu các Media đính kèm + for (const m of mediaAttachments) { + await db.runAsync( + `INSERT INTO media_attachments (workout_id, latitude, longitude, type, file_uri, caption, timestamp) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [currentWorkoutId, m.lat, m.lng, m.type, m.uri, m.caption, Date.now()] + ); + } + + return currentWorkoutId; + } catch (error) { + console.error("Lỗi khi lưu trữ SQLite:", error); + return null; + } + } +})); + +## 7. THIẾT KẾ CÁC MÀN HÌNH CHÍNH (MOBILE SCREEN TEMPLATES) +### A. Màn hình HUD ghi nhật ký (RecordScreen.tsx) + +Giao diện tối giản (Minimalism) tối ưu không gian hiển thị cho bản đồ và các chỉ số thể thao quan trọng. +Tất cả các tính năng cài đặt, lịch tập, thống kê biểu đồ đều được gom gọn vào nút Hamburger trượt từ bên phải. + +// src/app/(tabs)/index.tsx +import React, { useEffect, useRef, useState } from 'react'; +import { View, Text, TouchableOpacity, Alert, SafeAreaView } from 'react-native'; +import MapView, { Polyline, PROVIDER_GOOGLE } from 'react-native-maps'; +import { useWorkoutStore } from '@/store/useWorkoutStore'; +import * as Location from 'expo-location'; +import * as TaskManager from 'expo-task-manager'; +import { Ionicons } from '@expo/vector-icons'; +import { fetchCurrentWeather } from '@/services/weatherService'; + +const LOCATION_TASK_NAME = 'background-location-task'; + +export default function RecordScreen({ navigation }: any) { + const { + isTracking, + distance, + duration, + currentSpeed, + currentHeartRate, + caloriesBurned, + startWorkout, + stopWorkout, + routePoints + } = useWorkoutStore(); + + const [weather, setWeather] = useState({ temp: '--', icon: 'cloud-outline' }); + const mapRef = useRef(null); + + // 1. Lấy dữ liệu thời tiết thực tế từ Weather Service + useEffect(() => { + const getWeather = async () => { + const { status } = await Location.requestForegroundPermissionsAsync(); + if (status !== 'granted') return; + const loc = await Location.getCurrentPositionAsync({}); + const wData = await fetchCurrentWeather(loc.coords.latitude, loc.coords.longitude); + setWeather(wData); + }; + getWeather(); + }, []); + + // 2. Chạy đồng hồ đếm thời gian tập + useEffect(() => { + let interval: any = null; + if (isTracking) { + interval = setInterval(() => { + useWorkoutStore.getState().updateTimer(); + }, 1000); + } else { + clearInterval(interval); + } + return () => clearInterval(interval); + }, [isTracking]); + + // 3. Khởi tạo tác vụ ghi GPS chạy ngầm + const handleToggleTracking = async () => { + if (!isTracking) { + const { status: foreground } = await Location.requestForegroundPermissionsAsync(); + const { status: background } = await Location.requestBackgroundPermissionsAsync(); + + if (foreground !== 'granted' || background !== 'granted') { + Alert.alert('Quyền Định Vị', 'Bạn cần cấp quyền định vị Always (Luôn cho phép) để ứng dụng chạy ngầm không bị mất log.'); + return; + } + + startWorkout('RUNNING'); + + await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, { + accuracy: Location.Accuracy.BestForNavigation, + timeInterval: 5000, + distanceInterval: 5, + foregroundService: { + notificationTitle: "GeoSport Log đang hoạt động", + notificationBody: "Đang ghi lại lộ trình di chuyển của bạn...", + notificationColor: "#10b981" + } + }); + } else { + await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME); + const savedId = await stopWorkout(); + if (savedId) { + Alert.alert('Thành Công', 'Đã lưu lại hành trình của bạn vào Nhật ký cục bộ.'); + } + } + }; + + const formatTime = (secs: number) => { + const m = Math.floor(secs / 60).toString().padStart(2, '0'); + const s = (secs % 60).toString().padStart(2, '0'); + return `${m}:${s}`; + }; + + return ( + + + {routePoints.length > 0 && ( + ({ latitude: p.lat, longitude: p.lng }))} + strokeColor="#10b981" + strokeWidth={5} + /> + )} + + + {/* HEADER OVERLAY HUD */} + + + + {weather.temp} + + + + + GEOSPORT LOG + + + + navigation.openDrawer()} + className="w-10 h-10 rounded-2xl bg-slate-950/90 border border-slate-800 flex items-center justify-center text-white" + > + + + + + {/* LOWER STATUS AND CONTROL HUD */} + + + + + Cự ly + {distance.toFixed(2)} km + + + Thời gian + {formatTime(duration)} + + + Tốc độ + {currentSpeed.toFixed(1)} km/h + + + + + + + {currentHeartRate} BPM + + + + {Math.round(caloriesBurned)} Kcal + + + + + + + + {isTracking ? 'KẾT THÚC & LƯU CHUYẾN ĐI' : 'BẮT ĐẦU GHI HÀNH TRÌNH'} + + + + + ); +} + +TaskManager.defineTask(LOCATION_TASK_NAME, ({ data, error }: any) => { + if (error) return; + if (data) { + const { locations } = data; + const location = locations[0]; + + const speedKmh = location.coords.speed ? location.coords.speed * 3.6 : 10; + const simulatedHR = Math.round(110 + (speedKmh * 3.5) + (Math.random() * 8 - 4)); + + useWorkoutStore.getState().addTrackpoint({ + lat: location.coords.latitude, + lng: location.coords.longitude, + speed: parseFloat(speedKmh.toFixed(1)), + hr: Math.min(Math.max(simulatedHR, 70), 185), + timestamp: location.timestamp + }); + } +}); + +const darkMapStyle = [ + { "elementType": "geometry", "stylers": [{ "color": "#050811" }] }, + { "elementType": "labels.text.fill", "stylers": [{ "color": "#747985" }] }, + { "elementType": "labels.text.stroke", "stylers": [{ "color": "#050811" }] }, + { "featureType": "road", "elementType": "geometry", "stylers": [{ "color": "#0d1527" }] }, + { "featureType": "water", "elementType": "geometry", "stylers": [{ "color": "#02040a" }] } +]; + +### B. Màn hình Trình phát phim Cinematic (ReplayScreen.tsx) + +Bản vẽ mô phỏng góc nhìn POV di chuyển dọc theo lộ trình. Khi đi qua các tọa độ có gắn thẻ ảnh/video đa phương tiện từ SQLite, Camera sẽ tự động dừng lại, mở popup trình phát nội dung, sau đó tự động tiếp tục cho đến vạch đích. + +#### Nâng cấp mới cấu trúc lớp phủ HUD di động: + +Khi xem Video: Xuất hiện một SVG vẽ lộ trình thu nhỏ (màu đỏ rực rỡ) ở góc trên bên trái, kèm chấm xanh neon biểu diễn định vị GPS di động biến động theo tiến độ của video clip. +Khi xem Ảnh: Xuất hiện lớp phủ HUD bán trong suốt ở đáy ảnh, toàn bộ chữ (Địa danh, Tọa độ GPS, Thời gian chụp) mang màu vàng nắng phát sáng lung linh (textShadowColor: '#facc15') tăng độ hiển thị thẩm mỹ tối đa. + +// src/app/replay/[id].tsx +import React, { useEffect, useState, useRef } from 'react'; +import { View, Text, Image, TouchableOpacity, SafeAreaView, Dimensions, StyleSheet } from 'react-native'; +import { Video, ResizeMode, AVPlaybackStatus } from 'expo-av'; +import MapView, { Marker, Polyline } from 'react-native-maps'; +import Svg, { Polyline as SvgPolyline, Circle as SvgCircle } from 'react-native-svg'; +import { Trackpoint, MediaAttachment } from '@/types/workout.types'; +import { useWorkoutStore } from '@/store/useWorkoutStore'; +import { Ionicons } from '@expo/vector-icons'; +import { useLocalSearchParams } from 'expo-router'; + +export default function ReplayScreen() { + const { id } = useLocalSearchParams(); + const [points, setPoints] = useState([]); + const [media, setMedia] = useState([]); + const [currentIdx, setCurrentIdx] = useState(0); + const [isPlaying, setIsPlaying] = useState(false); + const [activeMedia, setActiveMedia] = useState(null); + + // Video playback progress (0 to 1) to animate the mini tracklog indicator + const [videoProgress, setVideoProgress] = useState(0); + const [mediaCountdown, setMediaCountdown] = useState(6); + + const mapRef = useRef(null); + const videoRef = useRef