Skip to content

Commit

Permalink
Merge pull request #1834 from coopcycle/feature/barcode
Browse files Browse the repository at this point in the history
Barcode scanner
  • Loading branch information
r0xsh authored Dec 26, 2024
2 parents 6bdd829 + bcc9096 commit 60cc013
Show file tree
Hide file tree
Showing 18 changed files with 1,011 additions and 38 deletions.
144 changes: 144 additions & 0 deletions src/components/BarcodeCameraView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import React, { useState, forwardRef } from 'react';
import { Dimensions, Text } from 'react-native';
import { View, Vibration } from 'react-native';
import { CameraView, useCameraPermissions } from 'expo-camera/next';
import _ from 'lodash';
import { Icon } from 'native-base';
import Ionicons from 'react-native-vector-icons/Ionicons';
import { TouchableOpacity } from 'react-native-gesture-handler';
import FocusHandler from './FocusHandler';

function getBoxBoundaries(points) {
const xs = points.map(point => point.x);
const ys = points.map(point => point.y);
const minX = Math.min(...xs);
const maxX = Math.max(...xs);
const minY = Math.min(...ys);
const maxY = Math.max(...ys);
return { minX, maxX, minY, maxY };
}

function horizontalLineIntersectsBox(lineY, lineStartX, lineEndX, points) {
const { minX, maxX, minY, maxY } = getBoxBoundaries(points);

if (lineY < minY || lineY > maxY) {
return false;
}
if (lineStartX > lineEndX) {
[lineStartX, lineEndX] = [lineEndX, lineStartX];
}
return lineEndX >= minX && lineStartX <= maxX;
}

function BarcodeCameraView({ disabled, ...props }, ref) {
const [hasPermission, requestPermission] = useCameraPermissions();

const { width: CameraWidth } = Dimensions.get('window');
const CameraHeight = CameraWidth * 0.55;
const PaddingHorizontal = CameraWidth * 0.1618;

const [barcode, setBarcode] = useState(null);
const [flash, setFlash] = useState(false);

//The hide behavior is a hack to force the camera to re-render
//if another camera is opened on modal. If we don't do this, the
//camera will not render.
const [hide, setHide] = useState(disabled ?? false);

if (!hasPermission) {
return (
<View>
<Text>No camera permission</Text>
</View>
);
}

if (!hasPermission.granted) {
requestPermission();
return <View />;
}

const onScanned = _.throttle(result => {
const { data, cornerPoints } = result;
if (
!horizontalLineIntersectsBox(
CameraHeight / 2,
PaddingHorizontal,
CameraWidth - PaddingHorizontal,
cornerPoints,
)
) {
return;
}

if (data !== barcode) {
Vibration.vibrate();
setBarcode(data);
props.onScanned(data);
}
}, 200);

return (
<>
<FocusHandler
onFocus={() => {
if (!disabled) {
setHide(false);
}
}}
onBlur={() => {
setHide(true);
}}
/>
{!hide && (
<CameraView
ref={ref}
enableTorch={flash}
style={{
height: CameraHeight,
width: CameraWidth,
position: 'relative',
}}
onBarcodeScanned={onScanned}
barcodeScannerSettings={{
barcodeTypes: ['code128'],
}}>
<View
style={{
position: 'absolute',
borderColor: 'rgba(255, 0, 0, 0.6)',
borderBottomWidth: 1,
top: CameraHeight / 2,
left: PaddingHorizontal,
right: PaddingHorizontal,
}}
/>
<View
style={{
position: 'absolute',
top: 10,
right: 10,
}}>
<TouchableOpacity onPress={() => setFlash(!flash)}>
<Icon
as={Ionicons}
name={flash ? 'flash-sharp' : 'flash-off-sharp'}
style={{ color: 'rgba(255, 255, 255, 0.6)' }}
/>
</TouchableOpacity>
</View>
</CameraView>
)}
{hide && (
<View
style={{
height: CameraHeight,
width: CameraWidth,
backgroundColor: 'black',
}}></View>
)}
</>
);
}

export default forwardRef(BarcodeCameraView);
14 changes: 12 additions & 2 deletions src/components/NotificationHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { useDispatch, useSelector } from 'react-redux';

import {
clearNotifications,
shouldNotificationBeDisplayed,
startSound,
stopSound,
} from '../redux/App/actions';
import NotificationModal from './NotificationModal';
import {
selectNotificationsToDisplay,
selectNotificationsWithSound,
selectShouldNotificationBeDisplayed
} from '../redux/App/selectors';
import { AppState } from 'react-native';

Expand All @@ -25,6 +27,7 @@ const NOTIFICATION_DURATION_MS = 10000;
export default function NotificationHandler() {
const notificationsToDisplay = useSelector(selectNotificationsToDisplay);
const notificationsWithSound = useSelector(selectNotificationsWithSound);
const shouldNotificationBeDisplayed = useSelector(selectShouldNotificationBeDisplayed);

const hasNotifications = useMemo(
() =>
Expand Down Expand Up @@ -53,12 +56,19 @@ export default function NotificationHandler() {
// but it's very limited, e.g. handlers set via setTimeout are not executed
// so we do not play sound in that case, because we will not be able to stop it
// use memoized value to avoid re-starting the sound when more notifications arrive
if (hasNotificationsWithSound && AppState.currentState === 'active') {
if (
shouldNotificationBeDisplayed &&
hasNotificationsWithSound &&
AppState.currentState === 'active') {
dispatch(startSound());
} else {
dispatch(stopSound());
}
}, [hasNotificationsWithSound, dispatch]);
}, [shouldNotificationBeDisplayed, hasNotificationsWithSound, dispatch]);

if (!shouldNotificationBeDisplayed) {
return null;
}

return (
<NotificationModal
Expand Down
21 changes: 20 additions & 1 deletion src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,25 @@
"CART_TIME_RANGE_CHANGED_MODAL_MESSAGE": "Die Öffnungszeiten erlauben es nicht, die Bestellung heute aufzugeben",
"CART_TIME_RANGE_CHANGED_MODAL_CHOOSE_TIME_RANGE_TEXT": "Bitte wählen Sie einen anderen Zeitraum",
"CART_TIME_RANGE_CHANGED_MODAL_SELECT_TIME_RANGE_ACTION": "Lieferzeit ändern",
"CART_TIME_RANGE_CHANGED_MODAL_CHOOSE_RESTAURANT_ACTION": "Wählen Sie ein anderes Restaurant"
"CART_TIME_RANGE_CHANGED_MODAL_CHOOSE_RESTAURANT_ACTION": "Wählen Sie ein anderes Restaurant",
"BARCODE_TASK_ALREADY_ASSIGNED_TITLE": "Aufgabe bereits zugewiesen",
"BARCODE_TASK_ALREADY_ASSIGNED_ANOTHER_MESSAGE": "Diese Aufgabe wurde bereits einem anderen Benutzer zugewiesen",
"BARCODE_TASK_ALREADY_ASSIGNED_SELF_MESSAGE": "Diese Aufgabe wurde dir bereits zugewiesen",
"BARCODE_TASK_ALREADY_ASSIGNED_UNASSIGN": "Zuweisung aufheben",
"BARCODE_TASK_ALREADY_ASSIGNED_ASSIGN_TO_ME": "Mir zuweisen",
"TASK_MULTIPLE_PACKAGES": "Mehrere Pakete",
"X_PACKAGES": "{{count}} Pakete",
"NO_NEED_TO_SCAN_OTHERS": "Es ist nicht nötig, die anderen Pakete zu scannen.",
"ADD_NOTE": "Notiz hinzufügen",
"REPORT_AN_INCIDENT": "Einen Vorfall melden",
"UPDATE_PARCEL_DETAILS": "Paketdetails aktualisieren",
"PICTURES": "Fotos",
"TASK_TODO": "Zu erledigen",
"TASK_DOING": "In Bearbeitung",
"TASK_FAILED": "Fehlgeschlagen",
"TASK_DONE": "Erledigt",
"TASK_CANCELLED": "Abgebrochen",
"ASK_TO_START_PICKUP_TITLE": "Abholaufgabe",
"ASK_TO_START_PICKUP_MESSAGE": "Die Abholaufgabe wurde noch nicht gestartet."
}
}
23 changes: 21 additions & 2 deletions src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,25 @@
"ORDER__SHIPPING_TIME_RANGE__NOT_AVAILABLE": "Not available at the moment",
"LOOPEAT_HAS_RETURNS": "This task has zero waste returns",
"EDENRED_ELIGIBLE_AMOUNT": "Ticket Restaurant® eligible amount",
"EDENRED_COMPLEMENT": "Complement"
}
"EDENRED_COMPLEMENT": "Complement",
"BARCODE_TASK_ALREADY_ASSIGNED_TITLE": "Task already assigned",
"BARCODE_TASK_ALREADY_ASSIGNED_ANOTHER_MESSAGE": "This task has already been assigned to another user",
"BARCODE_TASK_ALREADY_ASSIGNED_SELF_MESSAGE": "This task has already been assigned to you",
"BARCODE_TASK_ALREADY_ASSIGNED_UNASSIGN": "Unassign",
"BARCODE_TASK_ALREADY_ASSIGNED_ASSIGN_TO_ME": "Assign to me",
"TASK_MULTIPLE_PACKAGES": "Multiple packages",
"X_PACKAGES": "{{count}} packages",
"NO_NEED_TO_SCAN_OTHERS": "No need to scan the others packages.",
"ADD_NOTE": "Add Note",
"REPORT_AN_INCIDENT": "Report an incident",
"UPDATE_PARCEL_DETAILS": "Update parcel details",
"PICTURES": "Photos",
"TASK_TODO": "To do",
"TASK_DOING": "In progress",
"TASK_FAILED": "Failed",
"TASK_DONE": "Done",
"TASK_CANCELLED": "Cancelled",
"ASK_TO_START_PICKUP_TITLE": "Pickup task",
"ASK_TO_START_PICKUP_MESSAGE": "The pickup task is not yet started."
}
}
21 changes: 20 additions & 1 deletion src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,25 @@
"CART_TIME_RANGE_CHANGED_MODAL_CHOOSE_RESTAURANT_ACTION": "Elige otro restaurante",
"ORDER__SHIPPED_AT__NOT_AVAILABLE": "El horario de entrega ya no está disponible",
"ORDER__SHIPPED_AT__EXPIRED": "El horario de entrega ya ha pasado",
"ORDER__SHIPPING_TIME_RANGE__NOT_AVAILABLE": "No esta disponible en estos momentos"
"ORDER__SHIPPING_TIME_RANGE__NOT_AVAILABLE": "No esta disponible en estos momentos",
"BARCODE_TASK_ALREADY_ASSIGNED_TITLE": "Tarea ya asignada",
"BARCODE_TASK_ALREADY_ASSIGNED_ANOTHER_MESSAGE": "Esta tarea ya ha sido asignada a otro usuario",
"BARCODE_TASK_ALREADY_ASSIGNED_SELF_MESSAGE": "Esta tarea ya te ha sido asignada",
"BARCODE_TASK_ALREADY_ASSIGNED_UNASSIGN": "Desasignar",
"BARCODE_TASK_ALREADY_ASSIGNED_ASSIGN_TO_ME": "Asignármela",
"TASK_MULTIPLE_PACKAGES": "Varios paquetes",
"X_PACKAGES": "{{count}} paquetes",
"NO_NEED_TO_SCAN_OTHERS": "No es necesario escanear los demás paquetes.",
"ADD_NOTE": "Agregar nota",
"REPORT_AN_INCIDENT": "Reportar un incidente",
"UPDATE_PARCEL_DETAILS": "Actualizar detalles del paquete",
"PICTURES": "Fotos",
"TASK_TODO": "Por hacer",
"TASK_DOING": "En progreso",
"TASK_FAILED": "Fallido",
"TASK_DONE": "Hecho",
"TASK_CANCELLED": "Cancelado",
"ASK_TO_START_PICKUP_TITLE": "Tarea de recogida",
"ASK_TO_START_PICKUP_MESSAGE": "La tarea de recogida aún no ha comenzado."
}
}
21 changes: 20 additions & 1 deletion src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,25 @@
"ORDER__SHIPPED_AT__EXPIRED": "L'horaire de livraison est dépassé",
"ORDER__SHIPPING_TIME_RANGE__NOT_AVAILABLE": "Indisponible pour l'instant",
"EDENRED_ELIGIBLE_AMOUNT": "Montant éligible Ticket Restaurant®",
"EDENRED_COMPLEMENT": "Complément"
"EDENRED_COMPLEMENT": "Complément",
"BARCODE_TASK_ALREADY_ASSIGNED_TITLE": "Tâche déjà assignée",
"BARCODE_TASK_ALREADY_ASSIGNED_ANOTHER_MESSAGE": "Cette tâche a déjà été assignée à un autre utilisateur",
"BARCODE_TASK_ALREADY_ASSIGNED_SELF_MESSAGE": "Cette tâche vous a déjà été assignée",
"BARCODE_TASK_ALREADY_ASSIGNED_UNASSIGN": "Désassigner",
"BARCODE_TASK_ALREADY_ASSIGNED_ASSIGN_TO_ME": "M'assigner",
"TASK_MULTIPLE_PACKAGES": "Plusieurs colis",
"X_PACKAGES": "{{count}} colis",
"NO_NEED_TO_SCAN_OTHERS": "Pas besoin de scanner les autres colis.",
"ADD_NOTE": "Ajouter une note",
"REPORT_AN_INCIDENT": "Signaler un incident",
"UPDATE_PARCEL_DETAILS": "Mettre à jour les détails du colis",
"PICTURES": "Photos",
"TASK_TODO": "A faire",
"TASK_DOING": "En cours",
"TASK_FAILED": "Echouée",
"TASK_DONE": "Fait",
"TASK_CANCELLED": "Annulée",
"ASK_TO_START_PICKUP_TITLE": "Tâche de ramassage",
"ASK_TO_START_PICKUP_MESSAGE": "La tâche de ramassage n'est pas encore commencée."
}
}
21 changes: 20 additions & 1 deletion src/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,25 @@
"MULTIPLE_SERVERS_IN_SAME_CITY_MODAL_TEXT": "C'è più di una cooperativa nella tua città, puoi navigare tra esse usando i tab o facendo swipe a sinistra o destra.",
"DO_NOT_SHOW_IT_AGAIN": "Non mostrare più",
"total_packages": "Totale: {{count}} pacchetto",
"total_packages_plural": "Totale: {{count}} pacchetti"
"total_packages_plural": "Totale: {{count}} pacchetti",
"BARCODE_TASK_ALREADY_ASSIGNED_TITLE": "Attività già assegnata",
"BARCODE_TASK_ALREADY_ASSIGNED_ANOTHER_MESSAGE": "Questa attività è già stata assegnata a un altro utente",
"BARCODE_TASK_ALREADY_ASSIGNED_SELF_MESSAGE": "Questa attività è già stata assegnata a te",
"BARCODE_TASK_ALREADY_ASSIGNED_UNASSIGN": "Rimuovi assegnazione",
"BARCODE_TASK_ALREADY_ASSIGNED_ASSIGN_TO_ME": "Assegna a me",
"TASK_MULTIPLE_PACKAGES": "Più pacchi",
"X_PACKAGES": "{{count}} pacchi",
"NO_NEED_TO_SCAN_OTHERS": "Non è necessario scansionare gli altri pacchi.",
"ADD_NOTE": "Aggiungi nota",
"REPORT_AN_INCIDENT": "Segnala un incidente",
"UPDATE_PARCEL_DETAILS": "Aggiorna i dettagli del pacco",
"PICTURES": "Foto",
"TASK_TODO": "Da fare",
"TASK_DOING": "In corso",
"TASK_FAILED": "Fallito",
"TASK_DONE": "Fatto",
"TASK_CANCELLED": "Annullato",
"ASK_TO_START_PICKUP_TITLE": "Attività di ritiro",
"ASK_TO_START_PICKUP_MESSAGE": "L'attività di ritiro non è ancora iniziata."
}
}
Loading

0 comments on commit 60cc013

Please sign in to comment.