feat: use Capacitor Camera API to select/capture photos to preserve EXIF location on Android app

This commit is contained in:
2026-06-27 22:21:28 +07:00
parent 964a72514f
commit 0152e7014d
3 changed files with 61 additions and 18 deletions
Binary file not shown.
+4 -4
View File
@@ -21,13 +21,13 @@
<meta name="twitter:title" content="Travel Planner - Lên kế hoạch chuyến đi của bạn" />
<meta name="twitter:description" content="Khám phá những hành trình tuyệt vời và chia sẻ khoảnh khắc đẹp với cộng đồng du lịch." />
<meta name="twitter:image" content="https://yotrip.labz.io.vn/background.avif" />
<script type="module" crossorigin src="/assets/index-CRcJJCpn.js"></script>
<script type="module" crossorigin src="/assets/index-C0KfLnj9.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CMxvf4Kt.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-others-C077EAcu.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-others-CNhtyHGs.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-editor-BDwQQzB8.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-maps-1-B38H26.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-react-DRCYLnn6.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-TQOwx6ff.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-react-BF5_05kG.js">
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-C3XQY6t9.js">
<link rel="stylesheet" crossorigin href="/assets/vendor-maps-CcXJxNtP.css">
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
<link rel="stylesheet" crossorigin href="/assets/index-w726aohe.css">
+57 -14
View File
@@ -1,5 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { Compass, Map as MapIcon, Camera, ShieldAlert, Image as ImageIcon, X } from 'lucide-react';
import { Compass, Map as MapIcon, Camera as CameraIcon, ShieldAlert, Image as ImageIcon, X } from 'lucide-react';
import { LoginModal } from '../components/LoginModal';
import { ReportBusinessModal } from '../components/ReportBusinessModal';
import { TagSelectModal } from '../components/TagSelectModal';
@@ -15,6 +15,8 @@ import { processImageModeration } from '../hooks/useImageModeration';
import { useTranslation } from '../hooks/useTranslation';
import { processAndResizeImage } from '../utils/imageProcessor';
import { getDeviceLocation } from '../utils/geolocation';
import { Capacitor } from '@capacitor/core';
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
interface LandingPageProps {
onContinue?: () => void;
@@ -188,12 +190,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({
return () => clearInterval(interval);
}, [publicPhotos]);
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
notify({ title: 'Đang xử lý...', message: 'Vui lòng chờ trong giây lát.', type: 'info' });
const processAndUploadFile = async (file: File) => {
try {
// Process image: Reads EXIF data and forces 2K resizing on device memory
const { file: processedFile, latitude: exifLat, longitude: exifLng } = await processAndResizeImage(file);
@@ -262,12 +259,50 @@ export const LandingPage: React.FC<LandingPageProps> = ({
setIsTagsModalOpen(true);
} catch (error: any) {
notify({ title: 'Lỗi', message: error.message, type: 'error' });
} finally {
// Reset input để có thể chọn lại cùng 1 file
if (event.target) event.target.value = '';
}
};
const handleNativePhotoPick = async (source: CameraSource) => {
try {
const image = await Camera.getPhoto({
quality: 90,
allowEditing: false,
resultType: CameraResultType.Uri,
source: source,
saveToGallery: source === CameraSource.Camera // Tự động lưu ảnh gốc vào thư viện nếu chụp bằng Camera
});
if (image && image.webPath) {
notify({ title: 'Đang xử lý...', message: 'Vui lòng chờ trong giây lát.', type: 'info' });
// Convert Capacitor webPath resource back to standard File instance
const response = await fetch(image.webPath);
const blob = await response.blob();
const originalName = `photo-${Date.now()}.${image.format}`;
const file = new File([blob], originalName, { type: `image/${image.format}` });
// Process this file using our standard handler
await processAndUploadFile(file);
}
} catch (error: any) {
console.error('Lỗi chọn ảnh native:', error);
if (error?.message !== 'User cancelled photos app' && error?.message !== 'User cancelled camera') {
notify({
title: 'Lỗi',
message: 'Không thể truy cập máy ảnh hoặc thư viện ảnh.',
type: 'error'
});
}
}
};
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
await processAndUploadFile(file);
if (event.target) event.target.value = '';
};
const handleConfirmTags = async (selectedTags: string[]) => {
if (!pendingPhotoFile) return;
@@ -675,7 +710,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({
onClick={() => setIsPhotoSourceModalOpen(true)}
className="flex-1 flex items-center justify-center gap-2 bg-white/20 backdrop-blur-lg hover:bg-white/30 text-white font-bold py-3.5 rounded-2xl transition-all shadow-2xl border border-white/30 active:scale-95 text-xs sm:text-sm whitespace-nowrap"
>
<Camera className="w-4.5 h-4.5" />
<CameraIcon className="w-4.5 h-4.5" />
<span>{t('shortCamera') || 'Chụp ảnh'}</span>
</button>
</div>
@@ -748,12 +783,16 @@ export const LandingPage: React.FC<LandingPageProps> = ({
<button
onClick={() => {
setIsPhotoSourceModalOpen(false);
cameraInputRef.current?.click();
if (Capacitor.isNativePlatform()) {
handleNativePhotoPick(CameraSource.Camera);
} else {
cameraInputRef.current?.click();
}
}}
className="w-full flex items-center gap-4 p-4 bg-[var(--background)] hover:bg-[var(--background-alt)] border border-[var(--border)] rounded-2xl transition-all active:scale-95"
>
<div className="flex-shrink-0 p-3 bg-blue-100 rounded-full">
<Camera className="w-6 h-6 text-blue-600" />
<CameraIcon className="w-6 h-6 text-blue-600" />
</div>
<div className="flex-1 text-left">
<div className="font-bold text-[var(--text-primary)]">Chụp nh bằng camera</div>
@@ -765,7 +804,11 @@ export const LandingPage: React.FC<LandingPageProps> = ({
<button
onClick={() => {
setIsPhotoSourceModalOpen(false);
galleryInputRef.current?.click();
if (Capacitor.isNativePlatform()) {
handleNativePhotoPick(CameraSource.Photos);
} else {
galleryInputRef.current?.click();
}
}}
className="w-full flex items-center gap-4 p-4 bg-[var(--background)] hover:bg-[var(--background-alt)] border border-[var(--border)] rounded-2xl transition-all active:scale-95"
>