feat(profile): avatar presets, upload, nickname editing
- FastAPI: preset assets 01–08, GET list/static, PUT /me/avatar/preset, safer uploaded-avatar path validation, preset_avatars + HTTP tests. - Expo: personal-info (library + presets), profile tab avatar, resolveApiMediaUrl, auth hooks cache sync, Web multipart helper, partial-save messaging + profile i18n. - Includes existing edits to conversation screen and voice use-player. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -15,6 +15,10 @@ interface UsePlayerResult {
|
||||
enqueue: (item: PlaybackItem) => void;
|
||||
/** Replace queue and play this item (e.g. user voice bubble vs other sources). */
|
||||
enqueueExclusive: (item: PlaybackItem) => Promise<void>;
|
||||
/** Pause native playback without draining queue(与 stop 清空队列不同)。 */
|
||||
pausePlayback: () => void;
|
||||
/** Continue after pausePlayback(需 status === 'paused') */
|
||||
resumePlayback: () => void;
|
||||
stop: () => void;
|
||||
}
|
||||
|
||||
@@ -68,9 +72,11 @@ export function usePlayer(): UsePlayerResult {
|
||||
useEffect(() => {
|
||||
if (!currentSource || !player) return;
|
||||
if (!playerStatus.isLoaded) return;
|
||||
/** 先于 isLoaded「抢暂停」时需保留暂停,避免本条自动 play 覆盖 pause */
|
||||
if (status === 'paused') return;
|
||||
player.play();
|
||||
isPlayingRef.current = true;
|
||||
}, [currentSource, player, playerStatus.isLoaded]);
|
||||
}, [currentSource, player, playerStatus.isLoaded, status]);
|
||||
|
||||
const playNext = useCallback(async () => {
|
||||
if (isPlayNextInProgressRef.current) return;
|
||||
@@ -114,6 +120,7 @@ export function usePlayer(): UsePlayerResult {
|
||||
|
||||
// Detect playback completion → advance queue(必须曾 playing,避免换源瞬间沿用上一条的 duration/currentTime)
|
||||
useEffect(() => {
|
||||
if (status === 'paused') return;
|
||||
if (!currentSource || !isPlayingRef.current) return;
|
||||
|
||||
const { playing, currentTime, duration } = playerStatus;
|
||||
@@ -128,7 +135,32 @@ export function usePlayer(): UsePlayerResult {
|
||||
isPlayingRef.current = false;
|
||||
playNext();
|
||||
}
|
||||
}, [playerStatus, currentSource, playNext]);
|
||||
}, [playerStatus, currentSource, playNext, status]);
|
||||
|
||||
const pausePlayback = useCallback(() => {
|
||||
setStatus((s) => {
|
||||
if (s !== 'playing') return s;
|
||||
if (player) {
|
||||
player.pause();
|
||||
}
|
||||
isPlayingRef.current = false;
|
||||
return 'paused';
|
||||
});
|
||||
}, [player]);
|
||||
|
||||
const resumePlayback = useCallback(async () => {
|
||||
if (status !== 'paused') return;
|
||||
const acquired = await audioFocus.acquireForPlayback();
|
||||
if (!acquired) {
|
||||
setStatus('idle');
|
||||
return;
|
||||
}
|
||||
if (!player) return;
|
||||
if (!playerStatus.isLoaded) return;
|
||||
player.play();
|
||||
setStatus('playing');
|
||||
isPlayingRef.current = true;
|
||||
}, [status, player, playerStatus.isLoaded]);
|
||||
|
||||
// Subscribe to audioFocus owner changes for recorder → idle recovery
|
||||
useEffect(() => {
|
||||
@@ -205,6 +237,8 @@ export function usePlayer(): UsePlayerResult {
|
||||
currentPlaybackItem,
|
||||
enqueue,
|
||||
enqueueExclusive,
|
||||
pausePlayback,
|
||||
resumePlayback,
|
||||
stop,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user