feat(social): add listening activity, shared rooms and modern chat
ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
ci / api / api (push) Canceled after 0s
ci / module boundaries / boundaries (push) Canceled after 0s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Canceled after 11m29s
ci / db / db (push) Canceled after 11m22s
ci / mobile / mobile (Metro-бандл Android) (push) Canceled after 6m43s
ci / recommender / recommender (push) Canceled after 6m47s
ci / ts / ts (packages + web) (push) Canceled after 1s
This commit is contained in:
1 parent
0a05bfe2cd
commit
11a16cd023
59 files changed
+5022
-542
No files matched your search
@@ -39,6 +39,7 @@ import { SearchBox } from './SearchBox';
|
||||
import s from './AppShell.module.css';
|
||||
import { DesktopUpdateDialog, DesktopOfflineBanner } from '../features/desktop/DesktopFeatures';
|
||||
import { useSocialUnread, SocialProvider } from '../features/social/Social';
|
||||
import { ListeningBanner } from '../features/social/Listening';
|
||||
|
||||
type NavPath =
|
||||
| '/friends'
|
||||
@@ -312,6 +313,7 @@ function ShellMain() {
|
||||
return (
|
||||
<main id="content" className={s.content} tabIndex={-1}>
|
||||
<DesktopOfflineBanner />
|
||||
<ListeningBanner />
|
||||
<div key={key} className={s.routeIn}>
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<Outlet />
|
||||
|
||||
@@ -40,6 +40,7 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
const player = usePlayer();
|
||||
const openShare = useMusicShare();
|
||||
const st = usePlayerState();
|
||||
const locked = st.listening?.locked === true;
|
||||
const narrow = useNarrow();
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
const [queueOpen, setQueueOpen] = useState(false);
|
||||
@@ -132,7 +133,7 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
>
|
||||
<Icon src={track.reaction === 'dislike' ? dislikeFillIcon : dislikeIcon} />
|
||||
</button>
|
||||
<button type="button" className={s.icon} aria-label={t('player.prev')} onClick={() => player.prev()}>
|
||||
<button type="button" className={s.icon} aria-label={t('player.prev')} disabled={locked} onClick={() => player.prev()}>
|
||||
<Icon src={prevIcon} size={26} />
|
||||
</button>
|
||||
<button
|
||||
@@ -140,11 +141,11 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
className={s.play}
|
||||
aria-label={t(st.playing ? 'player.pause' : 'player.play')}
|
||||
aria-busy={st.loading || undefined}
|
||||
onClick={() => player.toggle()}
|
||||
disabled={locked} onClick={() => player.toggle()}
|
||||
>
|
||||
<Icon src={st.playing ? pauseIcon : playIcon} size={28} />
|
||||
</button>
|
||||
<button type="button" className={s.icon} aria-label={t('player.next')} onClick={() => player.next()}>
|
||||
<button type="button" className={s.icon} aria-label={t('player.next')} disabled={locked} onClick={() => player.next()}>
|
||||
<Icon src={nextIcon} size={26} />
|
||||
</button>
|
||||
<button
|
||||
@@ -168,7 +169,7 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
value={Math.min(st.positionMs, st.durationMs || track.duration_ms)}
|
||||
aria-label={t('player.progress')}
|
||||
aria-valuetext={formatDuration(st.positionMs)}
|
||||
onChange={(e) => player.seek(Number(e.target.value))}
|
||||
disabled={locked} onChange={(e) => player.seek(Number(e.target.value))}
|
||||
style={{ '--pct': `${(st.positionMs / (st.durationMs || track.duration_ms)) * 100}%` } as CSSProperties}
|
||||
/>
|
||||
<span className={s.time}>{formatDuration(st.durationMs || track.duration_ms)}</span>
|
||||
@@ -190,7 +191,7 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
className={cx(s.icon, st.shuffle && s.accent)}
|
||||
aria-label={t('player.shuffle')}
|
||||
aria-pressed={st.shuffle}
|
||||
onClick={() => player.toggleShuffle()}
|
||||
disabled={locked} onClick={() => player.toggleShuffle()}
|
||||
>
|
||||
<Icon src={shuffleIcon} size={20} />
|
||||
</button>
|
||||
@@ -199,7 +200,7 @@ export function PlayerBar({ variant = 'bar' }: { variant?: 'bar' | 'island' }) {
|
||||
className={cx(s.icon, st.repeat !== 'off' && s.accent)}
|
||||
aria-label={repeatLabel}
|
||||
title={repeatLabel}
|
||||
onClick={() => player.cycleRepeat()}
|
||||
disabled={locked} onClick={() => player.cycleRepeat()}
|
||||
>
|
||||
<Icon src={st.repeat === 'one' ? repeatOneIcon : repeatIcon} size={20} />
|
||||
</button>
|
||||
@@ -271,6 +272,7 @@ function NowPlayingSheet({
|
||||
const { t } = useTranslation();
|
||||
const player = usePlayer();
|
||||
const st = usePlayerState();
|
||||
const locked = st.listening?.locked === true;
|
||||
const track = st.track;
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
@@ -321,7 +323,7 @@ function NowPlayingSheet({
|
||||
step={1000}
|
||||
value={Math.min(st.positionMs, st.durationMs || track.duration_ms)}
|
||||
aria-label={t('player.progress')}
|
||||
onChange={(e) => player.seek(Number(e.target.value))}
|
||||
disabled={locked} onChange={(e) => player.seek(Number(e.target.value))}
|
||||
style={{ '--pct': `${(st.positionMs / (st.durationMs || track.duration_ms)) * 100}%` } as CSSProperties}
|
||||
/>
|
||||
<span className={s.time}>{formatDuration(st.durationMs || track.duration_ms)}</span>
|
||||
@@ -330,13 +332,13 @@ function NowPlayingSheet({
|
||||
<button type="button" className={cx(s.icon, track.reaction === 'dislike' && s.danger)} aria-label={t(track.reaction === 'dislike' ? 'player.undislike' : 'player.dislike')} aria-pressed={track.reaction === 'dislike'} onClick={() => react('dislike')}>
|
||||
<Icon src={track.reaction === 'dislike' ? dislikeFillIcon : dislikeIcon} />
|
||||
</button>
|
||||
<button type="button" className={s.icon} aria-label={t('player.prev')} onClick={() => player.prev()}>
|
||||
<button type="button" className={s.icon} aria-label={t('player.prev')} disabled={locked} onClick={() => player.prev()}>
|
||||
<Icon src={prevIcon} size={28} />
|
||||
</button>
|
||||
<button type="button" className={s.play} aria-label={t(st.playing ? 'player.pause' : 'player.play')} onClick={() => player.toggle()}>
|
||||
<button type="button" className={s.play} aria-label={t(st.playing ? 'player.pause' : 'player.play')} disabled={locked} onClick={() => player.toggle()}>
|
||||
<Icon src={st.playing ? pauseIcon : playIcon} size={32} />
|
||||
</button>
|
||||
<button type="button" className={s.icon} aria-label={t('player.next')} onClick={() => player.next()}>
|
||||
<button type="button" className={s.icon} aria-label={t('player.next')} disabled={locked} onClick={() => player.next()}>
|
||||
<Icon src={nextIcon} size={28} />
|
||||
</button>
|
||||
<button type="button" className={cx(s.icon, track.reaction === 'like' && s.accent)} aria-label={t(track.reaction === 'like' ? 'player.unlike' : 'player.like')} aria-pressed={track.reaction === 'like'} onClick={() => react('like')}>
|
||||
@@ -344,10 +346,10 @@ function NowPlayingSheet({
|
||||
</button>
|
||||
</div>
|
||||
<div className={s.sheetExtra}>
|
||||
<button type="button" className={cx(s.icon, st.shuffle && s.accent)} aria-label={t('player.shuffle')} aria-pressed={st.shuffle} onClick={() => player.toggleShuffle()}>
|
||||
<button type="button" className={cx(s.icon, st.shuffle && s.accent)} aria-label={t('player.shuffle')} aria-pressed={st.shuffle} disabled={locked} onClick={() => player.toggleShuffle()}>
|
||||
<Icon src={shuffleIcon} size={22} />
|
||||
</button>
|
||||
<button type="button" className={cx(s.icon, st.repeat !== 'off' && s.accent)} aria-label={repeatLabel} onClick={() => player.cycleRepeat()}>
|
||||
<button type="button" className={cx(s.icon, st.repeat !== 'off' && s.accent)} aria-label={repeatLabel} disabled={locked} onClick={() => player.cycleRepeat()}>
|
||||
<Icon src={st.repeat === 'one' ? repeatOneIcon : repeatIcon} size={22} />
|
||||
</button>
|
||||
<button type="button" className={s.icon} aria-label={t('player.share')} onClick={() => void share()}>
|
||||
@@ -379,6 +381,7 @@ function QueueDrawer({ id, variant, onClose }: { id: string; variant: 'bar' | 'i
|
||||
const { t } = useTranslation();
|
||||
const player = usePlayer();
|
||||
const st = usePlayerState();
|
||||
const locked = st.listening?.locked === true;
|
||||
const upcoming = st.queue.slice(st.index, st.index + 40);
|
||||
const close = useRef<HTMLButtonElement>(null);
|
||||
// Клавиатура: при открытии фокус в панель, Escape закрывает.
|
||||
@@ -408,7 +411,7 @@ function QueueDrawer({ id, variant, onClose }: { id: string; variant: 'bar' | 'i
|
||||
type="button"
|
||||
className={cx(s.queueRow, k === 0 && s.queueCurrent)}
|
||||
aria-current={k === 0 || undefined}
|
||||
onClick={() => player.jump(st.index + k)}
|
||||
disabled={st.listening?.locked} onClick={() => player.jump(st.index + k)}
|
||||
>
|
||||
<Cover album={tr.album} size={40} />
|
||||
<span className={s.meta}>
|
||||
|
||||
Reference in new issue
Block a user