Share what's playing in any Android music app
A local Expo module listens to system media sessions (the only way Android exposes other apps' playback, gated by notification access; we never read notifications). It filters to known music apps, debounces and deduplicates changes, sends a heartbeat while playing, and delivers reports through WorkManager so they survive the app being closed. - One-screen onboarding that doubles as Google Play's prominent disclosure, opening straight to the app's toggle in Android settings - Only media notifications are pre-ticked on that settings screen - Device registration is single-flight (several screens react to the same permission change) and self-heals if the server revokes a token - Sign-out revokes the phone's token; resuming sharing re-sends the current track immediately - Pause controls in Settings and on the feed - Fix invisible tab icons and labels on Android - Keep Metro out of build output so it doesn't exhaust file watchers Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
@@ -34,7 +34,7 @@ export function NowPlayingCard({
|
||||
{np.isPlaying && <View style={styles.dot} />}
|
||||
<Text style={[styles.status, np.isPlaying && { color: colors.live }]} numberOfLines={1}>
|
||||
{np.isPlaying ? 'Listening now' : `Played ${timeAgo(np.updatedAt)}`}
|
||||
<Text style={styles.via}> · {SERVICE_LABELS[np.source]}</Text>
|
||||
<Text style={styles.via}> · {np.sourceApp ?? SERVICE_LABELS[np.source]}</Text>
|
||||
</Text>
|
||||
{onTogglePin && (
|
||||
<Pressable
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { useState } from 'react';
|
||||
import { pausedUntilLabel } from '@musicshare/shared';
|
||||
import { api } from '@/lib/api';
|
||||
import { showError } from '@/lib/errors';
|
||||
import { resendCurrentPlayback } from '@/lib/nowPlayingReader';
|
||||
import { useSession } from '@/lib/session';
|
||||
import { Body, Button, Card, Heading, Row } from './ui';
|
||||
|
||||
/** Pause/resume sharing. Compact mode is the banner at the top of the feed. */
|
||||
export function SharingControls({ compact }: { compact?: boolean }) {
|
||||
const { me, setMe } = useSession();
|
||||
const [busy, setBusy] = useState(false);
|
||||
if (!me) return null;
|
||||
|
||||
const set = async (pause: '1h' | '8h' | 'indefinitely' | null) => {
|
||||
setBusy(true);
|
||||
try {
|
||||
setMe(await api.setSharing(pause));
|
||||
if (pause === null) resendCurrentPlayback();
|
||||
} catch (err) {
|
||||
showError(err);
|
||||
}
|
||||
setBusy(false);
|
||||
};
|
||||
|
||||
if (compact) {
|
||||
if (!me.sharingPausedUntil) return null;
|
||||
return (
|
||||
<Card>
|
||||
<Body>Sharing is paused {pausedUntilLabel(me.sharingPausedUntil)}.</Body>
|
||||
<Button small variant="primary" title="Resume" loading={busy} onPress={() => set(null)} style={{ alignSelf: 'flex-start' }} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Heading>Sharing</Heading>
|
||||
{me.sharingPausedUntil ? (
|
||||
<>
|
||||
<Body muted style={{ fontSize: 13 }}>
|
||||
Paused {pausedUntilLabel(me.sharingPausedUntil)}. Friends don’t see what you play, and nothing is added to your history.
|
||||
</Body>
|
||||
<Button small variant="primary" title="Resume sharing" loading={busy} onPress={() => set(null)} style={{ alignSelf: 'flex-start' }} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Body muted style={{ fontSize: 13 }}>
|
||||
Friends see what you’re playing. Pause any time.
|
||||
</Body>
|
||||
<Row>
|
||||
<Button small title="1 hour" disabled={busy} onPress={() => set('1h')} />
|
||||
<Button small title="Until tomorrow" disabled={busy} onPress={() => set('8h')} />
|
||||
<Button small title="Until I turn it back on" disabled={busy} onPress={() => set('indefinitely')} />
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user