Skip to content

Commit

Permalink
QR code import
Browse files Browse the repository at this point in the history
  • Loading branch information
belinde committed Apr 22, 2024
1 parent 050c709 commit c96c011
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 3 deletions.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"react-native-safe-area-context": "4.8.2",
"react-native-screens": "~3.29.0",
"react-native-svg": "14.1.0",
"react-qr-code": "^2.0.12"
"react-qr-code": "^2.0.12",
"expo-camera": "~14.1.3"
},
"devDependencies": {
"@babel/core": "^7.23.7",
Expand Down
3 changes: 2 additions & 1 deletion src/pages/Library/LibraryStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useNativeStackNavigatorOptions } from "../../hooks/useNativeStackNaviga
import { CreateSong } from "./CreateSong";
import { EditSong } from "./EditSong";
import { ListSongs } from "./ListSongs";
import { ListSongsMenu } from "./ListSongsMenu";
import { QRSong } from "./QRSong";
import { ViewSong } from "./ViewSong";
import { ViewSongMenu } from "./ViewSongMenu";
Expand Down Expand Up @@ -31,7 +32,7 @@ export const LibraryStack: FC = () => {
<Stack.Screen
name="List"
component={ListSongs}
options={{ title: "Repertorio" }}
options={{ title: "Repertorio", headerRight: ListSongsMenu }}
/>
<Stack.Screen
name="Create"
Expand Down
63 changes: 63 additions & 0 deletions src/pages/Library/ListSongsMenu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useNavigation } from "@react-navigation/native";
import {
BarcodeScanningResult,
CameraView,
useCameraPermissions,
} from "expo-camera/next";
import { FC, useCallback, useRef, useState } from "react";
import { StyleSheet, useWindowDimensions } from "react-native";
import { Dialog, IconButton, Portal } from "react-native-paper";
import { useDataContext } from "../../hooks/useDataContext";
import { unserializeSong } from "../../serialization";

export const ListSongsMenu: FC = () => {
const { width } = useWindowDimensions();
const [visible, setVisible] = useState(false);
const [status, requestPermissions] = useCameraPermissions();
const { songs } = useDataContext();
const scanned = useRef("");
const navigation = useNavigation();

const showScanner = useCallback(async () => {
if (!status) return;
let current = status;
while (!current.granted) {
current = await requestPermissions();
}
setVisible(true);
}, [requestPermissions, status]);

const qrcodeScanned = useCallback(
(res: BarcodeScanningResult) => {
const maybeSong = unserializeSong(res.data);
if (maybeSong && scanned.current !== maybeSong.id) {
scanned.current = maybeSong.id;
songs.add(maybeSong);
setVisible(false);
navigation.navigate("Library", {
screen: "View",
params: { song: maybeSong.id },
});
}
},
[navigation, songs]
);

return (
<>
<IconButton icon="qrcode-plus" onPress={showScanner} />
<Portal>
<Dialog visible={visible} onDismiss={() => setVisible(false)}>
<Dialog.Title>Scansiona QR</Dialog.Title>
<Dialog.Content style={{ height: width }}>
<CameraView
barcodeScannerSettings={{ barcodeTypes: ["qr"] }}
onBarcodeScanned={qrcodeScanned}
style={StyleSheet.absoluteFillObject}
/>
</Dialog.Content>
</Dialog>
</Portal>
</>
);
};
53 changes: 52 additions & 1 deletion src/serialization.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alteration, NoteName, Section, Song } from "./types";
import { Alteration, InitialNote, NoteName, Section, Song } from "./types";

const COMPRESSED_SECTIONS: Record<Section, number> = {
soprani: 0,
Expand All @@ -25,6 +25,26 @@ type CompressedSong = [
CompressedInitialNote[],
];

export const isCompressedSong = (data: unknown): data is CompressedSong =>
Array.isArray(data) &&
data.length === 5 &&
typeof data[0] === "string" &&
typeof data[1] === "string" &&
typeof data[2] === "string" &&
typeof data[3] === "string" &&
Array.isArray(data[4]) &&
data[4].every(
(note) =>
Array.isArray(note) &&
note.length === 5 &&
typeof note[0] === "number" &&
typeof note[1] === "number" &&
typeof note[2] === "string" &&
note[2].length === 1 &&
(note[3] === "" || note[3] === "#" || note[3] === "b") &&
typeof note[4] === "number"
);

export const serializeSong = (song: Song): string => {
const compressed: CompressedSong = [
song.id,
Expand All @@ -41,3 +61,34 @@ export const serializeSong = (song: Song): string => {
];
return JSON.stringify(compressed);
};

export const unserializeSong = (data: string): Song | null => {
const compressed = JSON.parse(data);
if (!isCompressedSong(compressed)) return null;
const song: Song = {
id: compressed[0],
title: compressed[1],
artist: compressed[2],
annotations: compressed[3],
initialNotes: {},
};

compressed[4].forEach((note) => {
const section = Object.keys(COMPRESSED_SECTIONS).find(
(section) => COMPRESSED_SECTIONS[section as Section] === note[0]
) as Section;
const subsection = note[1];
const initialNote: InitialNote = {
section,
subsection,
note: {
note: note[2],
alteration: note[3] || undefined,
octave: note[4],
},
};
song.initialNotes[`${section}${subsection}`] = initialNote;
});

return song;
};

0 comments on commit c96c011

Please sign in to comment.