diff --git a/src/components/MediaCard.jsx b/src/components/MediaCard.jsx
index 237c4f0..f08238e 100644
--- a/src/components/MediaCard.jsx
+++ b/src/components/MediaCard.jsx
@@ -1,8 +1,8 @@
// Author: fruit.com.cn
-import { Play } from 'lucide-react';
+import { MoreVertical, Play } from 'lucide-react';
import { Link } from 'react-router-dom';
export function MediaCard({ video, compact = false }) {
const image = video.image?.startsWith('/') || video.image?.startsWith('http') ? video.image : `/assets/${video.image}`;
- return
{video.duration}{video.channel?.[0] || 'Y'}{video.title}{video.channel}
{video.views} · 2 hari lalu
;
+ return
{video.duration}{compact ? {video.title} : {video.channel?.[0] || 'Y'}{video.title}{video.channel}
{video.views} · 2 hari lalu
};
}
diff --git a/src/i18n/index.js b/src/i18n/index.js
index 0d2ea38..d35f4d2 100644
--- a/src/i18n/index.js
+++ b/src/i18n/index.js
@@ -1,7 +1,7 @@
// Author: fruit.com.cn
export const locales = {
- id: { code: 'id', htmlLang: 'id', title: 'YMB · Cerita pendek, emosi besar', description: 'Temukan drama pendek terbaik dan episode baru setiap hari.', search: 'Cari drama, aktor, atau genre', home: 'Beranda', shorts: 'Shorts', subscriptions: 'Langganan', library: 'Perpustakaan', history: 'Riwayat', liked: 'Drama disukai', hero: 'Cerita pendek,', accent: 'emosi besar.', watch: 'Mulai menonton', picks: 'Drama pilihan minggu ini', shortTitle: 'Cerita dalam satu tarikan napas', viewAll: 'Lihat semua →', explore: 'Jelajahi →', videoCount: 'video', channels: 'Ikuti cerita favoritmu', genres: ['Semua', 'Romansa', 'Balas dendam', 'Keluarga', 'Rahasia', 'Kisah kedua'], follow: 'Ikuti', following: 'Mengikuti', empty: 'Belum ada drama yang cocok.', loading: 'Memuat dari server…', apiFallback: 'Server belum tersedia, menampilkan katalog demo sementara.', playbackUnavailable: 'Pilih episode untuk mulai menonton.', swipeHint: 'Geser ke atas untuk menemukan cerita berikutnya' },
- en: { code: 'en', htmlLang: 'en', title: 'YMB · Short stories, big emotions', description: 'Discover the best short dramas and new episodes every day.', search: 'Search dramas, actors, or genres', home: 'Home', shorts: 'Shorts', subscriptions: 'Subscriptions', library: 'Library', history: 'History', liked: 'Liked dramas', hero: 'Short stories,', accent: 'big emotions.', watch: 'Watch now', picks: 'Drama picks this week', shortTitle: 'Stories in one breath', viewAll: 'View all →', explore: 'Explore →', videoCount: 'videos', channels: 'Follow your favorite stories', genres: ['All', 'Romance', 'Revenge', 'Family', 'Secrets', 'Second chance'], follow: 'Follow', following: 'Following', empty: 'No dramas match your search.', loading: 'Loading from server…', apiFallback: 'Server unavailable, showing the demo catalog temporarily.', playbackUnavailable: 'Select an episode to start watching.', swipeHint: 'Swipe up to discover the next story' },
- zh: { code: 'zh', htmlLang: 'zh-CN', title: 'YMB · 短故事,大情绪', description: '发现精彩短剧,每天更新新剧集。', search: '搜索短剧、演员或题材', home: '首页', shorts: 'Shorts', subscriptions: '订阅', library: '媒体库', history: '观看记录', liked: '喜欢的短剧', hero: '短故事,', accent: '大情绪。', watch: '立即观看', picks: '本周精选短剧', shortTitle: '一口气看完的故事', viewAll: '查看全部 →', explore: '探索 →', videoCount: '个视频', channels: '关注你喜欢的故事', genres: ['全部', '爱情', '复仇', '家庭', '秘密', '重生'], follow: '关注', following: '已关注', empty: '没有找到匹配的短剧。', loading: '正在从服务端加载…', apiFallback: '服务端暂不可用,暂时展示演示目录。', playbackUnavailable: '请选择剧集开始播放。', swipeHint: '向上滑动发现下一个故事' }
+ id: { code: 'id', htmlLang: 'id', title: 'YMB · Cerita pendek, emosi besar', description: 'Temukan drama pendek terbaik dan episode baru setiap hari.', search: 'Cari drama, aktor, atau genre', voiceSearch: 'Cari dengan suara', create: 'Buat', notifications: 'Notifikasi', account: 'Akun', home: 'Beranda', shorts: 'Shorts', subscriptions: 'Langganan', library: 'Perpustakaan', history: 'Riwayat', you: 'Anda', playlists: 'Playlist', watchLater: 'Tonton nanti', likedVideos: 'Video yang disukai', myVideos: 'Video saya', explore: 'Jelajahi', music: 'Musik', movies: 'Film', live: 'Live', more: 'Lainnya', liked: 'Drama disukai', hero: 'Cerita pendek,', accent: 'emosi besar.', watch: 'Mulai menonton', picks: 'Drama pilihan minggu ini', shortTitle: 'Shorts', viewAll: 'Lihat semua →', videoCount: 'video', channels: 'Ikuti cerita favoritmu', genres: ['Semua', 'Romansa', 'Balas dendam', 'Keluarga', 'Rahasia', 'Kisah kedua'], homeCategories: ['Semua', 'Podcast', 'Musik', 'Game', 'Live', 'Travel', 'Baru diupload', 'Temukan video baru'], follow: 'Ikuti', following: 'Mengikuti', empty: 'Belum ada drama yang cocok.', loading: 'Memuat dari server…', apiFallback: 'Server belum tersedia, menampilkan katalog demo sementara.', playbackUnavailable: 'Pilih episode untuk mulai menonton.', swipeHint: 'Geser ke atas untuk menemukan cerita berikutnya' },
+ en: { code: 'en', htmlLang: 'en', title: 'YMB · Short stories, big emotions', description: 'Discover the best short dramas and new episodes every day.', search: 'Search dramas, actors, or genres', voiceSearch: 'Search with your voice', create: 'Create', notifications: 'Notifications', account: 'Account', home: 'Home', shorts: 'Shorts', subscriptions: 'Subscriptions', library: 'Library', history: 'History', you: 'You', playlists: 'Playlists', watchLater: 'Watch later', likedVideos: 'Liked videos', myVideos: 'Your videos', explore: 'Explore', music: 'Music', movies: 'Movies', live: 'Live', more: 'Show more', liked: 'Liked dramas', hero: 'Short stories,', accent: 'big emotions.', watch: 'Watch now', picks: 'Drama picks this week', shortTitle: 'Shorts', viewAll: 'View all →', videoCount: 'videos', channels: 'Follow your favorite stories', genres: ['All', 'Romance', 'Revenge', 'Family', 'Secrets', 'Second chance'], homeCategories: ['All', 'Podcasts', 'Music', 'Gaming', 'Live', 'Travel', 'Recently uploaded', 'New to you'], follow: 'Follow', following: 'Following', empty: 'No dramas match your search.', loading: 'Loading from server…', apiFallback: 'Server unavailable, showing the demo catalog temporarily.', playbackUnavailable: 'Select an episode to start watching.', swipeHint: 'Swipe up to discover the next story' },
+ zh: { code: 'zh', htmlLang: 'zh-CN', title: 'YMB · 短故事,大情绪', description: '发现精彩短剧,每天更新新剧集。', search: '搜索短剧、演员或题材', voiceSearch: '通过语音搜索内容', create: '创建', notifications: '通知', account: '账号菜单', home: '首页', shorts: 'Shorts', subscriptions: '订阅', library: '媒体库', history: '观看记录', you: '我', playlists: '播放列表', watchLater: '稍后观看', likedVideos: '赞过的视频', myVideos: '我的视频', explore: '探索', music: '音乐', movies: '电影', live: '直播', more: '展开', liked: '喜欢的短剧', hero: '短故事,', accent: '大情绪。', watch: '立即观看', picks: '本周精选短剧', shortTitle: 'Shorts', viewAll: '查看全部 →', videoCount: '个视频', channels: '关注你喜欢的故事', genres: ['全部', '爱情', '复仇', '家庭', '秘密', '重生'], homeCategories: ['全部', '播客', '音乐', '游戏', '直播', '旅游', '最近上传', '发现新视频'], follow: '关注', following: '已关注', empty: '没有找到匹配的短剧。', loading: '正在从服务端加载…', apiFallback: '服务端暂不可用,暂时展示演示目录。', playbackUnavailable: '请选择剧集开始播放。', swipeHint: '向上滑动发现下一个故事' }
};
export function getLocale(code) { return locales[code] || locales.id; }
diff --git a/src/layouts/AppShell.jsx b/src/layouts/AppShell.jsx
index 22d42a7..74f782d 100644
--- a/src/layouts/AppShell.jsx
+++ b/src/layouts/AppShell.jsx
@@ -1,5 +1,5 @@
// Author: fruit.com.cn
-import { Bell, Bookmark, Compass, History, Home, Menu, Search, UserRound, X } from 'lucide-react';
+import { Bell, Bookmark, ChevronDown, ChevronRight, Compass, Film, History, Home, ListVideo, Menu, Mic, Music2, Plus, Radio, Search, ThumbsUp, UserRound, Video, X } from 'lucide-react';
import { useState } from 'react';
import { NavLink, Outlet, useNavigate } from 'react-router-dom';
import { useAppStore } from '../app/store';
@@ -9,7 +9,9 @@ export function AppShell() {
const { locale, query, setQuery, sidebarOpen, setSidebarOpen } = useAppStore();
const navigate = useNavigate();
const [searchOpen, setSearchOpen] = useState(false);
- const navItems = [[Home, locale.home, '/'], [Compass, locale.shorts, '/shorts'], [UserRound, locale.subscriptions, '/subscriptions'], [Bookmark, locale.library, '/library'], [History, locale.history, '/history']];
+ const primaryItems = [[Home, locale.home, '/'], [Compass, locale.shorts, '/shorts'], [UserRound, locale.subscriptions, '/subscriptions']];
+ const libraryItems = [[History, locale.history, '/history'], [ListVideo, locale.playlists, '/library'], [Bookmark, locale.watchLater, '/library'], [ThumbsUp, locale.likedVideos, '/library'], [Video, locale.myVideos, '/library']];
+ const exploreItems = [[Music2, locale.music, '/search?genre=music'], [Film, locale.movies, '/search?genre=movies'], [Radio, locale.live, '/shorts']];
const submitSearch = (event) => {
event.preventDefault();
navigate(`/search?q=${encodeURIComponent(query)}`);
@@ -28,17 +30,19 @@ export function AppShell() {
setQuery(event.target.value)} placeholder={locale.search} aria-label={locale.search} />
+
-
-
N
+
+
+
N
{sidebarOpen && }
>
diff --git a/src/main.jsx b/src/main.jsx
index 78dc0ef..f843dba 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -3,5 +3,6 @@ import { createRoot } from 'react-dom/client';
import { App } from './app/App';
import './styles.css';
import './enterprise.css';
+import './youtube-overrides.css';
createRoot(document.getElementById('root')).render();
diff --git a/src/pages/HomePage.jsx b/src/pages/HomePage.jsx
index ed50268..e57c51f 100644
--- a/src/pages/HomePage.jsx
+++ b/src/pages/HomePage.jsx
@@ -9,7 +9,8 @@ import { appApi } from '../services/appApi';
import { normalizeDrama, uniqueDramas } from '../services/normalizers';
import { useApi } from '../hooks/useApi';
-const SHORTS_LIMIT = 6;
+const SHORTS_LIMIT = 5;
+const FIRST_SHELF_LIMIT = 5;
const ALL_GENRES_INDEX = 0;
function normalizeHomeFeed(payload, localeCode) {
@@ -26,7 +27,8 @@ function filterByGenre(items, genre, localeGenres) {
export function HomePage() {
const { locale } = useAppStore();
- const [genre, setGenre] = useState(locale.genres[ALL_GENRES_INDEX]);
+ const categoryItems = locale.homeCategories || locale.genres;
+ const [genre, setGenre] = useState(categoryItems[ALL_GENRES_INDEX]);
const api = useApi(
() => appApi.homeFeed().then((payload) => normalizeHomeFeed(payload, locale.code)),
[locale.code],
@@ -34,34 +36,28 @@ export function HomePage() {
const remoteItems = api.data || [];
const fallbackItems = searchCatalog('', genre, locale.genres.indexOf(genre));
const items = useMemo(
- () => filterByGenre(remoteItems.length ? remoteItems : fallbackItems, genre, locale.genres),
- [fallbackItems, genre, locale.genres, remoteItems],
+ () => filterByGenre(remoteItems.length ? remoteItems : fallbackItems, genre, categoryItems),
+ [categoryItems, fallbackItems, genre, remoteItems],
);
const shortsItems = (remoteItems.length ? remoteItems : videos).slice(0, SHORTS_LIMIT);
+ const firstShelf = items.slice(0, FIRST_SHELF_LIMIT);
+ const secondShelf = items.slice(FIRST_SHELF_LIMIT);
useEffect(() => {
- setGenre(locale.genres[ALL_GENRES_INDEX]);
- }, [locale.code, locale.genres]);
+ setGenre(categoryItems[ALL_GENRES_INDEX]);
+ }, [categoryItems, locale.code]);
return (
<>
-
-
-
YMB HOME
-
{locale.picks}
-
{locale.description}
-
- {items.length} {locale.videoCount}
-
-
-
-
-
- {locale.genres.map((item) => (
+
+
+ {categoryItems.map((item) => (