add signal online searching

This commit is contained in:
Boof2015
2026-07-15 11:26:46 -04:00
parent 0cb36167ba
commit a7a4639f70
5 changed files with 89 additions and 5 deletions
+14 -2
View File
@@ -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()}
/>
+3 -3
View File
@@ -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.');
+14
View File
@@ -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}%`));
+7
View File
@@ -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,