mirror of
https://github.com/Boof2015/astra-mobile.git
synced 2026-08-18 19:54:26 +02:00
add signal online searching
This commit is contained in:
+14
-2
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Pressable, StyleSheet, View } from 'react-native';
|
||||
import { Linking, Pressable, StyleSheet, View } from 'react-native';
|
||||
import { CameraView, useCameraPermissions } from 'expo-camera';
|
||||
import * as DocumentPicker from 'expo-document-picker';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { decodeSignalFromUri } from '@/audio/signalDecodeImage';
|
||||
import { matchSignalToLibrary } from '@/audio/signalLocalMatch';
|
||||
import { SIGNAL_SCAN_GUIDE } from '@/audio/signalScanGeometry';
|
||||
import { encodeSignalWebUrl } from '@/audio/signalShare';
|
||||
import { enqueueEnd, playTracks } from '@/audio/playbackController';
|
||||
import { dbTrackToTrack } from '@/library/trackAdapter';
|
||||
import { playHaptic } from '@/lib/haptics';
|
||||
@@ -26,7 +27,7 @@ import { createThemedStyles, useColors } from '@/theme/themed';
|
||||
import { useRipple } from '@/theme/ripple';
|
||||
import type { SignalPayload } from '@boof2015/astra-signal';
|
||||
|
||||
const MIN_READING_MS = 600;
|
||||
const MIN_READING_MS = 300;
|
||||
const FAILURE_RETURN_MS = 480;
|
||||
|
||||
export default function SignalScanScreen() {
|
||||
@@ -151,6 +152,16 @@ export default function SignalScanScreen() {
|
||||
}
|
||||
};
|
||||
|
||||
const findOnline = async (payload: SignalPayload) => {
|
||||
playHaptic('action');
|
||||
setActionError(null);
|
||||
try {
|
||||
await Linking.openURL(encodeSignalWebUrl(payload));
|
||||
} catch {
|
||||
setActionError("Couldn't open the online Signal lookup.");
|
||||
}
|
||||
};
|
||||
|
||||
const standaloneResult = result !== null && !permission?.granted;
|
||||
const resultContent = result ? (
|
||||
<SignalResolutionPanel
|
||||
@@ -160,6 +171,7 @@ export default function SignalScanScreen() {
|
||||
actionError={actionError}
|
||||
onPlay={(track) => void playMatchedTrack(track)}
|
||||
onQueue={(track) => void queueMatchedTrack(track)}
|
||||
onFindOnline={() => void findOnline(result)}
|
||||
onScanAnother={scanAnother}
|
||||
onDone={() => router.back()}
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EncodingType, readAsStringAsync } from 'expo-file-system/legacy';
|
||||
import { File } from 'expo-file-system';
|
||||
import { AlphaType, ColorType, Skia, rect, type SkImage } from '@shopify/react-native-skia';
|
||||
import {
|
||||
decodeSignalImage,
|
||||
@@ -66,8 +66,8 @@ export async function decodeSignalFromUri(
|
||||
uri: string,
|
||||
options: DecodeSignalFromUriOptions = {}
|
||||
): Promise<SignalPayload> {
|
||||
const b64 = await readAsStringAsync(uri, { encoding: EncodingType.Base64 });
|
||||
const encoded = Skia.Data.fromBase64(b64);
|
||||
const bytes = await new File(uri).bytes();
|
||||
const encoded = Skia.Data.fromBytes(bytes);
|
||||
const source = Skia.Image.MakeImageFromEncoded(encoded);
|
||||
encoded.dispose();
|
||||
if (!source) throw new Error('Could not read that image.');
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { Track } from '../types/audio.ts';
|
||||
import {
|
||||
SIGNAL_LINK_PREFIX,
|
||||
decodeTrackSignalLink,
|
||||
encodeSignalWebUrl,
|
||||
encodeTrackSignalLink,
|
||||
signalInputFromTrack,
|
||||
signalLayoutFromTrack,
|
||||
@@ -62,6 +63,19 @@ test('preserves ASCII case and punctuation in track metadata', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('builds a web resolver URL with the complete Signal in the fragment', () => {
|
||||
const url = encodeSignalWebUrl({ artist: 'ナナツカゼ', title: 'Replay!', durationSec: 201 });
|
||||
assert.match(url, /^https:\/\/astramusic\.dev\/signal\/#astra:signal:v3:/);
|
||||
const signalLink = new URL(url).hash.slice(1);
|
||||
assert.deepEqual(decodeTrackSignalLink(signalLink), {
|
||||
version: 3,
|
||||
type: 'metadata',
|
||||
artist: 'ナナツカゼ',
|
||||
title: 'Replay!',
|
||||
durationSec: 201,
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects non-v3, malformed, and CRC-corrupted links', () => {
|
||||
assert.throws(() => decodeTrackSignalLink('astra:signal:v2:AAAA'), /v3 link/);
|
||||
assert.throws(() => decodeTrackSignalLink(`${SIGNAL_LINK_PREFIX}%`));
|
||||
|
||||
@@ -11,6 +11,8 @@ import type { Track } from '../types/audio';
|
||||
|
||||
export { SIGNAL_LINK_PREFIX };
|
||||
|
||||
export const SIGNAL_WEB_URL = 'https://astramusic.dev/signal/';
|
||||
|
||||
/** Convert local track metadata into the database-free v3 Signal input. */
|
||||
export function signalInputFromTrack(track: Track): SignalInput {
|
||||
return {
|
||||
@@ -30,6 +32,11 @@ export function encodeTrackSignalLink(track: Track): string {
|
||||
return encodeLibrarySignalLink(signalInputFromTrack(track));
|
||||
}
|
||||
|
||||
/** Build the privacy-preserving web resolver URL. The Signal stays in the fragment. */
|
||||
export function encodeSignalWebUrl(input: SignalInput): string {
|
||||
return `${SIGNAL_WEB_URL}#${encodeLibrarySignalLink(input)}`;
|
||||
}
|
||||
|
||||
export function decodeTrackSignalLink(value: string): SignalPayload {
|
||||
return decodeLibrarySignalLink(value);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ interface SignalResolutionPanelProps {
|
||||
actionError: string | null;
|
||||
onPlay: (track: DbTrack) => void;
|
||||
onQueue: (track: DbTrack) => void;
|
||||
onFindOnline: () => void;
|
||||
onScanAnother: () => void;
|
||||
onDone: () => void;
|
||||
}
|
||||
@@ -30,6 +31,7 @@ export function SignalResolutionPanel({
|
||||
actionError,
|
||||
onPlay,
|
||||
onQueue,
|
||||
onFindOnline,
|
||||
onScanAnother,
|
||||
onDone,
|
||||
}: SignalResolutionPanelProps) {
|
||||
@@ -38,6 +40,29 @@ export function SignalResolutionPanel({
|
||||
const ripple = useRipple();
|
||||
const actionBusy = actionState === 'playing' || actionState === 'queueing';
|
||||
|
||||
const onlineAction = (primary = false) => (
|
||||
<Pressable
|
||||
android_ripple={ripple.bounded}
|
||||
style={[
|
||||
styles.onlineButton,
|
||||
primary ? styles.onlineButtonPrimary : styles.onlineButtonSecondary,
|
||||
actionBusy && styles.disabledButton,
|
||||
]}
|
||||
onPress={onFindOnline}
|
||||
disabled={actionBusy}
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<Ionicons
|
||||
name="open-outline"
|
||||
size={18}
|
||||
color={primary ? colors.accentTextStrong : colors.textPrimary}
|
||||
/>
|
||||
<Text variant="body" color={primary ? colors.accentTextStrong : colors.textPrimary}>
|
||||
Find online
|
||||
</Text>
|
||||
</Pressable>
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<View style={styles.footerActions}>
|
||||
<Pressable
|
||||
@@ -90,6 +115,12 @@ export function SignalResolutionPanel({
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
{onlineAction(true)}
|
||||
{actionError ? (
|
||||
<Text variant="label" color={colors.warning} style={styles.centerCopy}>
|
||||
{actionError}
|
||||
</Text>
|
||||
) : null}
|
||||
{footer}
|
||||
</View>
|
||||
);
|
||||
@@ -132,6 +163,7 @@ export function SignalResolutionPanel({
|
||||
{actionError}
|
||||
</Text>
|
||||
) : null}
|
||||
{onlineAction()}
|
||||
{footer}
|
||||
</View>
|
||||
);
|
||||
@@ -194,6 +226,8 @@ export function SignalResolutionPanel({
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{onlineAction()}
|
||||
|
||||
{actionError ? (
|
||||
<Text variant="label" color={colors.warning} style={styles.centerCopy}>
|
||||
{actionError}
|
||||
@@ -264,6 +298,23 @@ const useStyles = createThemedStyles((colors) => ({
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
},
|
||||
onlineButton: {
|
||||
minHeight: 46,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: spacing.sm,
|
||||
paddingHorizontal: spacing.md,
|
||||
borderRadius: radius.sm,
|
||||
},
|
||||
onlineButtonPrimary: {
|
||||
backgroundColor: colors.accent,
|
||||
},
|
||||
onlineButtonSecondary: {
|
||||
borderWidth: StyleSheet.hairlineWidth,
|
||||
borderColor: colors.glassBorder,
|
||||
backgroundColor: colors.bgSecondary,
|
||||
},
|
||||
primaryButton: {
|
||||
minHeight: 48,
|
||||
flex: 1,
|
||||
|
||||
Reference in New Issue
Block a user