feat: use Capacitor Camera API to select/capture photos to preserve EXIF location on Android app
This commit is contained in:
Binary file not shown.
Vendored
+4
-4
@@ -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: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: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" />
|
<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/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-editor-BDwQQzB8.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-maps-1-B38H26.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-react-BF5_05kG.js">
|
||||||
<link rel="modulepreload" crossorigin href="/assets/vendor-pdf-TQOwx6ff.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-maps-CcXJxNtP.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
|
<link rel="stylesheet" crossorigin href="/assets/vendor-react-CuSj0z1j.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-w726aohe.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-w726aohe.css">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
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 { LoginModal } from '../components/LoginModal';
|
||||||
import { ReportBusinessModal } from '../components/ReportBusinessModal';
|
import { ReportBusinessModal } from '../components/ReportBusinessModal';
|
||||||
import { TagSelectModal } from '../components/TagSelectModal';
|
import { TagSelectModal } from '../components/TagSelectModal';
|
||||||
@@ -15,6 +15,8 @@ import { processImageModeration } from '../hooks/useImageModeration';
|
|||||||
import { useTranslation } from '../hooks/useTranslation';
|
import { useTranslation } from '../hooks/useTranslation';
|
||||||
import { processAndResizeImage } from '../utils/imageProcessor';
|
import { processAndResizeImage } from '../utils/imageProcessor';
|
||||||
import { getDeviceLocation } from '../utils/geolocation';
|
import { getDeviceLocation } from '../utils/geolocation';
|
||||||
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera';
|
||||||
|
|
||||||
interface LandingPageProps {
|
interface LandingPageProps {
|
||||||
onContinue?: () => void;
|
onContinue?: () => void;
|
||||||
@@ -188,12 +190,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [publicPhotos]);
|
}, [publicPhotos]);
|
||||||
|
|
||||||
const handleFileChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const processAndUploadFile = async (file: File) => {
|
||||||
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' });
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Process image: Reads EXIF data and forces 2K resizing on device memory
|
// Process image: Reads EXIF data and forces 2K resizing on device memory
|
||||||
const { file: processedFile, latitude: exifLat, longitude: exifLng } = await processAndResizeImage(file);
|
const { file: processedFile, latitude: exifLat, longitude: exifLng } = await processAndResizeImage(file);
|
||||||
@@ -262,12 +259,50 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
setIsTagsModalOpen(true);
|
setIsTagsModalOpen(true);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify({ title: 'Lỗi', message: error.message, type: 'error' });
|
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[]) => {
|
const handleConfirmTags = async (selectedTags: string[]) => {
|
||||||
if (!pendingPhotoFile) return;
|
if (!pendingPhotoFile) return;
|
||||||
|
|
||||||
@@ -675,7 +710,7 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
onClick={() => setIsPhotoSourceModalOpen(true)}
|
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"
|
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>
|
<span>{t('shortCamera') || 'Chụp ảnh'}</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -748,12 +783,16 @@ export const LandingPage: React.FC<LandingPageProps> = ({
|
|||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPhotoSourceModalOpen(false);
|
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"
|
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">
|
<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>
|
||||||
<div className="flex-1 text-left">
|
<div className="flex-1 text-left">
|
||||||
<div className="font-bold text-[var(--text-primary)]">Chụp ảnh bằng camera</div>
|
<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
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsPhotoSourceModalOpen(false);
|
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"
|
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"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user