Split shorts from regular videos with a vertical shorts feed
- Detect shorts by duration (SHORTS_MAX_DURATION_SECONDS, default 180)
and expose is_short in feed, video and channel-activity DTOs.
- Feed gains type=all|long|short and anchor; lists show a two-mode
'Обычные | Shorts' filter (no 'Все') persisted in the URL.
- Clicking a short opens /shorts/🆔 a vertical scroll-snap feed with
autoplay for the active slide, context-aware endpoints and infinite
loading.
- Keep the sound choice across swipes, syncing with the player's own
mute control and guarding against the widget's stale isMuted() reads.
This commit is contained in:
parent
43adec5224
commit
252f38597d
25 changed files with 1297 additions and 88 deletions
|
|
@ -64,12 +64,21 @@ function Sidebar({ close }: { close: () => void }) {
|
|||
const categoriesQuery = useQuery({ queryKey: ['categories'], queryFn: listCategories })
|
||||
const categories = categoriesQuery.data ?? []
|
||||
const location = useLocation()
|
||||
// Keep the current view mode ("Лента | Каналы") when switching between
|
||||
// "Все видео" and category links; badges (?new=1) are feed-specific.
|
||||
const viewQuery = new URLSearchParams(location.search).get('view') === 'channels' ? '?view=channels' : ''
|
||||
// Keep the current view mode ("Лента | Каналы") and the video-type filter
|
||||
// ("Обычные | Shorts") when switching between "Все видео" and category
|
||||
// links; badges (?new=1) are feed-specific. Only `?type=short` is carried —
|
||||
// "Обычные" is the default and has no parameter.
|
||||
const currentParams = new URLSearchParams(location.search)
|
||||
const preservedParams = new URLSearchParams()
|
||||
if (currentParams.get('view') === 'channels') preservedParams.set('view', 'channels')
|
||||
const activeType = currentParams.get('type') === 'short' ? 'short' : null
|
||||
if (activeType) preservedParams.set('type', activeType)
|
||||
const viewQuery = preservedParams.toString() ? `?${preservedParams.toString()}` : ''
|
||||
const typeQuery = activeType ? '?type=short' : ''
|
||||
const typeSuffix = activeType ? '&type=short' : ''
|
||||
const links = [
|
||||
{ to: `/${viewQuery}`, icon: 'home' as const, text: 'Все видео', end: true },
|
||||
{ to: '/local', icon: 'server' as const, text: 'На сервере' },
|
||||
{ to: `/local${typeQuery}`, icon: 'server' as const, text: 'На сервере' },
|
||||
]
|
||||
return <nav className="sidebar-nav" aria-label="Основная навигация">
|
||||
<div className="sidebar-group">
|
||||
|
|
@ -79,7 +88,7 @@ function Sidebar({ close }: { close: () => void }) {
|
|||
<div className="sidebar-heading">Категории</div>
|
||||
{categories.map((category) => <div key={category.id} className="sidebar-category-row">
|
||||
<NavLink to={`/category/${category.id}${viewQuery}`} onClick={close} className={({ isActive }) => `sidebar-link category-link ${isActive ? 'active' : ''}`}><span className="category-dot" /><span className="truncate">{category.name}</span></NavLink>
|
||||
{category.new_videos_count > 0 && <Link to={`/category/${category.id}?new=1`} onClick={close} className="sidebar-badge sidebar-badge-link" aria-label={`${category.new_videos_count} новых видео в категории «${category.name}»`}>{category.new_videos_count}</Link>}
|
||||
{category.new_videos_count > 0 && <Link to={`/category/${category.id}?new=1${typeSuffix}`} onClick={close} className="sidebar-badge sidebar-badge-link" aria-label={`${category.new_videos_count} новых видео в категории «${category.name}»`}>{category.new_videos_count}</Link>}
|
||||
</div>)}
|
||||
{categories.length === 0 && !categoriesQuery.isLoading && <p className="sidebar-hint">Пока нет категорий</p>}
|
||||
<Link to="/settings/categories" onClick={close} className="sidebar-link sidebar-add"><Icon name="plus" size={18} /><span>Новая категория</span></Link>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue