- FastAPI + PostgreSQL + Alembic + React/Vite skeleton, Docker Compose, healthcheck - Google OAuth (single allowed account), encrypted refresh token storage - Subscriptions sync with pagination, uploads playlist batch fetch - Categories CRUD, many-to-many channel assignment, category filtering - Video sync (playlistItems + videos.list batching), cached feed with cursor pagination, background scheduler (APScheduler) - Video detail page with YouTube embed player - SPA fallback routing, optimistic UI updates, client-side query caching 40 backend tests covering OAuth allow-list, sync idempotency, cascade deletes, cursor pagination, and category filtering. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
28 KiB
UI/UX-спецификация: персональный YouTube-клиент
Назначение: передать coding agent вместе с основным техническим заданием.
Формат: single-user self-hosted web app, в будущем PWA/mobile client.
1. Концепция
Приложение должно ощущаться как персональная лента YouTube-подписок, но с нормальной категоризацией каналов и локальным хранением выбранных видео.
Пользовательский сценарий:
Открыть приложение
→ выбрать категорию
→ увидеть последние видео каналов этой категории
→ смотреть через YouTube
→ при желании нажать "Скачать"
→ после загрузки видеть "На сервере"
→ дальше смотреть локальную копию через MeTube
Интерфейс не должен выглядеть как admin panel. Это consumer media UI.
2. Общий layout
Desktop:
┌──────────────────────────────────────────────────────────────────────┐
│ Header │
├──────────────────────┬───────────────────────────────────────────────┤
│ Sidebar │ Main content │
│ │ │
│ Все видео │ Video feed │
│ Без категории │ │
│ На сервере │ │
│ │ │
│ КАТЕГОРИИ │ │
│ Linux │ │
│ Шахматы │ │
│ Научпоп │ │
│ │ │
│ Каналы │ │
│ Настройки │ │
└──────────────────────┴───────────────────────────────────────────────┘
Sidebar: 220–260 px.
Header: фиксированный сверху.
Main content: responsive grid.
Mobile: sidebar превращается в drawer/hamburger.
3. Визуальный стиль
Тёмная тема по умолчанию.
Рекомендуемые design tokens:
Background main #0F0F0F
Surface #181818
Surface hover #272727
Border #303030
Primary text #F1F1F1
Secondary text #AAAAAA
Accent #3EA6FF
Success/local #2BA640
Warning/downloading #F5A623
Error #E53935
Использовать CSS variables, а не раскидывать цвета по компонентам.
Типографика:
Inter / Roboto / system-ui
Page title 24px / 600
Section title 18px / 600
Video title 15–16px / 500
Metadata 12–14px
Buttons 14px / 500
4. Header
Пример:
┌──────────────────────────────────────────────────────────────────────┐
│ ▶ MyTube 🔍 Поиск... ✓ 12 мин ⚙ │
└──────────────────────────────────────────────────────────────────────┘
Слева:
- логотип;
- рабочее название
MyTubeили configurable app name.
Центр:
- поиск по локальной БД: video title + channel title.
Справа:
- sync indicator;
- settings.
Sync states:
✓ Обновлено 12 мин назад
↻ Обновление...
⚠ Ошибка синхронизации
По клику на sync indicator:
Подписки: обновлены 20:11
Видео: обновлены 20:12
MeTube: доступен
5. Sidebar
Пример:
● Все видео
Без категории
На сервере
КАТЕГОРИИ
🐧 Linux
♟ Шахматы
🔬 Научпоп
🎮 Игры
+ Новая категория
────────────
Каналы
Настройки
Активная категория должна иметь заметный, но спокойный selected state.
Все видео, Без категории, На сервере — виртуальные категории.
6. Feed
Route:
/
или:
/feed
Header выбранной категории:
Linux
24 канала
Обновлено 12 минут назад [↻ Обновить]
Опциональные chips:
[Все] [На сервере] [Не скачаны]
Сортировка видео:
published_at DESC
7. Video grid
Responsive:
>= 1800 px → 5 columns
>= 1400 px → 4 columns
>= 1000 px → 3 columns
>= 700 px → 2 columns
mobile → 1 column
Рекомендуемый CSS:
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
8. Video card
Пример:
┌────────────────────────────────────┐
│ │
│ THUMBNAIL │
│ 18:42 │
│ │
├────────────────────────────────────┤
│ Linux Kernel 7.0 — What's New │
│ The Linux Experiment │
│ 2 часа назад │
│ │
│ [▶ Смотреть] [⬇ Скачать] │
└────────────────────────────────────┘
Thumbnail:
- aspect ratio 16:9;
- duration badge справа снизу.
Карточка содержит:
- thumbnail;
- title;
- channel name;
- publish time;
- duration;
- download/local state;
- Watch;
- Download.
9. Состояния скачивания
Не скачано
[▶ Смотреть] [⬇ Скачать]
В очереди
[▶ Смотреть] [В очереди...]
Скачивается
[▶ Смотреть] [64%]
Дополнительно progress-line внизу thumbnail.
Post-processing
[▶ Смотреть] [Обработка...]
На сервере
[▶ Смотреть] [✓ На сервере]
Ошибка
[▶ Смотреть] [⚠ Повторить]
Не показывать traceback.
10. Video page
Route:
/video/:youtubeVideoId
Desktop:
┌──────────────────────────────────────────────────────────────────┐
│ │
│ VIDEO PLAYER │
│ │
└──────────────────────────────────────────────────────────────────┘
Linux Kernel 7.0 — What's New
The Linux Experiment
2 часа назад [⬇ Скачать]
Источник: YouTube
[Linux] [IT]
────────────────────────────────────────────────────────────────────
Описание видео...
Если local copy существует:
Источник: ● Локальная копия · mediaVM
и вместо YouTube iframe используется HTML5 <video>.
Пользователь не выбирает источник вручную по умолчанию.
Логика:
local available?
yes → local player
no → YouTube player
Если local player вернул ошибку/404:
Локальная копия недоступна.
Переключено на YouTube.
11. Channels page
Route:
/channels
Главная задача — быстро распределять подписки по категориям.
Пример:
Каналы
[Поиск каналов...] [Только без категории]
┌────────────────────────────────────────────────────┐
│ ○ The Linux Experiment │
│ [Linux ×] [IT ×] [+ Категория] │
└────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────┐
│ ○ GothamChess │
│ [Шахматы ×] [+ Категория] │
└────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────┐
│ ○ Veritasium │
│ [Научпоп ×] [+ Категория] │
└────────────────────────────────────────────────────┘
Не делать отдельную страницу редактирования для каждого канала.
12. Category picker
По кнопке:
+ Категория
показывать popover:
Категории
☑ Linux
☑ IT
☐ Научпоп
☐ Игры
──────────────
+ Создать новую
Checkbox сохраняется сразу.
Отдельная кнопка Сохранить не нужна.
13. Массовая категоризация
Желательная функция.
Пользователь отмечает несколько каналов:
☑ Channel A
☑ Channel B
☑ Channel C
Появляется action bar:
Выбрано: 3
[Добавить в категорию] [Убрать из категории] [Отмена]
Если MVP затягивается, допускается реализовать после базовой версии.
14. Category management
Route:
/settings/categories
Пример:
Категории
≡ Linux 24 канала ✎ ⋮
≡ Шахматы 12 каналов ✎ ⋮
≡ Научпоп 8 каналов ✎ ⋮
[+ Новая категория]
Поддержать:
- create;
- rename;
- delete;
- reorder.
При удалении:
Удалить категорию "Linux"?
Каналы удалены не будут.
Они просто перестанут относиться к этой категории.
[Отмена] [Удалить]
15. Virtual category "Без категории"
Показывает каналы/видео каналов, которые ещё не распределены.
Header:
Без категории
17 каналов пока не распределены.
[Перейти к каналам]
16. Virtual category "На сервере"
Показывает видео:
download_status = completed
Это простая локальная библиотека без отдельного сложного media-manager.
17. Empty states
Нет категорий
Категорий пока нет
Раздели подписки по темам:
Linux, Шахматы, Научпоп и т.д.
[Создать первую категорию]
В категории нет видео
Здесь пока нет видео
Добавь каналы в эту категорию
или обнови подписки.
[Добавить каналы]
YouTube ещё не подключён
Подключи свой YouTube
Мы импортируем список подписок и последние видео.
Доступ к YouTube только read-only.
[Подключить YouTube]
18. Loading states
Не использовать fullscreen spinner.
Использовать skeleton video cards.
Background sync не должен блокировать интерфейс.
19. Error UX
Global toast:
Не удалось связаться с MeTube
или:
YouTube временно недоступен
Critical banner:
⚠ Требуется повторная авторизация YouTube
[Подключить заново]
Background sync не должен постоянно спамить toast notifications.
20. Settings
Route:
/settings
Sections:
YouTube
MeTube
Синхронизация
Интерфейс
О приложении
YouTube
Google account:
user@example.com
Статус: Подключено
Последняя синхронизация:
Сегодня, 20:15
[Обновить сейчас]
[Переподключить]
MeTube
MeTube
Статус: ● Доступен
Сервер: mediaVM
Адрес: 192.168.8.177:8081
[Проверить соединение]
Не показывать на основном UI filesystem path или internal job IDs.
Sync
Подписки: каждые 6 часов
Видео: каждые 60 минут
Последний запуск:
20:12
[Синхронизировать сейчас]
21. First-run onboarding
Flow:
Open app
→ Подключить YouTube
→ Google OAuth
→ initial sync
→ channels imported
→ предложить категоризацию
После initial sync:
Готово!
Импортировано:
143 канала
Теперь можно распределить их по категориям.
[Настроить категории]
[Сделать позже]
22. Mobile UX
Header:
☰ MyTube ↻
Категории можно показывать horizontal chips:
[Все] [Linux] [Шахматы] [Научпоп] →
Video card:
┌───────────────────────────────┐
│ │
│ Thumbnail │
│ │
└───────────────────────────────┘
Video title
Channel · 2 часа назад
[▶ Смотреть] [⬇ Скачать]
Touch targets минимум ~44×44 px.
23. Navigation routes
Рекомендуемые routes:
/ all feed
/category/:categoryId category feed
/channels channels
/video/:youtubeVideoId video detail/player
/local local videos
/uncategorized uncategorized
/settings settings
/settings/categories category management
Browser Back после просмотра видео должен возвращать:
- в ту же категорию;
- примерно на ту же scroll position.
24. Components
Frontend должен быть разделён на reusable components:
AppShell
Header
Sidebar
CategoryNav
VideoGrid
VideoCard
VideoThumbnail
DownloadButton
DownloadStatus
Player
YouTubePlayer
LocalPlayer
ChannelRow
CategoryPicker
CategoryChip
SyncIndicator
EmptyState
ErrorBanner
Toast
ConfirmDialog
SkeletonVideoCard
Не делать весь UI одним React component.
25. Feed API object
Желательно, чтобы backend сразу отдавал frontend всё необходимое для одной карточки:
{
"youtube_video_id": "abc123",
"title": "Linux Kernel 7.0",
"channel": {
"id": 12,
"youtube_channel_id": "UC...",
"title": "The Linux Experiment",
"thumbnail_url": "..."
},
"thumbnail_url": "...",
"published_at": "2026-09-16T18:00:00Z",
"duration_seconds": 1042,
"categories": [
{
"id": 2,
"name": "Linux"
}
],
"local": {
"available": true,
"status": "completed",
"progress_percent": 100,
"media_url": "http://..."
}
}
Запрещён frontend pattern:
GET feed
→ отдельный request channel для каждой карточки
→ отдельный request download status для каждой карточки
Не должно быть N+1 API requests.
26. Desktop wireframe
┌────────────────────────────────────────────────────────────────────────────┐
│ ▶ MyTube 🔍 Поиск ✓ 12 мин ⚙ │
├──────────────────────┬─────────────────────────────────────────────────────┤
│ │ │
│ ● Все видео │ Linux │
│ Без категории │ 24 канала [↻ Обновить] │
│ На сервере │ │
│ │ [Все] [На сервере] [Не скачаны] │
│ КАТЕГОРИИ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ 🐧 Linux │ │ thumbnail │ │ thumbnail │ │ thumbnail │ │
│ ♟ Шахматы │ │ 18:42│ │ 12:03│ │ 8:41│ │
│ 🔬 Научпоп │ └─────────────┘ └─────────────┘ └─────────────┘ │
│ 🎮 Игры │ Kernel 7.0... NixOS update... New GNOME... │
│ │ Linux Exp. Brodie R. DistroTube │
│ + Категория │ 2 часа назад 4 часа назад вчера │
│ │ [▶] [⬇] [▶] [64%] [▶] [✓ Local] │
│ ───────────── │ │
│ Каналы │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ Настройки │ │ │ │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │
└──────────────────────┴─────────────────────────────────────────────────────┘
27. Channels wireframe
┌──────────────────────────────────────────────────────────────────────┐
│ Каналы │
│ │
│ 🔍 Поиск каналов... [Только без категории] │
│ │
├──────────────────────────────────────────────────────────────────────┤
│ ○ The Linux Experiment │
│ [Linux ×] [IT ×] [+ Категория] │
├──────────────────────────────────────────────────────────────────────┤
│ ○ GothamChess │
│ [Шахматы ×] [+ Категория] │
├──────────────────────────────────────────────────────────────────────┤
│ ○ Veritasium │
│ [Научпоп ×] [+ Категория] │
├──────────────────────────────────────────────────────────────────────┤
│ ○ Some Channel │
│ [+ Категория] │
└──────────────────────────────────────────────────────────────────────┘
28. Video page wireframe
┌──────────────────────────────────────────────────────────────────────┐
│ │
│ VIDEO PLAYER │
│ │
└──────────────────────────────────────────────────────────────────────┘
Linux Kernel 7.0 — What's New
The Linux Experiment
2 часа назад
● Локальная копия · mediaVM [✓ На сервере]
[Linux] [IT]
────────────────────────────────────────────────────────────────────────
Описание видео...
Когда local отсутствует:
Источник: YouTube [⬇ Скачать]
29. UX-антипаттерны
Не делать:
- Bootstrap-like admin tables как основной UI;
- raw JSON;
- container names/job IDs на главном экране;
- filesystem paths;
- огромные формы;
- отдельный Save после каждого checkbox;
- модальное окно на каждое простое действие;
- fullscreen spinner при background sync;
- N+1 API requests;
- слишком много badges на thumbnail;
- сильные hover zoom animations.
30. Accessibility
Минимум:
- semantic HTML;
- keyboard navigation;
- visible focus ring;
- aria-label для icon buttons;
- alt у изображений;
- нормальный contrast;
<button>для кнопок;- состояние не должно кодироваться только цветом.
31. Icons
Использовать один icon set, например Lucide React.
Основные icons:
Home
Folder
Download
Play
Check
RefreshCw
Settings
Search
MoreVertical
AlertTriangle
Server
Youtube
Menu
32. Анимации
Минимально:
- hover 150–200 ms;
- popover fade;
- drawer slide;
- progress;
- skeleton pulse.
Не делать сложные page transitions.
33. Рекомендуемый порядок реализации UI
Сначала сделать интерфейс на mock data:
App shell
Sidebar
Video grid
Video card
Channels page
Category picker
Video page
Mobile responsive
Только после визуальной проверки подключать реальные backend API.
34. Definition of Done UI
UI MVP готов, если:
- Пользователь может подключить YouTube.
- После sync видит подписанные каналы.
- Может создать категорию.
- Может назначить каналу одну или несколько категорий.
- Может выбрать категорию и увидеть соответствующую video feed.
- Карточка показывает thumbnail/title/channel/time/duration.
- Можно открыть video page.
- Нескачанный ролик играет через YouTube.
- Работает Download.
- Отображаются queued/downloading/completed/failed.
- Completed ролик имеет статус
На сервере. - Completed ролик играет через MeTube local URL.
- Есть loading/error/empty states.
- UI usable на desktop и mobile.
- Browser refresh сохраняет route.
- UI не показывает secrets/internal paths/job IDs.
- На feed нет N+1 requests.
- Основной экран воспринимается как видеоприложение, а не административная панель.
35. Главное UX-правило
Пользователь должен воспринимать сервис так:
Это моя лента YouTube-подписок, только каналы нормально разложены по темам, а понравившееся видео я могу одним кликом оставить у себя.
Технические детали:
OAuth
YouTube Data API
PostgreSQL
MeTube
Socket.IO
mediaVM
filenames
должны быть скрыты за простым интерфейсом.